|
|
@@ -68,7 +68,10 @@ class SQLiteListDB:
|
|
|
if top:
|
|
|
sql = re.sub(r'SELECT TOP \d+', 'SELECT', sql, count=1) + ' LIMIT ' + top[1]
|
|
|
sql = sql.replace('nvarchar(max)', 'TEXT')
|
|
|
- cursor = self.connection.execute(sql, values)
|
|
|
+ # Keep the captured production parameters typed, but adapt dates explicitly
|
|
|
+ # for SQLite rather than relying on its deprecated implicit adapter.
|
|
|
+ cursor = self.connection.execute(sql, {key: value.isoformat() if isinstance(value, date) else value
|
|
|
+ for key, value in values.items()})
|
|
|
columns = [item[0] for item in cursor.description]
|
|
|
record = namedtuple('Result', columns)
|
|
|
rows = []
|
|
|
@@ -138,6 +141,43 @@ class MultiFilterTests(unittest.TestCase):
|
|
|
self.assertIn('F.[Beleg_Datum] >= :filter_rechnungsdatumvon', sql)
|
|
|
self.assertIn('F.[Beleg_Datum] <= :filter_rechnungsdatumbis', sql)
|
|
|
|
|
|
+ def test_date_filters_bind_native_dates_and_preserve_day_and_month(self):
|
|
|
+ for field, column in (
|
|
|
+ ('Rechnungsdatum', 'Beleg_Datum'), ('Wiedervorlage', 'Wiedervorlage'),
|
|
|
+ ):
|
|
|
+ for selected in (date(2026, 11, 5), date(2026, 11, 18), date(2028, 2, 29)):
|
|
|
+ with self.subTest(field=field, selected=selected):
|
|
|
+ self.db.connection.execute('DELETE FROM dbo.Forderungen_Liste')
|
|
|
+ self.db.add(10, **{column: selected.replace(day=selected.day - 1)})
|
|
|
+ self.db.add(11, **{column: selected})
|
|
|
+ self.db.add(12, **{column: selected.replace(day=selected.day - 2)})
|
|
|
+ for suffix, count in (('Von', 1), ('Bis', 3)):
|
|
|
+ name = field + suffix
|
|
|
+ response = self.client.get('/app/forderungen/liste', params={name: selected.isoformat()})
|
|
|
+ self.assertEqual(response.status_code, 200)
|
|
|
+ self.assertIn(f'id="list-count">{count}', response.text)
|
|
|
+ self.assertRegex(response.text, rf'id="{name}"[^>]*value="{selected.isoformat()}"')
|
|
|
+ bound = self.db.calls[-1][1]['filter_' + name.lower()]
|
|
|
+ self.assertIs(type(bound), date)
|
|
|
+ self.assertEqual(bound, selected)
|
|
|
+ response = self.client.get('/app/forderungen/liste', params={
|
|
|
+ field + 'Von': selected.isoformat(), field + 'Bis': selected.isoformat(),
|
|
|
+ })
|
|
|
+ self.assertIn('id="list-count">1', response.text)
|
|
|
+
|
|
|
+ def test_invalid_dates_are_rejected_before_database_access(self):
|
|
|
+ for field in ('RechnungsdatumVon', 'RechnungsdatumBis', 'WiedervorlageVon', 'WiedervorlageBis'):
|
|
|
+ for value in ('2026-18-11', '18.11.2026', '2026-02-29', '20261118', "2026-11-18' OR 1=1--"):
|
|
|
+ for path in ('/app/forderungen/liste', '/app/forderungen/liste/vorschlaege'):
|
|
|
+ with self.subTest(field=field, value=value, path=path):
|
|
|
+ self.db.calls.clear()
|
|
|
+ params = {field: value}
|
|
|
+ if path.endswith('vorschlaege'):
|
|
|
+ params['field'] = 'Kunde'
|
|
|
+ response = self.client.get(path, params=params)
|
|
|
+ self.assertEqual(response.status_code, 422)
|
|
|
+ self.assertEqual(self.db.calls, [])
|
|
|
+
|
|
|
def test_comma_value_survives_render_and_detail_return(self):
|
|
|
value = encode_filter_values(['Müller, GmbH', 'Meier'])
|
|
|
response = self.client.get(list_url({'Kunde': value}))
|