Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
447 lines
14 KiB
JavaScript
447 lines
14 KiB
JavaScript
/**
|
|
* ADR-025: Fitness Module 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';
|
|
|
|
// 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 authToken;
|
|
let spaceId;
|
|
|
|
/**
|
|
* Helper to setup test user and get auth token
|
|
*/
|
|
async function setupAuthenticatedUser() {
|
|
const authRoutes = (await import('../routes/v3/auth.js')).default;
|
|
app.use('/api/v3/auth', authRoutes);
|
|
|
|
// Register user with unique email to avoid conflicts
|
|
const registerRes = await request(app)
|
|
.post('/api/v3/auth/register')
|
|
.send({ email: `fitness-${Date.now()}@test.com`, password: 'TestPass123!', name: 'Fitness User' });
|
|
|
|
authToken = registerRes.body.data?.accessToken;
|
|
|
|
// Get user's personal space
|
|
const spacesRoutes = (await import('../routes/v3/spaces.js')).default;
|
|
app.use('/api/v3/spaces', spacesRoutes);
|
|
|
|
const spacesRes = await request(app)
|
|
.get('/api/v3/spaces')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
// Find personal space
|
|
const personalSpace = spacesRes.body.data?.find(s => s.type === 'personal');
|
|
spaceId = personalSpace?.id || 1;
|
|
|
|
return { authToken, spaceId };
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await setupTestDatabase();
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
// Setup fitness routes
|
|
const fitnessRoutes = (await import('../routes/v3/fitness.js')).default;
|
|
app.use('/api/v3/fitness', fitnessRoutes);
|
|
|
|
await setupAuthenticatedUser();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestDatabase();
|
|
});
|
|
|
|
describe('Fitness API - Workouts CRUD', () => {
|
|
|
|
describe('POST /api/v3/fitness/workouts', () => {
|
|
test('should create a new workout', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'Morning Push Day',
|
|
started_at: '2026-01-17T08:00:00Z',
|
|
notes: 'Feeling strong today'
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.id).toBeDefined();
|
|
expect(response.body.data.title).toBe('Morning Push Day');
|
|
expect(response.body.data.space_id).toBe(spaceId);
|
|
});
|
|
|
|
test('should require space_id', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
title: 'No Space Workout',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.success).toBe(false);
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
test('should require started_at', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'No Date Workout'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/workouts', () => {
|
|
test('should list workouts for space', async () => {
|
|
// Create workout first
|
|
await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'Test Workout',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/workouts')
|
|
.query({ space_id: spaceId })
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
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);
|
|
});
|
|
|
|
test('should return empty array for space with no workouts', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/workouts')
|
|
.query({ space_id: 9999 })
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v3/fitness/workouts/:id', () => {
|
|
test('should get single workout with sets', async () => {
|
|
// Create workout
|
|
const createRes = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'Detailed Workout',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
const workoutId = createRes.body.data.id;
|
|
|
|
// Add a set
|
|
await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Bench Press',
|
|
set_index: 1,
|
|
weight_kg: 80,
|
|
reps: 10
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get(`/api/v3/fitness/workouts/${workoutId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.id).toBe(workoutId);
|
|
expect(response.body.data.title).toBe('Detailed Workout');
|
|
expect(Array.isArray(response.body.data.sets)).toBe(true);
|
|
expect(response.body.data.sets.length).toBe(1);
|
|
expect(response.body.data.sets[0].exercise_name).toBe('Bench Press');
|
|
});
|
|
|
|
test('should return 404 for non-existent workout', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/workouts/99999')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(404);
|
|
expect(response.body.success).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/v3/fitness/workouts/:id', () => {
|
|
test('should update workout', async () => {
|
|
// Create workout
|
|
const createRes = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'Original Title',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
const workoutId = createRes.body.data.id;
|
|
|
|
const response = await request(app)
|
|
.put(`/api/v3/fitness/workouts/${workoutId}`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
title: 'Updated Title',
|
|
ended_at: '2026-01-17T09:30:00Z',
|
|
notes: 'Great session!'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.title).toBe('Updated Title');
|
|
expect(response.body.data.notes).toBe('Great session!');
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/v3/fitness/workouts/:id', () => {
|
|
test('should delete workout and its sets', async () => {
|
|
// Create workout with set
|
|
const createRes = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'To Delete',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
const workoutId = createRes.body.data.id;
|
|
|
|
// Add set
|
|
await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Squat',
|
|
set_index: 1,
|
|
weight_kg: 100,
|
|
reps: 5
|
|
});
|
|
|
|
// Delete workout
|
|
const deleteRes = await request(app)
|
|
.delete(`/api/v3/fitness/workouts/${workoutId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(deleteRes.status).toBe(200);
|
|
expect(deleteRes.body.success).toBe(true);
|
|
|
|
// Verify workout is gone
|
|
const getRes = await request(app)
|
|
.get(`/api/v3/fitness/workouts/${workoutId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(getRes.status).toBe(404);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Fitness API - Sets CRUD', () => {
|
|
let workoutId;
|
|
|
|
beforeEach(async () => {
|
|
// Create a workout for set tests
|
|
const createRes = await request(app)
|
|
.post('/api/v3/fitness/workouts')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
title: 'Set Test Workout',
|
|
started_at: '2026-01-17T08:00:00Z'
|
|
});
|
|
|
|
workoutId = createRes.body.data?.id;
|
|
});
|
|
|
|
describe('POST /api/v3/fitness/workouts/:workoutId/sets', () => {
|
|
test('should create a new set', async () => {
|
|
const response = await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Deadlift',
|
|
set_index: 1,
|
|
set_type: 'normal',
|
|
weight_kg: 140,
|
|
reps: 5,
|
|
rpe: 8
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.id).toBeDefined();
|
|
expect(response.body.data.exercise_name).toBe('Deadlift');
|
|
expect(Number(response.body.data.weight_kg)).toBe(140);
|
|
expect(response.body.data.reps).toBe(5);
|
|
});
|
|
|
|
test('should allow creating warmup set', async () => {
|
|
const response = await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Bench Press',
|
|
set_index: 1,
|
|
set_type: 'warmup',
|
|
weight_kg: 40,
|
|
reps: 15
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.data.set_type).toBe('warmup');
|
|
});
|
|
|
|
test('should require exercise_name', async () => {
|
|
const response = await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
set_index: 1,
|
|
weight_kg: 80,
|
|
reps: 10
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
});
|
|
|
|
describe('PUT /api/v3/fitness/sets/:id', () => {
|
|
test('should update a set', async () => {
|
|
// Create set
|
|
const createRes = await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Squat',
|
|
set_index: 1,
|
|
weight_kg: 100,
|
|
reps: 8
|
|
});
|
|
|
|
const setId = createRes.body.data.id;
|
|
|
|
// Update set
|
|
const response = await request(app)
|
|
.put(`/api/v3/fitness/sets/${setId}`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
weight_kg: 105,
|
|
reps: 10,
|
|
is_pr: true
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(Number(response.body.data.weight_kg)).toBe(105);
|
|
expect(response.body.data.reps).toBe(10);
|
|
expect(response.body.data.is_pr).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('DELETE /api/v3/fitness/sets/:id', () => {
|
|
test('should delete a set', async () => {
|
|
// Create set
|
|
const createRes = await request(app)
|
|
.post(`/api/v3/fitness/workouts/${workoutId}/sets`)
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
exercise_name: 'Pull-up',
|
|
set_index: 1,
|
|
reps: 12
|
|
});
|
|
|
|
const setId = createRes.body.data.id;
|
|
|
|
// Delete set
|
|
const response = await request(app)
|
|
.delete(`/api/v3/fitness/sets/${setId}`)
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Fitness API - Exercises Catalog', () => {
|
|
describe('GET /api/v3/fitness/exercises', () => {
|
|
test('should list exercises', async () => {
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/exercises')
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
});
|
|
|
|
test('should search exercises by name', async () => {
|
|
// First create an exercise with unique name
|
|
const uniqueName = `Barbell Bench Press ${Date.now()}`;
|
|
await request(app)
|
|
.post('/api/v3/fitness/exercises')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
name: uniqueName,
|
|
primary_muscle: 'chest',
|
|
equipment: 'barbell'
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/api/v3/fitness/exercises')
|
|
.query({ search: 'bench' })
|
|
.set('Authorization', `Bearer ${authToken}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.data.some(e => e.name.toLowerCase().includes('bench'))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/v3/fitness/exercises', () => {
|
|
test('should create custom exercise', async () => {
|
|
const uniqueName = `My Custom Exercise ${Date.now()}`;
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/exercises')
|
|
.set('Authorization', `Bearer ${authToken}`)
|
|
.send({
|
|
space_id: spaceId,
|
|
name: uniqueName,
|
|
primary_muscle: 'back',
|
|
equipment: 'cable'
|
|
});
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(response.body.data.name).toBe(uniqueName);
|
|
expect(response.body.data.space_id).toBe(spaceId);
|
|
});
|
|
});
|
|
});
|