Browse Source
- Add seed-test-projects.js utility script - Generates test project data matching API schema - Creates projects with handleIds, jwtIds, planSummary, previousClaim - Supports export, seed, and generate commands - Can seed projects to localhost API server - Add test-api-server-with-seed.js - Standalone Express server for localhost testing - Auto-seeds test projects on startup - Implements /api/v2/report/plansLastUpdatedBetween endpoint - Includes debugging endpoints (/api/test/projects, /api/test/health) - Ready to use immediately without database setup - Update localhost testing guide - Add seeding instructions and examples - Document test API server usage - Explain how to integrate with existing API servers This enables testing prefetch functionality even when your localhost API has no project data. The test server can be started immediately and provides 5 seeded test projects ready for prefetch queries.master
3 changed files with 491 additions and 7 deletions
@ -0,0 +1,249 @@ |
|||
#!/usr/bin/env node
|
|||
/** |
|||
* Seed Test Projects for Localhost Testing |
|||
* |
|||
* Creates test project data for localhost API server testing. |
|||
* Can be run standalone or integrated into your local dev server. |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
const http = require('http'); |
|||
|
|||
/** |
|||
* Generate a test JWT ID with timestamp prefix for sorting |
|||
*/ |
|||
function generateJwtId(timestamp = Date.now()) { |
|||
const random = Math.random().toString(36).substring(2, 8); |
|||
const hash = Math.random().toString(36).substring(2, 10); |
|||
return `${Math.floor(timestamp / 1000)}_${random}_${hash}`; |
|||
} |
|||
|
|||
/** |
|||
* Generate test project data |
|||
*/ |
|||
function generateTestProject(handleId, index = 0) { |
|||
const now = Date.now(); |
|||
const startTime = new Date(now + (index * 86400000)); // 1 day apart
|
|||
const endTime = new Date(startTime.getTime() + (30 * 86400000)); // 30 days later
|
|||
|
|||
return { |
|||
jwtId: generateJwtId(now + (index * 1000)), |
|||
handleId: handleId, |
|||
name: `Test Project ${index + 1}`, |
|||
description: `This is test project ${index + 1} for localhost prefetch testing. Created for User Zero starred plans querying.`, |
|||
issuerDid: "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F", |
|||
agentDid: "did:test:agent_" + index, |
|||
startTime: startTime.toISOString(), |
|||
endTime: endTime.toISOString(), |
|||
locLat: 40.7128 + (index * 0.1), // Vary location slightly
|
|||
locLon: -74.0060 + (index * 0.1), |
|||
url: `https://test-project-${index + 1}.timesafari.test`, |
|||
version: "1.0.0" |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Generate complete test project with previous claim |
|||
*/ |
|||
function generateTestProjectWithClaim(handleId, index = 0) { |
|||
const project = generateTestProject(handleId, index); |
|||
const previousClaimJwtId = generateJwtId(Date.now() - (86400000 * 2)); // 2 days ago
|
|||
|
|||
return { |
|||
planSummary: project, |
|||
previousClaim: { |
|||
jwtId: previousClaimJwtId, |
|||
claimType: "project_update", |
|||
claimData: { |
|||
message: `Previous update for ${handleId}`, |
|||
version: "0.9.0" |
|||
}, |
|||
metadata: { |
|||
createdAt: new Date(Date.now() - (86400000 * 2)).toISOString(), |
|||
updatedAt: new Date(Date.now() - (86400000)).toISOString() |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Default test project IDs from config |
|||
*/ |
|||
const DEFAULT_TEST_PROJECT_IDS = [ |
|||
"test_project_1", |
|||
"test_project_2", |
|||
"test_project_3", |
|||
"demo_project_alpha", |
|||
"demo_project_beta" |
|||
]; |
|||
|
|||
/** |
|||
* Generate all test projects |
|||
*/ |
|||
function generateAllTestProjects(projectIds = DEFAULT_TEST_PROJECT_IDS) { |
|||
return projectIds.map((handleId, index) => |
|||
generateTestProjectWithClaim(handleId, index) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Seed projects to localhost API server |
|||
* |
|||
* Makes POST requests to create projects in your local API |
|||
*/ |
|||
function seedToLocalhost(apiUrl, projectIds = DEFAULT_TEST_PROJECT_IDS) { |
|||
return new Promise((resolve, reject) => { |
|||
const projects = generateAllTestProjects(projectIds); |
|||
|
|||
console.log(`📦 Generating ${projects.length} test projects...`); |
|||
projects.forEach((project, index) => { |
|||
console.log(` ${index + 1}. ${project.planSummary.handleId} - ${project.planSummary.name}`); |
|||
}); |
|||
|
|||
// If your API has a seed endpoint, use this:
|
|||
const seedUrl = `${apiUrl}/api/test/seed-projects`; |
|||
const postData = JSON.stringify({ projects }); |
|||
|
|||
const options = { |
|||
method: 'POST', |
|||
headers: { |
|||
'Content-Type': 'application/json', |
|||
'Content-Length': Buffer.byteLength(postData) |
|||
} |
|||
}; |
|||
|
|||
const req = http.request(seedUrl, options, (res) => { |
|||
let data = ''; |
|||
|
|||
res.on('data', (chunk) => { |
|||
data += chunk; |
|||
}); |
|||
|
|||
res.on('end', () => { |
|||
if (res.statusCode === 200 || res.statusCode === 201) { |
|||
console.log('✅ Test projects seeded successfully'); |
|||
console.log(`📊 Response: ${data}`); |
|||
resolve(JSON.parse(data)); |
|||
} else { |
|||
console.error(`❌ Seed failed: ${res.statusCode} ${res.statusMessage}`); |
|||
console.error(`Response: ${data}`); |
|||
reject(new Error(`HTTP ${res.statusCode}: ${data}`)); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
req.on('error', (error) => { |
|||
console.error(`❌ Request error: ${error.message}`); |
|||
reject(error); |
|||
}); |
|||
|
|||
req.write(postData); |
|||
req.end(); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Generate mock API server response data |
|||
* |
|||
* Returns the data structure that your localhost API should return |
|||
*/ |
|||
function getMockApiResponse(projectIds = DEFAULT_TEST_PROJECT_IDS, afterId = null) { |
|||
const allProjects = generateAllTestProjects(projectIds); |
|||
|
|||
// Filter by afterId if provided (simple comparison by jwtId)
|
|||
let filteredProjects = allProjects; |
|||
if (afterId) { |
|||
const afterIndex = allProjects.findIndex(p => p.planSummary.jwtId === afterId); |
|||
if (afterIndex >= 0) { |
|||
filteredProjects = allProjects.slice(afterIndex + 1); |
|||
} else { |
|||
// If afterId not found, return all (for testing)
|
|||
filteredProjects = allProjects; |
|||
} |
|||
} |
|||
|
|||
const nextAfterId = filteredProjects.length > 0 |
|||
? filteredProjects[filteredProjects.length - 1].planSummary.jwtId |
|||
: null; |
|||
|
|||
return { |
|||
data: filteredProjects, |
|||
hitLimit: false, |
|||
pagination: { |
|||
hasMore: false, |
|||
nextAfterId: nextAfterId |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Export project data as JSON file |
|||
*/ |
|||
function exportToJSON(filename = 'test-projects.json', projectIds = DEFAULT_TEST_PROJECT_IDS) { |
|||
const fs = require('fs'); |
|||
const projects = generateAllTestProjects(projectIds); |
|||
const data = { |
|||
projects: projects, |
|||
generatedAt: new Date().toISOString(), |
|||
count: projects.length |
|||
}; |
|||
|
|||
fs.writeFileSync(filename, JSON.stringify(data, null, 2)); |
|||
console.log(`💾 Exported ${projects.length} test projects to ${filename}`); |
|||
return data; |
|||
} |
|||
|
|||
// CLI usage
|
|||
if (require.main === module) { |
|||
const args = process.argv.slice(2); |
|||
const command = args[0]; |
|||
|
|||
switch (command) { |
|||
case 'export': |
|||
{ |
|||
const filename = args[1] || 'test-projects.json'; |
|||
const projectIds = args[2] ? args[2].split(',') : DEFAULT_TEST_PROJECT_IDS; |
|||
exportToJSON(filename, projectIds); |
|||
} |
|||
break; |
|||
|
|||
case 'seed': |
|||
{ |
|||
const apiUrl = args[1] || 'http://localhost:3000'; |
|||
const projectIds = args[2] ? args[2].split(',') : DEFAULT_TEST_PROJECT_IDS; |
|||
seedToLocalhost(apiUrl, projectIds) |
|||
.then(() => { |
|||
console.log('✅ Seeding complete'); |
|||
process.exit(0); |
|||
}) |
|||
.catch((error) => { |
|||
console.error('❌ Seeding failed:', error.message); |
|||
process.exit(1); |
|||
}); |
|||
} |
|||
break; |
|||
|
|||
case 'generate': |
|||
default: |
|||
{ |
|||
const projectIds = args[1] ? args[1].split(',') : DEFAULT_TEST_PROJECT_IDS; |
|||
const projects = generateAllTestProjects(projectIds); |
|||
console.log(JSON.stringify({ data: projects }, null, 2)); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
generateTestProject, |
|||
generateTestProjectWithClaim, |
|||
generateAllTestProjects, |
|||
getMockApiResponse, |
|||
seedToLocalhost, |
|||
exportToJSON, |
|||
DEFAULT_TEST_PROJECT_IDS, |
|||
generateJwtId |
|||
}; |
|||
|
|||
@ -0,0 +1,170 @@ |
|||
#!/usr/bin/env node
|
|||
/** |
|||
* Test API Server with Project Seeding |
|||
* |
|||
* A simple Express server that: |
|||
* 1. Provides the plansLastUpdatedBetween endpoint for prefetch testing |
|||
* 2. Automatically seeds test projects on startup |
|||
* 3. Returns seeded project data in API responses |
|||
* |
|||
* Usage: |
|||
* node scripts/test-api-server-with-seed.js [port] |
|||
* |
|||
* Then in Android emulator, configure: |
|||
* api.serverMode = "localhost" |
|||
* api.servers.localhost.android = "http://10.0.2.2:3000" |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
const express = require('express'); |
|||
const { generateAllTestProjects, getMockApiResponse, DEFAULT_TEST_PROJECT_IDS } = require('./seed-test-projects'); |
|||
|
|||
const PORT = process.argv[2] || 3000; |
|||
const app = express(); |
|||
|
|||
// Store seeded projects in memory
|
|||
let seededProjects = []; |
|||
|
|||
// Middleware
|
|||
app.use(express.json()); |
|||
app.use((req, res, next) => { |
|||
console.log(`📥 ${req.method} ${req.path}`); |
|||
next(); |
|||
}); |
|||
|
|||
/** |
|||
* Seed test projects on startup |
|||
*/ |
|||
function seedProjects(projectIds = DEFAULT_TEST_PROJECT_IDS) { |
|||
seededProjects = generateAllTestProjects(projectIds); |
|||
console.log(`🌱 Seeded ${seededProjects.length} test projects:`); |
|||
seededProjects.forEach((project, index) => { |
|||
console.log(` ${index + 1}. ${project.planSummary.handleId} - ${project.planSummary.name}`); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* POST /api/v2/report/plansLastUpdatedBetween |
|||
* |
|||
* Endpoint that the plugin calls for starred projects prefetch |
|||
*/ |
|||
app.post('/api/v2/report/plansLastUpdatedBetween', (req, res) => { |
|||
const { planIds, afterId } = req.body; |
|||
|
|||
console.log('📥 Prefetch request received:', { |
|||
planIds: planIds || 'not provided', |
|||
afterId: afterId || 'none', |
|||
authorization: req.headers.authorization ? 'present' : 'missing', |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
|
|||
// Filter seeded projects to only those requested
|
|||
let filteredProjects = seededProjects; |
|||
if (planIds && Array.isArray(planIds) && planIds.length > 0) { |
|||
filteredProjects = seededProjects.filter(p => |
|||
planIds.includes(p.planSummary.handleId) |
|||
); |
|||
console.log(` Filtered to ${filteredProjects.length} projects matching planIds`); |
|||
} |
|||
|
|||
// Filter by afterId if provided
|
|||
if (afterId) { |
|||
const afterIndex = filteredProjects.findIndex(p => p.planSummary.jwtId === afterId); |
|||
if (afterIndex >= 0) { |
|||
filteredProjects = filteredProjects.slice(afterIndex + 1); |
|||
console.log(` Filtered to ${filteredProjects.length} projects after ${afterId}`); |
|||
} |
|||
} |
|||
|
|||
// Generate response
|
|||
const response = { |
|||
data: filteredProjects, |
|||
hitLimit: false, |
|||
pagination: { |
|||
hasMore: false, |
|||
nextAfterId: filteredProjects.length > 0 |
|||
? filteredProjects[filteredProjects.length - 1].planSummary.jwtId |
|||
: null |
|||
} |
|||
}; |
|||
|
|||
console.log(`📤 Sending ${response.data.length} projects in response`); |
|||
|
|||
res.json(response); |
|||
}); |
|||
|
|||
/** |
|||
* GET /api/test/projects |
|||
* |
|||
* View all seeded projects (for debugging) |
|||
*/ |
|||
app.get('/api/test/projects', (req, res) => { |
|||
res.json({ |
|||
projects: seededProjects, |
|||
count: seededProjects.length, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
}); |
|||
|
|||
/** |
|||
* POST /api/test/seed-projects |
|||
* |
|||
* Reseed test projects (useful for resetting) |
|||
*/ |
|||
app.post('/api/test/seed-projects', (req, res) => { |
|||
const { projectIds } = req.body; |
|||
const idsToUse = projectIds || DEFAULT_TEST_PROJECT_IDS; |
|||
|
|||
seedProjects(idsToUse); |
|||
|
|||
res.json({ |
|||
success: true, |
|||
message: `Seeded ${seededProjects.length} test projects`, |
|||
count: seededProjects.length, |
|||
projectIds: idsToUse |
|||
}); |
|||
}); |
|||
|
|||
/** |
|||
* GET /api/test/health |
|||
* |
|||
* Health check endpoint |
|||
*/ |
|||
app.get('/api/test/health', (req, res) => { |
|||
res.json({ |
|||
status: 'ok', |
|||
server: 'test-api-server-with-seed', |
|||
port: PORT, |
|||
projectsSeeded: seededProjects.length, |
|||
timestamp: new Date().toISOString() |
|||
}); |
|||
}); |
|||
|
|||
// Start server
|
|||
seedProjects(); // Seed on startup
|
|||
|
|||
app.listen(PORT, () => { |
|||
console.log(''); |
|||
console.log('🧪 Test API Server with Project Seeding'); |
|||
console.log('========================================'); |
|||
console.log(`📡 Server running on http://localhost:${PORT}`); |
|||
console.log(`📱 Android emulator URL: http://10.0.2.2:${PORT}`); |
|||
console.log(''); |
|||
console.log('Endpoints:'); |
|||
console.log(` POST /api/v2/report/plansLastUpdatedBetween - Main prefetch endpoint`); |
|||
console.log(` GET /api/test/projects - View all seeded projects`); |
|||
console.log(` POST /api/test/seed-projects - Reseed projects`); |
|||
console.log(` GET /api/test/health - Health check`); |
|||
console.log(''); |
|||
console.log('✅ Ready for prefetch testing!'); |
|||
console.log(''); |
|||
}); |
|||
|
|||
// Handle graceful shutdown
|
|||
process.on('SIGINT', () => { |
|||
console.log('\n👋 Shutting down test API server...'); |
|||
process.exit(0); |
|||
}); |
|||
|
|||
Loading…
Reference in new issue