// SPDX-License-Identifier: MIT OR Apache-2.0
// Copyright (c) 2025 Pierre Fitness Intelligence
/**
* Integration Tests: Strava Provider with Mock Data
*
* These tests verify that the SDK correctly handles realistic Strava API
* responses throughout the entire flow: SDK → Pierre Server → Response
*
* Uses mock Strava data based on actual API v3 responses
*/
const { spawn } = require('child_process');
const { EventEmitter } = require('events');
// Import mock Strava data fixtures
const {
mockStravaActivities,
mockStravaAthlete,
mockStravaStats,
mockPierreActivity,
mockPierreAthlete,
mockPierreStats
} = require('../fixtures/strava-mock-data');
/**
* Mock MCP Client for testing
* Simulates the MCP protocol over stdio
*/
class MockMCPClient extends EventEmitter {
constructor() {
super();
this.requestId = 0;
this.pendingRequests = new Map();
}
async connect(serverUrl = 'http://localhost:8080/mcp') {
// For now, we'll simulate connection
// In future: could connect to real test server
this.connected = true;
}
async send(message) {
const id = ++this.requestId;
const request = {
jsonrpc: '2.0',
id,
...message
};
return new Promise((resolve, reject) => {
this.pendingRequests.set(id, { resolve, reject });
// Simulate MCP response based on mock data
setTimeout(() => {
const response = this.getMockResponse(message);
const pending = this.pendingRequests.get(id);
if (pending) {
this.pendingRequests.delete(id);
if (response.error) {
pending.reject(new Error(response.error.message));
} else {
pending.resolve(response.result);
}
}
}, 50);
});
}
getMockResponse(message) {
// Simulate Pierre server responses based on mock Strava data
if (message.method === 'tools/call') {
const toolName = message.params.name;
const args = message.params.arguments || {};
switch (toolName) {
case 'get_activities':
// Validate required provider parameter
if (!args.provider) {
return {
jsonrpc: '2.0',
id: this.requestId,
error: {
code: -32602,
message: 'Missing required parameter: provider'
}
};
}
// Validate provider is valid
if (args.provider !== 'strava' && args.provider !== 'garmin' && args.provider !== 'fitbit') {
return {
jsonrpc: '2.0',
id: this.requestId,
error: {
code: -32602,
message: `Invalid provider: ${args.provider}`
}
};
}
return {
jsonrpc: '2.0',
id: this.requestId,
result: {
content: [{
type: 'text',
text: JSON.stringify({
activities: [mockPierreActivity],
count: 1,
provider: args.provider
})
}]
}
};
case 'get_athlete':
return {
jsonrpc: '2.0',
id: this.requestId,
result: {
content: [{
type: 'text',
text: JSON.stringify(mockPierreAthlete)
}]
}
};
case 'get_stats':
return {
jsonrpc: '2.0',
id: this.requestId,
result: {
content: [{
type: 'text',
text: JSON.stringify(mockPierreStats)
}]
}
};
default:
return {
jsonrpc: '2.0',
id: this.requestId,
error: {
code: -32601,
message: `Tool not found: ${toolName}`
}
};
}
}
return {
jsonrpc: '2.0',
id: this.requestId,
error: {
code: -32600,
message: 'Invalid request'
}
};
}
disconnect() {
this.connected = false;
this.pendingRequests.clear();
}
}
describe('Strava Provider Integration Tests', () => {
let client;
beforeEach(async () => {
client = new MockMCPClient();
await client.connect();
});
afterEach(() => {
if (client) {
client.disconnect();
}
});
describe('get_activities - Strava Data', () => {
test('should return activities with realistic Strava data structure', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 10
}
}
});
expect(response).toBeDefined();
expect(response.content).toBeDefined();
expect(response.content[0].type).toBe('text');
const result = JSON.parse(response.content[0].text);
expect(result.activities).toBeInstanceOf(Array);
expect(result.activities.length).toBeGreaterThan(0);
expect(result.provider).toBe('strava');
}, 30000);
test('should return activity with all core fields from Strava', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Core activity fields from Strava
expect(activity).toMatchObject({
id: expect.any(String),
name: expect.any(String),
sport_type: expect.any(String),
start_date: expect.any(String),
duration_seconds: expect.any(Number),
provider: 'strava'
});
// Distance should be present for most activities
expect(typeof activity.distance_meters).toBe('number');
expect(activity.distance_meters).toBeGreaterThan(0);
}, 30000);
test('should include Strava-specific metrics (heart rate, speed, elevation)', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Strava typically provides these metrics
expect(activity.average_heart_rate).toBeDefined();
expect(activity.max_heart_rate).toBeDefined();
expect(activity.average_speed).toBeDefined();
expect(activity.max_speed).toBeDefined();
expect(activity.elevation_gain).toBeDefined();
// Validate metric values are reasonable
if (activity.average_heart_rate) {
expect(activity.average_heart_rate).toBeGreaterThan(40);
expect(activity.average_heart_rate).toBeLessThan(220);
}
if (activity.max_heart_rate) {
expect(activity.max_heart_rate).toBeGreaterThan(activity.average_heart_rate || 0);
}
}, 30000);
test('should handle Strava GPS coordinates correctly', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Strava provides start coordinates for outdoor activities
expect(activity.start_latitude).toBeDefined();
expect(activity.start_longitude).toBeDefined();
// Validate coordinates are in valid range
expect(activity.start_latitude).toBeGreaterThanOrEqual(-90);
expect(activity.start_latitude).toBeLessThanOrEqual(90);
expect(activity.start_longitude).toBeGreaterThanOrEqual(-180);
expect(activity.start_longitude).toBeLessThanOrEqual(180);
}, 30000);
test('should parse Strava activity types correctly', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Strava activity types
const validStravaTypes = [
'Run', 'Ride', 'Swim', 'Walk', 'Hike', 'AlpineSki', 'BackcountrySki',
'Canoeing', 'Crossfit', 'EBikeRide', 'Elliptical', 'Golf', 'Handcycle',
'IceSkate', 'InlineSkate', 'Kayaking', 'Kitesurf', 'NordicSki', 'RockClimbing',
'RollerSki', 'Rowing', 'Snowboard', 'Snowshoe', 'Soccer', 'StairStepper',
'StandUpPaddling', 'Surfing', 'VirtualRide', 'VirtualRun', 'WeightTraining',
'Wheelchair', 'Windsurf', 'Workout', 'Yoga'
];
expect(validStravaTypes).toContain(activity.sport_type);
}, 30000);
});
describe('get_athlete - Strava Profile Data', () => {
test('should return athlete profile with Strava data structure', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_athlete',
arguments: {
provider: 'strava'
}
}
});
expect(response).toBeDefined();
expect(response.content).toBeDefined();
const athlete = JSON.parse(response.content[0].text);
// Core Strava athlete fields
expect(athlete).toMatchObject({
id: expect.any(String),
firstname: expect.any(String),
lastname: expect.any(String),
provider: 'strava'
});
}, 30000);
test('should include optional Strava athlete fields when available', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_athlete',
arguments: {
provider: 'strava'
}
}
});
const athlete = JSON.parse(response.content[0].text);
// Strava often provides these fields
expect(athlete.city).toBeDefined();
expect(athlete.state).toBeDefined();
expect(athlete.country).toBeDefined();
expect(athlete.sex).toBeDefined();
// Validate sex field if present
if (athlete.sex) {
expect(['M', 'F']).toContain(athlete.sex);
}
// Validate FTP if present (for cyclists)
if (athlete.ftp) {
expect(athlete.ftp).toBeGreaterThan(0);
expect(athlete.ftp).toBeLessThan(500); // Realistic FTP range
}
}, 30000);
});
describe('get_stats - Strava Statistics', () => {
test('should return stats with Strava totals structure', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_stats',
arguments: {
provider: 'strava'
}
}
});
expect(response).toBeDefined();
const stats = JSON.parse(response.content[0].text);
// Strava provides recent, YTD, and all-time totals
expect(stats.recent_totals).toBeDefined();
expect(stats.ytd_totals).toBeDefined();
expect(stats.all_time_totals).toBeDefined();
expect(stats.provider).toBe('strava');
}, 30000);
test('should include activity-specific stats from Strava', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_stats',
arguments: {
provider: 'strava'
}
}
});
const stats = JSON.parse(response.content[0].text);
// Strava breaks down stats by activity type
const { recent_totals } = stats;
if (recent_totals.runs) {
expect(recent_totals.runs).toMatchObject({
count: expect.any(Number),
distance: expect.any(Number),
duration: expect.any(Number),
elevation: expect.any(Number)
});
}
if (recent_totals.rides) {
expect(recent_totals.rides).toMatchObject({
count: expect.any(Number),
distance: expect.any(Number),
duration: expect.any(Number),
elevation: expect.any(Number)
});
}
}, 30000);
test('should validate Strava stats have realistic values', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_stats',
arguments: {
provider: 'strava'
}
}
});
const stats = JSON.parse(response.content[0].text);
// Validate YTD totals are greater than or equal to recent totals
if (stats.recent_totals.runs && stats.ytd_totals.runs) {
expect(stats.ytd_totals.runs.count).toBeGreaterThanOrEqual(stats.recent_totals.runs.count);
expect(stats.ytd_totals.runs.distance).toBeGreaterThanOrEqual(stats.recent_totals.runs.distance);
}
// Validate all-time totals are greatest
if (stats.ytd_totals.runs && stats.all_time_totals.runs) {
expect(stats.all_time_totals.runs.count).toBeGreaterThanOrEqual(stats.ytd_totals.runs.count);
expect(stats.all_time_totals.runs.distance).toBeGreaterThanOrEqual(stats.ytd_totals.runs.distance);
}
// Biggest ride distance should be positive
if (stats.biggest_ride_distance) {
expect(stats.biggest_ride_distance).toBeGreaterThan(0);
}
// Biggest climb should be positive
if (stats.biggest_climb_elevation) {
expect(stats.biggest_climb_elevation).toBeGreaterThan(0);
}
}, 30000);
});
describe('Error Handling with Strava Provider', () => {
test('should handle missing Strava credentials gracefully', async () => {
// This would fail if Strava credentials are not set up
// For now, we expect the tool to return an error or empty result
await expect(async () => {
await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'invalid_provider',
limit: 10
}
}
});
}).rejects.toThrow();
}, 30000);
test('should require provider parameter for Strava tools', async () => {
await expect(async () => {
await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
limit: 10
// Missing provider parameter
}
}
});
}).rejects.toThrow(/provider/i);
}, 30000);
});
describe('Data Transformation: Strava → Pierre', () => {
test('should correctly transform Strava distance (meters)', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Pierre stores distance in meters (same as Strava)
expect(activity.distance_meters).toBe(8047.2);
}, 30000);
test('should correctly transform Strava time fields (seconds)', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Pierre stores duration in seconds (matches Strava's elapsed_time, not moving_time)
expect(activity.duration_seconds).toBe(2520);
}, 30000);
test('should preserve Strava activity ID as string', async () => {
const response = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: {
provider: 'strava',
limit: 1
}
}
});
const result = JSON.parse(response.content[0].text);
const activity = result.activities[0];
// Strava IDs are large integers, stored as strings to avoid precision loss
expect(typeof activity.id).toBe('string');
expect(activity.id).toBe('10543210987654321');
}, 30000);
test('should add provider field to all Strava data', async () => {
const activityResponse = await client.send({
method: 'tools/call',
params: {
name: 'get_activities',
arguments: { provider: 'strava', limit: 1 }
}
});
const athleteResponse = await client.send({
method: 'tools/call',
params: {
name: 'get_athlete',
arguments: { provider: 'strava' }
}
});
const statsResponse = await client.send({
method: 'tools/call',
params: {
name: 'get_stats',
arguments: { provider: 'strava' }
}
});
const activities = JSON.parse(activityResponse.content[0].text);
const athlete = JSON.parse(athleteResponse.content[0].text);
const stats = JSON.parse(statsResponse.content[0].text);
expect(activities.provider).toBe('strava');
expect(athlete.provider).toBe('strava');
expect(stats.provider).toBe('strava');
}, 30000);
});
});