Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
207 lines
7.2 KiB
JavaScript
207 lines
7.2 KiB
JavaScript
// @vitest-environment node
|
|
/**
|
|
* ADR-025: Fitness Analytics API Tests
|
|
* TDD - Test First Approach
|
|
*/
|
|
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
import request from 'supertest';
|
|
import express from 'express';
|
|
import { setupTestDatabase, cleanupTestDatabase } from './helpers/test-db.js';
|
|
import { dbRun } from '../database/connection.js';
|
|
|
|
// Set test mode
|
|
process.env.TEST_MODE = 'true';
|
|
process.env.JWT_SECRET = 'test-secret-key-for-fitness';
|
|
process.env.MASTER_ENCRYPTION_KEY = 'test-master-key-32-characters!!';
|
|
process.env.SKIP_DEV_USER = 'true';
|
|
|
|
let app;
|
|
let spaceId;
|
|
|
|
/**
|
|
* Helper to create test user and space
|
|
*/
|
|
const TEST_UID = 99100;
|
|
async function createTestUserAndSpace() {
|
|
await dbRun(`
|
|
INSERT INTO users (id, email, password_hash, name, encryption_key_encrypted)
|
|
VALUES ($1, $2, 'hash', 'Test User', 'encrypted_key')
|
|
ON CONFLICT (id) DO NOTHING
|
|
`, [TEST_UID, `fitness-analytics-${Date.now()}@test.com`]);
|
|
|
|
const result = await dbRun(`
|
|
INSERT INTO spaces (owner_id, name, type)
|
|
VALUES ($1, 'Test Space Analytics', 'personal')
|
|
`, [TEST_UID]);
|
|
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
/**
|
|
* Helper to seed workout data for analytics tests
|
|
*/
|
|
async function seedAnalyticsData(spaceId) {
|
|
// Create workouts over the past week (relative to today so streak/week filters work)
|
|
const today = new Date();
|
|
const makeDate = (daysAgo) => {
|
|
const d = new Date(today);
|
|
d.setDate(d.getDate() - daysAgo);
|
|
return d.toISOString().split('T')[0] + ' 08:00:00';
|
|
};
|
|
const dates = [
|
|
makeDate(7), // Day 1
|
|
makeDate(5), // Day 3
|
|
makeDate(3), // Day 5
|
|
makeDate(2), // Day 6
|
|
makeDate(0), // Day 8 (today)
|
|
];
|
|
|
|
// Clean up any previous fitness data to avoid PK conflicts with SERIAL
|
|
await dbRun(`DELETE FROM fitness_sets`);
|
|
await dbRun(`DELETE FROM fitness_workouts`);
|
|
|
|
for (let i = 0; i < dates.length; i++) {
|
|
const workoutResult = await dbRun(`
|
|
INSERT INTO fitness_workouts (space_id, title, started_at, ended_at)
|
|
VALUES (?, ?, ?, ?)
|
|
`, [spaceId, `Workout ${i + 1}`, dates[i], dates[i].replace('08:00:00', '09:00:00')]);
|
|
const workoutId = workoutResult.lastInsertRowid;
|
|
|
|
// Add sets with increasing weights (for PR detection)
|
|
await dbRun(`
|
|
INSERT INTO fitness_sets (workout_id, exercise_name, set_index, weight_kg, reps, is_pr)
|
|
VALUES (?, 'Bench Press', 1, ?, 5, ?)
|
|
`, [workoutId, 60 + i * 10, i === dates.length - 1 ? true : false]); // Last one is PR
|
|
|
|
await dbRun(`
|
|
INSERT INTO fitness_sets (workout_id, exercise_name, set_index, weight_kg, reps)
|
|
VALUES (?, 'Squat', 1, ?, 5)
|
|
`, [workoutId, 80 + i * 10]);
|
|
}
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await setupTestDatabase();
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
spaceId = await createTestUserAndSpace();
|
|
await seedAnalyticsData(spaceId);
|
|
|
|
const fitnessRoutes = (await import('../routes/v3/fitness.js')).default;
|
|
app.use('/api/v3/fitness', fitnessRoutes);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestDatabase();
|
|
});
|
|
|
|
describe('Fitness Analytics API', () => {
|
|
|
|
describe('GET /api/v3/fitness/analytics/summary', () => {
|
|
test('should return workout summary stats', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/summary')
|
|
.query({ space_id: spaceId });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.total_workouts).toBe(5);
|
|
expect(response.body.data.total_sets).toBe(10);
|
|
expect(response.body.data.total_volume).toBeGreaterThan(0);
|
|
expect(response.body.data.total_prs).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/analytics/volume', () => {
|
|
test('should return volume by day', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/volume')
|
|
.query({ space_id: spaceId, period: 'week' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThan(0);
|
|
expect(response.body.data[0]).toHaveProperty('date');
|
|
expect(response.body.data[0]).toHaveProperty('volume');
|
|
});
|
|
|
|
test('should return volume by exercise', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/volume')
|
|
.query({ space_id: spaceId, group_by: 'exercise' });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.some(d => d.exercise === 'Bench Press')).toBe(true);
|
|
expect(response.body.data.some(d => d.exercise === 'Squat')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/analytics/prs', () => {
|
|
test('should return personal records', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/prs')
|
|
.query({ space_id: spaceId });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body.data.length).toBeGreaterThanOrEqual(1);
|
|
expect(response.body.data[0]).toHaveProperty('exercise_name');
|
|
expect(response.body.data[0]).toHaveProperty('weight_kg');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/analytics/streak', () => {
|
|
test('should return current workout streak', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/streak')
|
|
.query({ space_id: spaceId });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.current_streak).toBeGreaterThanOrEqual(0);
|
|
expect(response.body.data.longest_streak).toBeGreaterThanOrEqual(0);
|
|
expect(response.body.data.workouts_this_week).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/analytics/muscle-volume', () => {
|
|
test('should return volume by muscle group', async () => {
|
|
// First add exercise with muscle mapping
|
|
await dbRun(`
|
|
INSERT INTO fitness_exercises (name, primary_muscle, equipment)
|
|
VALUES ('Bench Press', 'chest', 'barbell')
|
|
`);
|
|
await dbRun(`
|
|
INSERT INTO fitness_exercises (name, primary_muscle, equipment)
|
|
VALUES ('Squat', 'quadriceps', 'barbell')
|
|
`);
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/muscle-volume')
|
|
.query({ space_id: spaceId });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/analytics/exercise/:exerciseName', () => {
|
|
test('should return exercise-specific analytics', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/analytics/exercise/Bench Press')
|
|
.query({ space_id: spaceId });
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.exercise_name).toBe('Bench Press');
|
|
expect(response.body.data.total_sets).toBeGreaterThan(0);
|
|
expect(response.body.data.max_weight).toBeGreaterThan(0);
|
|
expect(response.body.data.estimated_1rm).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
});
|