import csv import gzip import io from datetime import datetime, timedelta from pathlib import Path import pytest from pytz import timezone from app import db from app.insights import parse_range, readings_query, chart_data, summary from app.models import Reading, Profile def add(timestamp, systolic=120, user_id=1, diastolic=75, heart_rate=65): item = Reading(user_id=user_id, timestamp=timestamp, systolic=systolic, diastolic=diastolic, heart_rate=heart_rate) db.session.add(item) db.session.commit() return item @pytest.mark.parametrize('url', ['/', '/dashboard', '/dashboard?view=history', '/dashboard?view=trends', '/dashboard?view=history&mode=calendar', '/reading/add', '/user/profile', '/data/', '/dashboard/report', '/auth/login', '/auth/signup']) def test_pages_render(client, url): response = client.get(url, follow_redirects=True) assert response.status_code == 200 assert 'BP Tracker' in response.text assert 'private, no-store' == response.headers['Cache-Control'] def test_empty_state(client): response = client.get('/dashboard') assert 'No readings yet' in response.text assert '0 / 0' not in response.text assert 'hx-trigger' not in response.text @pytest.mark.parametrize('day,hours', [('2026-10-04', 23), ('2026-04-05', 25)]) def test_range_includes_entire_local_day_excludes_next_midnight(app, day, hours): selected = parse_range({'start_date': day, 'end_date': day}, timezone('Australia/Sydney')) assert (selected.end_utc - selected.start_utc).total_seconds() == hours * 3600 add(selected.start_utc - timedelta(seconds=1)) add(selected.start_utc, 110) add(selected.end_utc - timedelta(microseconds=1), 130) add(selected.end_utc, 160) add(selected.start_utc, 200, user_id=2) stats = summary(readings_query(1, selected)) assert stats['count'] == 2 assert stats['systolic'] == 120 @pytest.mark.parametrize('query', ['start_date=no&end_date=no', 'start_date=2026-09-01', 'start_date=2026-09-08&end_date=2026-09-01', 'days=all', 'days=-1', 'start_date=0001-01-01&end_date=9999-12-31']) def test_invalid_range_is_actionable(client, query): assert client.get('/dashboard?' + query).status_code == 400 assert client.get('/data/export?' + query).status_code == 400 def test_save_another_and_validation_preserve_timestamp(client): values = dict(timestamp='2026-09-08T08:15', systolic='121', diastolic='77', heart_rate='67', action='another') result = client.post('/reading/add', data=values, follow_redirects=True) assert result.status_code == 200 assert result.request.path == '/reading/add' assert 'Reading added successfully.' in result.text assert 'class="notifications"' in result.text assert 'value="121"' not in result.text saved = Reading.query.one() assert saved.timestamp == datetime(2026, 9, 7, 22, 15) values['systolic'] = '999' result = client.post('/reading/add', data=values) assert result.status_code == 200 assert '2026-09-08T08:15' in result.text assert 'value="999"' in result.text assert Reading.query.count() == 1 def test_edit_timestamp_and_delete(client): item = add(datetime(2026, 9, 7, 22)) result = client.post(f'/reading/{item.id}/edit', data=dict(timestamp='2026-09-08T09:20', systolic=125, diastolic=80, heart_rate=70)) assert result.status_code == 302 assert db.session.get(Reading, item.id).timestamp == datetime(2026, 9, 7, 23, 20) assert client.get(f'/reading/{item.id}/confirm_delete').status_code == 200 result = client.post(f'/reading/{item.id}/delete') assert result.status_code == 302 assert Reading.query.count() == 0 def test_user_isolation_and_export(client): add(datetime(2026, 9, 7, 22), 123) other = add(datetime(2026, 9, 7, 22), 234, user_id=2) add(datetime(2026, 9, 8, 14), 190) # Midnight the following day in Sydney. query = 'start_date=2026-09-08&end_date=2026-09-08' response = client.get('/data/export?' + query) assert response.status_code == 200 rows = list(csv.reader(io.StringIO(response.text))) assert len(rows) == 2 assert rows[1] == ['2026-09-07 22:00:00', '123', '75', '65'] assert 'attachment' in response.headers['Content-Disposition'] report = client.get('/dashboard/report?' + query) assert report.status_code == 200 assert '234' not in report.text and '190' not in report.text for suffix in ('edit', 'confirm_delete'): assert client.get(f'/reading/{other.id}/{suffix}').status_code == 404 assert client.post(f'/reading/{other.id}/delete').status_code == 404 def test_saved_preferences(client): profile = Profile.query.filter_by(user_id=1).one() profile.systolic_threshold = 150 profile.diastolic_threshold = 100 profile.dark_mode = True db.session.commit() add(datetime(2026, 9, 7, 22), 140, diastolic=90) response = client.get('/dashboard?view=history&start_date=2026-09-08&end_date=2026-09-08') assert 'data-theme="dark"' in response.text assert 'Below threshold' in response.text assert '150/100' in response.text def test_large_history_and_payload_budgets(client): start = datetime(2020, 1, 1) db.session.bulk_save_objects([Reading(user_id=1, timestamp=start + timedelta(hours=i * 5), systolic=110 + i % 40, diastolic=65 + i % 25, heart_rate=60 + i % 30) for i in range(10_000)]) db.session.commit() selected = parse_range({'start_date': '2020-01-01', 'end_date': '2026-09-08'}, timezone('Australia/Sydney')) query = readings_query(1, selected) chart = chart_data(query, selected) assert len(chart['points']) <= 180 assert sum(p['count'] for p in chart['points']) == 10_000 assert summary(query)['count'] == 10_000 history = client.get('/dashboard?view=history&start_date=2020-01-01&end_date=2026-09-08') assert history.text.count('class="edit-link"') == 25 assert 'Page 1 of 400' in history.text urls = ['/dashboard', '/dashboard?view=trends&start_date=2020-01-01&end_date=2026-09-08', '/dashboard?view=history&mode=calendar&anchor=2020-01-01'] for url in urls: response = client.get(url) assert response.status_code == 200 assert len(gzip.compress(response.data)) <= 20_000 static = Path(__file__).resolve().parents[1] / 'app' / 'static' css_size = len(gzip.compress((static / 'css/tailwind.css').read_bytes())) js_size = len(gzip.compress((static / 'js/diy-turbo.min.js').read_bytes())) assert css_size <= 8_000 assert js_size <= 5_000 assert len(gzip.compress(client.get('/dashboard').data)) + css_size + js_size + 276 <= 40_000 def test_compression_and_static_cache(client): response = client.get('/dashboard', headers={'Accept-Encoding': 'gzip'}) assert response.headers['Content-Encoding'] == 'gzip' assert b'Overview' in gzip.decompress(response.data) import re css = re.search(r'href="([^"]+tailwind.css[^"]+)"', client.get('/dashboard').text).group(1) response = client.get(css.replace('&', '&')) assert 'immutable' in response.headers['Cache-Control'] def test_import_validation_and_atomicity(client): payload = b'Timestamp,Systolic,Diastolic,Heart Rate\n2026-09-07 22:00:00,120,75,65\n2026-09-07 23:00:00,999,75,65\n' response = client.post('/data/', data={'file': (io.BytesIO(payload), 'readings.csv')}, follow_redirects=True) assert 'Check the measurement values on row 3' in response.text assert Reading.query.count() == 0 payload = payload.split(b'2026-09-07 23:00:00')[0] response = client.post('/data/', data={'file': (io.BytesIO(payload), 'readings.csv')}, follow_redirects=True) assert 'Data imported successfully' in response.text assert Reading.query.count() == 1 @pytest.mark.parametrize('local_time', ['2026-10-04T02:30', '2026-04-05T02:30']) def test_ambiguous_or_nonexistent_entry_time(client, local_time): response = client.post('/reading/add', data=dict(timestamp=local_time, systolic=120, diastolic=75, heart_rate=65)) assert response.status_code == 200 assert 'daylight saving' in response.text assert Reading.query.count() == 0 def test_csrf_required_for_saves_deletes_and_imports(app, client): app.config['WTF_CSRF_ENABLED'] = True item = add(datetime(2026, 9, 7, 22)) response = client.post('/reading/add', data=dict(timestamp='2026-09-08T09:00', systolic=120, diastolic=75, heart_rate=65)) assert 'CSRF token is missing' in response.text client.post(f'/reading/{item.id}/delete') client.post('/data/', data={'file': (io.BytesIO(b'Timestamp,Systolic,Diastolic,Heart Rate\n2026-09-07 22:00:00,120,75,65\n'), 'readings.csv')}) assert Reading.query.count() == 1 @pytest.mark.parametrize('url,fragment', [('/dashboard/list', 'view=history'), ('/dashboard/table', 'view=history'), ('/dashboard/graph', 'view=trends'), ('/dashboard/weekly', 'unit=week'), ('/dashboard/monthly', 'unit=month')]) def test_old_bookmarks_still_work(client, url, fragment): response = client.get(url) assert response.status_code == 302 assert fragment in response.location assert client.get(response.location).status_code == 200