Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
180 lines
6.5 KiB
JavaScript
180 lines
6.5 KiB
JavaScript
/**
|
|
* ADR-025: Fitness CSV Import 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, dbGet } 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 = 99200;
|
|
async function createTestUserAndSpace() {
|
|
// Create user with unique ID
|
|
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-csv-${Date.now()}@test.com`]);
|
|
|
|
// Create space
|
|
const result = await dbRun(`
|
|
INSERT INTO spaces (owner_id, name, type)
|
|
VALUES ($1, 'Test Space CSV', 'personal')
|
|
`, [TEST_UID]);
|
|
|
|
return result.lastInsertRowid;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await setupTestDatabase();
|
|
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use(express.text({ type: 'text/csv' }));
|
|
|
|
// Create test space
|
|
spaceId = await createTestUserAndSpace();
|
|
|
|
// Setup fitness routes
|
|
const fitnessRoutes = (await import('../routes/v3/fitness.js')).default;
|
|
app.use('/api/v3/fitness', fitnessRoutes);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await cleanupTestDatabase();
|
|
});
|
|
|
|
describe('Fitness CSV Import', () => {
|
|
|
|
describe('POST /api/v3/fitness/import/csv', () => {
|
|
|
|
test('should import Hevy format CSV', async () => {
|
|
const hevyCSV = `title,start_time,end_time,description,exercise_title,superset_id,reps,weight_kg,distance_km,duration_seconds,notes,rpe,set_type
|
|
Push Day,2026-01-15 08:00:00,2026-01-15 09:00:00,Chest and triceps,Bench Press (Barbell),,10,60,,,,7,warmup
|
|
Push Day,2026-01-15 08:00:00,2026-01-15 09:00:00,Chest and triceps,Bench Press (Barbell),,8,80,,,,8,normal
|
|
Push Day,2026-01-15 08:00:00,2026-01-15 09:00:00,Chest and triceps,Bench Press (Barbell),,6,90,,,,9,normal
|
|
Push Day,2026-01-15 08:00:00,2026-01-15 09:00:00,Chest and triceps,Incline Dumbbell Press,,10,24,,,,7,normal
|
|
Push Day,2026-01-15 08:00:00,2026-01-15 09:00:00,Chest and triceps,Incline Dumbbell Press,,10,24,,,,7,normal`;
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId, format: 'hevy' })
|
|
.set('Content-Type', 'text/csv')
|
|
.send(hevyCSV);
|
|
|
|
if (response.status !== 200) {
|
|
console.log('Error response:', response.body);
|
|
}
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.workouts_created).toBe(1);
|
|
expect(response.body.data.sets_created).toBe(5);
|
|
});
|
|
|
|
test('should import Strong format CSV', async () => {
|
|
const strongCSV = `Date,Workout Name,Exercise Name,Set Order,Weight,Weight Unit,Reps,RPE,Distance,Distance Unit,Seconds,Notes,Workout Notes,Workout Duration
|
|
2026-01-16,Pull Day,Deadlift (Barbell),1,100,kg,5,8,,,,,,3600
|
|
2026-01-16,Pull Day,Deadlift (Barbell),2,120,kg,5,8,,,,,,3600
|
|
2026-01-16,Pull Day,Deadlift (Barbell),3,140,kg,3,9,,,,,,3600
|
|
2026-01-16,Pull Day,Lat Pulldown (Cable),1,60,kg,10,7,,,,,,3600`;
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId, format: 'strong' })
|
|
.set('Content-Type', 'text/csv')
|
|
.send(strongCSV);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.workouts_created).toBe(1);
|
|
expect(response.body.data.sets_created).toBe(4);
|
|
});
|
|
|
|
test('should auto-detect format when not specified', async () => {
|
|
const hevyCSV = `title,start_time,exercise_title,reps,weight_kg,set_type
|
|
Leg Day,2026-01-17 10:00:00,Squat (Barbell),5,100,normal
|
|
Leg Day,2026-01-17 10:00:00,Squat (Barbell),5,100,normal`;
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId })
|
|
.set('Content-Type', 'text/csv')
|
|
.send(hevyCSV);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.success).toBe(true);
|
|
expect(response.body.data.format_detected).toBeDefined();
|
|
});
|
|
|
|
test('should require space_id', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.set('Content-Type', 'text/csv')
|
|
.send('title,start_time\nTest,2026-01-17');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.code).toBe('VALIDATION_ERROR');
|
|
});
|
|
|
|
test('should handle empty CSV', async () => {
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId })
|
|
.set('Content-Type', 'text/csv')
|
|
.send('');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error.code).toBe('EMPTY_CSV');
|
|
});
|
|
|
|
test('should skip rows with missing required fields', async () => {
|
|
const csvWithMissing = `title,start_time,exercise_title,reps,weight_kg
|
|
Leg Day,2026-01-17 10:00:00,Squat,5,100
|
|
Leg Day,2026-01-17 10:00:00,,5,100
|
|
Leg Day,2026-01-17 10:00:00,Lunge,8,40`;
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId })
|
|
.set('Content-Type', 'text/csv')
|
|
.send(csvWithMissing);
|
|
|
|
expect(response.status).toBe(200);
|
|
// Should import 2 rows, skip 1 with missing exercise
|
|
expect(response.body.data.sets_created).toBe(2);
|
|
expect(response.body.data.rows_skipped).toBe(1);
|
|
});
|
|
|
|
test('should convert lbs to kg when weight_unit is lbs', async () => {
|
|
const strongCSV = `Date,Workout Name,Exercise Name,Set Order,Weight,Weight Unit,Reps
|
|
2026-01-17,Test,Bench Press,1,135,lbs,10`;
|
|
|
|
const response = await request(app)
|
|
.post('/api/v3/fitness/import/csv')
|
|
.query({ space_id: spaceId })
|
|
.set('Content-Type', 'text/csv')
|
|
.send(strongCSV);
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
// 135 lbs ≈ 61.24 kg
|
|
const { dbGet } = await import('../database/connection.js');
|
|
const set = await dbGet('SELECT weight_kg FROM fitness_sets ORDER BY id DESC LIMIT 1');
|
|
expect(set.weight_kg).toBeCloseTo(61.24, 1);
|
|
});
|
|
});
|
|
});
|