Browse Source

feat(test): add project seeding utilities for localhost testing

- 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
Matthew Raymer 3 days ago
parent
commit
f5dca34e84
  1. 79
      docs/localhost-testing-guide.md
  2. 249
      scripts/seed-test-projects.js
  3. 170
      scripts/test-api-server-with-seed.js

79
docs/localhost-testing-guide.md

@ -24,15 +24,39 @@ On Android emulator, `localhost` (127.0.0.1) refers to the **emulator itself**,
### 1. Start Your Local Development Server ### 1. Start Your Local Development Server
Start your TimeSafari API server on your host machine: You have two options:
#### Option A: Use the Test API Server (Recommended for Quick Testing)
The plugin includes a ready-to-use test API server with automatic project seeding:
```bash
cd /home/matthew/projects/timesafari/daily-notification-plugin
node scripts/test-api-server-with-seed.js [port]
# Default port: 3000
# Starts on http://localhost:3000
```
This server:
- ✅ Automatically seeds test projects on startup
- ✅ Implements the `/api/v2/report/plansLastUpdatedBetween` endpoint
- ✅ Ready to use immediately with test-project-1, test_project_2, etc.
- ✅ Provides debugging endpoints to view seeded projects
#### Option B: Use Your Existing TimeSafari API Server
If you have your own localhost API server, seed test projects into it:
```bash ```bash
# Example: If using Node.js/Express # Seed projects to your existing API server
cd /path/to/timesafari-api node scripts/seed-test-projects.js seed http://localhost:3000
npm start
# Server starts on http://localhost:3000 # Or export projects as JSON for manual import
node scripts/seed-test-projects.js export test-projects.json
``` ```
Then ensure your server implements the endpoint as described in the "Localhost API Server Requirements" section below.
### 2. Configure Test App for Localhost ### 2. Configure Test App for Localhost
Edit `test-apps/daily-notification-test/src/config/test-user-zero.ts`: Edit `test-apps/daily-notification-test/src/config/test-user-zero.ts`:
@ -160,9 +184,48 @@ Your localhost API server must implement:
- `Content-Type: application/json` - `Content-Type: application/json`
- `User-Agent: TimeSafari-DailyNotificationPlugin/1.0.0` - `User-Agent: TimeSafari-DailyNotificationPlugin/1.0.0`
## Quick Test Script ## Seeding Test Projects
If your localhost API has no projects, you can seed test data using the included scripts:
### Generate Test Projects
```bash
# Generate test projects and display as JSON
node scripts/seed-test-projects.js generate
Create a minimal localhost API server for testing: # Export projects to JSON file
node scripts/seed-test-projects.js export test-projects.json
# Seed projects directly to your API server
node scripts/seed-test-projects.js seed http://localhost:3000
# Use custom project IDs
node scripts/seed-test-projects.js seed http://localhost:3000 "project_1,project_2,project_3"
```
The seed script generates test projects matching the structure expected by the plugin, including:
- `handleId` (from your config)
- `jwtId` (timestamp-based for pagination)
- `planSummary` (name, description, dates, location)
- `previousClaim` (for change detection)
### Using the Test API Server (Includes Seeding)
The easiest way is to use the included test API server:
```bash
node scripts/test-api-server-with-seed.js
```
This server:
1. Seeds 5 test projects automatically on startup
2. Provides the prefetch endpoint ready to use
3. Includes debugging endpoints
## Quick Test Script (Alternative)
If you want to create your own minimal localhost API server:
```javascript ```javascript
// test-api-server.js // test-api-server.js
@ -217,6 +280,8 @@ Run with:
node test-api-server.js node test-api-server.js
``` ```
**Note**: The included `scripts/test-api-server-with-seed.js` provides the same functionality plus automatic seeding, so you may prefer to use that instead.
## Troubleshooting ## Troubleshooting
### Prefetch Not Executing ### Prefetch Not Executing

249
scripts/seed-test-projects.js

@ -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
};

170
scripts/test-api-server-with-seed.js

@ -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…
Cancel
Save