feat: add Doris Barbell v1 scaffold
This commit is contained in:
411
home/doris-barbell/tests/test_app.py
Normal file
411
home/doris-barbell/tests/test_app.py
Normal file
@@ -0,0 +1,411 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import create_app
|
||||
|
||||
|
||||
def make_client(tmp_path: Path) -> TestClient:
|
||||
app = create_app(state_dir=tmp_path)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_health_endpoint_reports_ok(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.get('/api/health')
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {'status': 'ok'}
|
||||
|
||||
|
||||
def test_dashboard_summary_seeds_john_and_manndra(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.get('/api/dashboard/summary')
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
user_ids = [user['id'] for user in payload['users']]
|
||||
|
||||
assert user_ids == ['john', 'manndra']
|
||||
assert payload['counts'] == {
|
||||
'exercise_templates': 0,
|
||||
'measurement_entries': 0,
|
||||
'routines': 0,
|
||||
'weight_entries': 0,
|
||||
'workout_sessions': 0,
|
||||
}
|
||||
john = next(user for user in payload['users'] if user['id'] == 'john')
|
||||
assert john['measurement_system'] == 'imperial'
|
||||
|
||||
|
||||
def test_updating_profile_and_logging_weight_computes_bmi(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
profile_response = client.put(
|
||||
'/api/profile/john',
|
||||
json={
|
||||
'display_name': 'John',
|
||||
'height_inches': 68.5,
|
||||
},
|
||||
)
|
||||
assert profile_response.status_code == 200
|
||||
|
||||
weight_response = client.post(
|
||||
'/api/profile/john/weight',
|
||||
json={
|
||||
'recorded_at': '2026-05-21T08:30:00',
|
||||
'weight_lbs': 220,
|
||||
'body_fat_percent': 24.5,
|
||||
},
|
||||
)
|
||||
|
||||
assert weight_response.status_code == 201
|
||||
saved_entry = weight_response.json()
|
||||
assert saved_entry['user_id'] == 'john'
|
||||
assert saved_entry['weight_lbs'] == 220
|
||||
assert saved_entry['body_fat_percent'] == 24.5
|
||||
assert round(saved_entry['bmi'], 1) == 33.0
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
john = next(user for user in summary['users'] if user['id'] == 'john')
|
||||
|
||||
assert john['current_weight_lbs'] == 220
|
||||
assert round(john['current_bmi'], 1) == 33.0
|
||||
|
||||
|
||||
def test_logging_measurements_tracks_latest_imperial_body_metrics(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.post(
|
||||
'/api/profile/john/measurements',
|
||||
json={
|
||||
'recorded_at': '2026-05-21T08:45:00',
|
||||
'waist_inches': 42,
|
||||
'hip_inches': 41,
|
||||
'chest_inches': 46,
|
||||
'neck_inches': 17,
|
||||
'notes': 'Post-op baseline',
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
saved_entry = response.json()
|
||||
assert saved_entry['user_id'] == 'john'
|
||||
assert saved_entry['waist_inches'] == 42
|
||||
assert saved_entry['hip_inches'] == 41
|
||||
assert saved_entry['chest_inches'] == 46
|
||||
assert saved_entry['neck_inches'] == 17
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
john = next(user for user in summary['users'] if user['id'] == 'john')
|
||||
|
||||
assert summary['counts']['measurement_entries'] == 1
|
||||
assert john['latest_measurements'] == {
|
||||
'recorded_at': '2026-05-21T08:45:00',
|
||||
'waist_inches': 42,
|
||||
'hip_inches': 41,
|
||||
'chest_inches': 46,
|
||||
'neck_inches': 17,
|
||||
}
|
||||
|
||||
|
||||
def test_can_create_and_list_exercise_templates(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
create_response = client.post(
|
||||
'/api/exercises/templates',
|
||||
json={
|
||||
'name': 'Bench Press',
|
||||
'primary_muscle': 'chest',
|
||||
'equipment': 'barbell',
|
||||
'notes': 'Flat bench barbell press.',
|
||||
},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
saved_template = create_response.json()
|
||||
assert saved_template['name'] == 'Bench Press'
|
||||
assert saved_template['equipment'] == 'barbell'
|
||||
|
||||
list_response = client.get('/api/exercises/templates')
|
||||
assert list_response.status_code == 200
|
||||
payload = list_response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]['name'] == 'Bench Press'
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
assert summary['counts']['exercise_templates'] == 1
|
||||
|
||||
|
||||
def test_can_create_routine_before_real_program_arrives(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
client.post(
|
||||
'/api/exercises/templates',
|
||||
json={
|
||||
'name': 'Bench Press',
|
||||
'primary_muscle': 'chest',
|
||||
'equipment': 'barbell',
|
||||
},
|
||||
)
|
||||
client.post(
|
||||
'/api/exercises/templates',
|
||||
json={
|
||||
'name': 'Incline Dumbbell Press',
|
||||
'primary_muscle': 'chest',
|
||||
'equipment': 'dumbbells',
|
||||
},
|
||||
)
|
||||
|
||||
create_response = client.post(
|
||||
'/api/routines',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'name': 'Push Day Placeholder',
|
||||
'notes': 'Temporary until John sends the real split.',
|
||||
'exercises': [
|
||||
{'exercise_name': 'Bench Press', 'target_sets': 3, 'target_reps': '8-10'},
|
||||
{'exercise_name': 'Incline Dumbbell Press', 'target_sets': 3, 'target_reps': '10-12'},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
saved_routine = create_response.json()
|
||||
assert saved_routine['user_id'] == 'john'
|
||||
assert saved_routine['name'] == 'Push Day Placeholder'
|
||||
assert len(saved_routine['exercises']) == 2
|
||||
|
||||
list_response = client.get('/api/routines')
|
||||
assert list_response.status_code == 200
|
||||
payload = list_response.json()
|
||||
assert len(payload) == 1
|
||||
assert payload[0]['name'] == 'Push Day Placeholder'
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
assert summary['counts']['routines'] == 1
|
||||
|
||||
|
||||
def test_history_endpoints_return_newest_first(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
client.post(
|
||||
'/api/profile/john/weight',
|
||||
json={'recorded_at': '2026-05-20T08:30:00', 'weight_lbs': 222},
|
||||
)
|
||||
client.post(
|
||||
'/api/profile/john/weight',
|
||||
json={'recorded_at': '2026-05-21T08:30:00', 'weight_lbs': 220},
|
||||
)
|
||||
client.post(
|
||||
'/api/profile/john/measurements',
|
||||
json={'recorded_at': '2026-05-20T08:45:00', 'waist_inches': 43},
|
||||
)
|
||||
client.post(
|
||||
'/api/profile/john/measurements',
|
||||
json={'recorded_at': '2026-05-21T08:45:00', 'waist_inches': 42},
|
||||
)
|
||||
client.post(
|
||||
'/api/workouts',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'performed_at': '2026-05-20T09:15:00',
|
||||
'routine_name': 'Old Workout',
|
||||
'exercises': [{'exercise_name': 'Bench Press', 'sets': [{'reps': 8, 'weight_lbs': 135}]}],
|
||||
},
|
||||
)
|
||||
client.post(
|
||||
'/api/workouts',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'performed_at': '2026-05-21T09:15:00',
|
||||
'routine_name': 'New Workout',
|
||||
'exercises': [{'exercise_name': 'Bench Press', 'sets': [{'reps': 8, 'weight_lbs': 145}]}],
|
||||
},
|
||||
)
|
||||
|
||||
weight_history = client.get('/api/history/john/weights')
|
||||
measurement_history = client.get('/api/history/john/measurements')
|
||||
workout_history = client.get('/api/history/john/workouts')
|
||||
|
||||
assert weight_history.status_code == 200
|
||||
assert measurement_history.status_code == 200
|
||||
assert workout_history.status_code == 200
|
||||
|
||||
assert [entry['weight_lbs'] for entry in weight_history.json()] == [220, 222]
|
||||
assert [entry['waist_inches'] for entry in measurement_history.json()] == [42, 43]
|
||||
assert [entry['routine_name'] for entry in workout_history.json()] == ['New Workout', 'Old Workout']
|
||||
|
||||
|
||||
def test_logging_structured_workout_updates_recent_activity(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.post(
|
||||
'/api/workouts',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'performed_at': '2026-05-21T09:15:00',
|
||||
'routine_name': 'Push Day',
|
||||
'notes': 'First pass',
|
||||
'exercises': [
|
||||
{
|
||||
'exercise_name': 'Bench Press',
|
||||
'sets': [
|
||||
{'reps': 8, 'weight_lbs': 135, 'rpe': 7.5, 'rest_seconds': 120},
|
||||
{'reps': 8, 'weight_lbs': 145, 'rpe': 8.0, 'rest_seconds': 120},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
workout = response.json()
|
||||
assert workout['user_id'] == 'john'
|
||||
assert workout['routine_name'] == 'Push Day'
|
||||
assert workout['exercise_count'] == 1
|
||||
assert workout['set_count'] == 2
|
||||
assert workout['total_volume_lbs'] == 2240
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
assert summary['counts']['workout_sessions'] == 1
|
||||
assert summary['recent_workouts'][0]['routine_name'] == 'Push Day'
|
||||
assert summary['recent_workouts'][0]['total_volume_lbs'] == 2240
|
||||
|
||||
|
||||
def test_progression_endpoint_summarizes_exercise_bests(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
client.post(
|
||||
'/api/workouts',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'performed_at': '2026-05-20T09:15:00',
|
||||
'routine_name': 'Push Day',
|
||||
'exercises': [
|
||||
{
|
||||
'exercise_name': 'Bench Press',
|
||||
'sets': [
|
||||
{'reps': 8, 'weight_lbs': 135},
|
||||
{'reps': 5, 'weight_lbs': 155},
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
client.post(
|
||||
'/api/workouts',
|
||||
json={
|
||||
'user_id': 'john',
|
||||
'performed_at': '2026-05-22T09:15:00',
|
||||
'routine_name': 'Push Day',
|
||||
'exercises': [
|
||||
{
|
||||
'exercise_name': 'Bench Press',
|
||||
'sets': [
|
||||
{'reps': 3, 'weight_lbs': 175},
|
||||
],
|
||||
},
|
||||
{
|
||||
'exercise_name': 'Incline Dumbbell Press',
|
||||
'sets': [
|
||||
{'reps': 10, 'weight_lbs': 55},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
response = client.get('/api/progression/john/exercises')
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload[0]['exercise_name'] == 'Bench Press'
|
||||
assert payload[0]['sessions'] == 2
|
||||
assert payload[0]['max_weight_lbs'] == 175
|
||||
assert payload[0]['best_set_estimated_1rm'] == 192.5
|
||||
assert payload[0]['last_performed_at'] == '2026-05-22T09:15:00'
|
||||
assert payload[1]['exercise_name'] == 'Incline Dumbbell Press'
|
||||
assert payload[1]['sessions'] == 1
|
||||
|
||||
|
||||
def test_homepage_renders_core_sections_and_forms(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.get('/')
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
assert 'Doris Barbell' in body
|
||||
assert 'Recent workouts' in body
|
||||
assert 'Current body metrics' in body
|
||||
assert 'Log weight' in body
|
||||
assert 'Add exercise template' in body
|
||||
assert 'Build routine draft' in body
|
||||
|
||||
|
||||
def test_weight_form_submission_redirects_and_updates_dashboard(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
response = client.post(
|
||||
'/log/weight',
|
||||
data={
|
||||
'user_id': 'john',
|
||||
'recorded_at': '2026-05-22T06:15:00',
|
||||
'weight_lbs': '218.5',
|
||||
'body_fat_percent': '23.7',
|
||||
'notes': 'Pre-gym weigh-in',
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303
|
||||
assert response.headers['location'] == '/'
|
||||
|
||||
summary = client.get('/api/dashboard/summary').json()
|
||||
john = next(user for user in summary['users'] if user['id'] == 'john')
|
||||
assert john['current_weight_lbs'] == 218.5
|
||||
|
||||
history = client.get('/api/history/john/weights').json()
|
||||
assert history[0]['notes'] == 'Pre-gym weigh-in'
|
||||
|
||||
|
||||
def test_template_and_routine_form_submissions_redirect_and_render_entries(tmp_path: Path) -> None:
|
||||
client = make_client(tmp_path)
|
||||
|
||||
template_response = client.post(
|
||||
'/templates',
|
||||
data={
|
||||
'name': 'Leg Press',
|
||||
'primary_muscle': 'quads',
|
||||
'equipment': 'machine',
|
||||
'notes': 'Plate-loaded sled.',
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert template_response.status_code == 303
|
||||
assert template_response.headers['location'] == '/'
|
||||
|
||||
routine_response = client.post(
|
||||
'/routines',
|
||||
data={
|
||||
'user_id': 'john',
|
||||
'name': 'Leg Day Draft',
|
||||
'notes': 'Temporary until John sends the real split.',
|
||||
'exercise_lines': 'Leg Press|4|10-12\nHamstring Curl|3|10-12|Slow negative',
|
||||
},
|
||||
follow_redirects=False,
|
||||
)
|
||||
assert routine_response.status_code == 303
|
||||
assert routine_response.headers['location'] == '/'
|
||||
|
||||
page = client.get('/')
|
||||
assert page.status_code == 200
|
||||
assert 'Leg Press' in page.text
|
||||
assert 'Leg Day Draft' in page.text
|
||||
|
||||
routines = client.get('/api/routines').json()
|
||||
assert routines[0]['exercises'][0]['exercise_name'] == 'Leg Press'
|
||||
assert routines[0]['exercises'][1]['notes'] == 'Slow negative'
|
||||
97
home/doris-barbell/tests/test_hotfuzz_theme.py
Normal file
97
home/doris-barbell/tests/test_hotfuzz_theme.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path('/home/fizzlepoof/repos/truenas-stacks/home')
|
||||
|
||||
|
||||
def read(relative_path: str) -> str:
|
||||
return (ROOT / relative_path).read_text()
|
||||
|
||||
|
||||
def test_hot_fuzz_theme_markers_exist_across_doris_sites() -> None:
|
||||
expectations = {
|
||||
'doris-kitchen/app/templates/base.html': [
|
||||
'doris-hot-fuzz',
|
||||
'Doris Constabulary',
|
||||
'N.W.A. Case File',
|
||||
'hot-fuzz-art',
|
||||
'constable-silhouette',
|
||||
'swan-stamp',
|
||||
'cinematic-glow',
|
||||
'app-casefile app-casefile-kitchen',
|
||||
'Pantry Evidence',
|
||||
'recipe-evidence-tag',
|
||||
],
|
||||
'doris-schoolhouse/app/templates/base.html': [
|
||||
'doris-hot-fuzz',
|
||||
'Doris Constabulary',
|
||||
'N.W.A. Case File',
|
||||
'hot-fuzz-art',
|
||||
'constable-silhouette',
|
||||
'swan-stamp',
|
||||
'cinematic-glow',
|
||||
'app-casefile app-casefile-schoolhouse',
|
||||
'Records Intake',
|
||||
'coursework-evidence-tag',
|
||||
],
|
||||
'doris-barbell/app/templates/base.html': [
|
||||
'doris-hot-fuzz',
|
||||
'Doris Constabulary',
|
||||
'N.W.A. Case File',
|
||||
'hot-fuzz-art',
|
||||
'constable-silhouette',
|
||||
'swan-stamp',
|
||||
'cinematic-glow',
|
||||
'app-casefile app-casefile-barbell',
|
||||
'Training Dossier',
|
||||
'barbell-evidence-tag',
|
||||
],
|
||||
'doris-dashboard/bin/generate_dashboard.py': [
|
||||
'Doris Constabulary',
|
||||
'N.W.A. Case File',
|
||||
'for-the-greater-good',
|
||||
'hot-fuzz-art',
|
||||
'constable-silhouette',
|
||||
'swan-stamp',
|
||||
'cinematic-glow',
|
||||
'Operator Evidence Board',
|
||||
'operator-evidence-tag',
|
||||
'hero-board',
|
||||
'lead-warrant',
|
||||
'hero-slip-grid',
|
||||
],
|
||||
}
|
||||
|
||||
for relative_path, markers in expectations.items():
|
||||
content = read(relative_path)
|
||||
for marker in markers:
|
||||
assert marker in content, f'{marker} missing from {relative_path}'
|
||||
|
||||
|
||||
def test_hot_fuzz_theme_tokens_exist_in_site_stylesheets() -> None:
|
||||
expectations = {
|
||||
'doris-kitchen/app/static/app.css': ['--hf-red', '--hf-amber', '.incident-tape', '.casefile-header', '.hot-fuzz-art', '.constable-silhouette', '.swan-stamp', '.cinematic-glow', '.app-casefile', '.film-grain', '@keyframes emergencyPulse', '.evidence-board', '.case-legend', '.dossier-grid'],
|
||||
'doris-schoolhouse/app/static/app.css': ['--hf-red', '--hf-amber', '.incident-tape', '.casefile-header', '.hot-fuzz-art', '.constable-silhouette', '.swan-stamp', '.cinematic-glow', '.app-casefile', '.film-grain', '@keyframes emergencyPulse', '.evidence-board', '.case-legend', '.dossier-grid'],
|
||||
'doris-barbell/app/static/app.css': ['--hf-red', '--hf-amber', '.incident-tape', '.casefile-header', '.hot-fuzz-art', '.constable-silhouette', '.swan-stamp', '.cinematic-glow', '.app-casefile', '.film-grain', '@keyframes emergencyPulse', '.evidence-board', '.case-legend', '.dossier-grid'],
|
||||
'doris-dashboard/public/style.css': ['--hf-red', '--hf-amber', '.incident-tape', '.casefile-header', '.hot-fuzz-art', '.constable-silhouette', '.swan-stamp', '.cinematic-glow', '.app-casefile', '.film-grain', '@keyframes emergencyPulse', '.evidence-board', '.case-legend', '.dossier-grid', '.hero-board', '.hero-dossier-card', '.hero-slip-grid'],
|
||||
}
|
||||
|
||||
for relative_path, markers in expectations.items():
|
||||
content = read(relative_path)
|
||||
for marker in markers:
|
||||
assert marker in content, f'{marker} missing from {relative_path}'
|
||||
|
||||
|
||||
def test_interior_casefile_markers_exist_on_key_pages() -> None:
|
||||
expectations = {
|
||||
'doris-kitchen/app/templates/dashboard.html': ['evidence-board', 'case-legend', 'Kitchen queue board', 'dossier-grid'],
|
||||
'doris-kitchen/app/templates/search.html': ['evidence-board', 'case-legend', 'Lead intake', 'Search warrant'],
|
||||
'doris-schoolhouse/app/templates/dashboard.html': ['evidence-board', 'case-legend', 'Course board', 'dossier-grid'],
|
||||
'doris-schoolhouse/app/templates/assignment_upload.html': ['evidence-board', 'chain-of-custody', 'Submission packet', 'intake-docket'],
|
||||
'doris-barbell/app/templates/dashboard.html': ['evidence-board', 'case-legend', 'Training floor board', 'dossier-grid'],
|
||||
'doris-dashboard/bin/generate_dashboard.py': ['evidence-board', 'case-legend', 'Operator board', 'dossier-grid'],
|
||||
}
|
||||
|
||||
for relative_path, markers in expectations.items():
|
||||
content = read(relative_path)
|
||||
for marker in markers:
|
||||
assert marker in content, f'{marker} missing from {relative_path}'
|
||||
Reference in New Issue
Block a user