Files
bloodpressure/tests/test_redesign.py
T

198 lines
9.0 KiB
Python

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</td>' 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('&amp;', '&'))
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