diff --git a/docs/localhost-testing-guide.md b/docs/localhost-testing-guide.md index 986dd17..7704eb8 100644 --- a/docs/localhost-testing-guide.md +++ b/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 -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 -# Example: If using Node.js/Express -cd /path/to/timesafari-api -npm start -# Server starts on http://localhost:3000 +# Seed projects to your existing API server +node scripts/seed-test-projects.js seed 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 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` - `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 // test-api-server.js @@ -217,6 +280,8 @@ Run with: 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 ### Prefetch Not Executing diff --git a/scripts/seed-test-projects.js b/scripts/seed-test-projects.js new file mode 100755 index 0000000..07d4dcd --- /dev/null +++ b/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 +}; + diff --git a/scripts/test-api-server-with-seed.js b/scripts/test-api-server-with-seed.js new file mode 100755 index 0000000..cd476e7 --- /dev/null +++ b/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); +}); +