feat(test): add localhost testing support for prefetch
- Add serverMode configuration to test-user-zero config - Supports: localhost, staging, production, mock, custom - Auto-detects platform (Android/iOS/Web) for localhost URLs - Android emulator uses 10.0.2.2 for host machine localhost - Add getApiServerUrl() helper function - Returns correct URL based on serverMode and platform - Handles Android emulator special case (10.0.2.2) - Update TestUserZeroAPI to respect serverMode - Checks mock mode before making network calls - Uses getApiServerUrl() for base URL resolution - Allows runtime URL switching via setBaseUrl() - Add localhost testing configuration - Configurable port and HTTPS settings - Development mode headers support - Create localhost testing guide - Step-by-step setup instructions - Platform-specific localhost addresses explained - Quick test API server example - Troubleshooting common issues - Monitoring prefetch execution commands - Update Capacitor config to use getApiServerUrl() - Fixes breaking change from api.server removal This enables testing prefetch functionality with a local development API server running on localhost, perfect for development and debugging of the 5-minute prefetch scheduling feature.
This commit is contained in:
293
docs/localhost-testing-guide.md
Normal file
293
docs/localhost-testing-guide.md
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
# Localhost Testing Guide
|
||||||
|
|
||||||
|
**Author**: Matthew Raymer
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Status**: Testing Setup
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide explains how to test the daily notification plugin's prefetch functionality with a localhost development API server running on your host machine.
|
||||||
|
|
||||||
|
## Android Emulator Localhost Access
|
||||||
|
|
||||||
|
On Android emulator, `localhost` (127.0.0.1) refers to the **emulator itself**, not your host machine. To access your host machine's localhost from the Android emulator, use:
|
||||||
|
|
||||||
|
**`10.0.2.2`** - Special alias that maps to host machine's localhost
|
||||||
|
|
||||||
|
### Platform-Specific Localhost Addresses
|
||||||
|
|
||||||
|
- **Android Emulator**: `http://10.0.2.2:PORT`
|
||||||
|
- **iOS Simulator**: `http://localhost:PORT` or `http://127.0.0.1:PORT`
|
||||||
|
- **Web Browser**: `http://localhost:PORT`
|
||||||
|
|
||||||
|
## Setup Steps
|
||||||
|
|
||||||
|
### 1. Start Your Local Development Server
|
||||||
|
|
||||||
|
Start your TimeSafari API server on your host machine:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example: If using Node.js/Express
|
||||||
|
cd /path/to/timesafari-api
|
||||||
|
npm start
|
||||||
|
# Server starts on http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Test App for Localhost
|
||||||
|
|
||||||
|
Edit `test-apps/daily-notification-test/src/config/test-user-zero.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
api: {
|
||||||
|
serverMode: "localhost", // Change from "mock" to "localhost"
|
||||||
|
servers: {
|
||||||
|
localhost: {
|
||||||
|
android: "http://10.0.2.2:3000", // Match your local server port
|
||||||
|
ios: "http://localhost:3000",
|
||||||
|
web: "http://localhost:3000"
|
||||||
|
},
|
||||||
|
// ... other server configs
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
testing: {
|
||||||
|
enableMockResponses: false, // Disable mocks to use real localhost API
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Update Starred Plans in Test App
|
||||||
|
|
||||||
|
In the test app UI (UserZeroView), use `updateStarredPlans()` to set which project IDs to query:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In UserZeroView.vue or your test code
|
||||||
|
await DailyNotification.updateStarredPlans({
|
||||||
|
planIds: ["test_project_1", "test_project_2", "test_project_3"]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Schedule Notification for Testing
|
||||||
|
|
||||||
|
Schedule a notification that will trigger prefetch 5 minutes before:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
await DailyNotification.scheduleDailyNotification({
|
||||||
|
time: "11:25", // Current time + 5+ minutes for testing
|
||||||
|
title: "Test Notification",
|
||||||
|
body: "Testing prefetch with localhost",
|
||||||
|
priority: "high"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The prefetch will run 5 minutes before (11:20 in this example) and query your localhost API.
|
||||||
|
|
||||||
|
## Testing Prefetch Flow
|
||||||
|
|
||||||
|
### Expected Timeline
|
||||||
|
|
||||||
|
1. **T-0**: You schedule notification for `11:25`
|
||||||
|
2. **T+0**: Plugin schedules prefetch for `11:20` (5 minutes before)
|
||||||
|
3. **T+0**: Plugin logs:
|
||||||
|
```
|
||||||
|
DN|SCHEDULE_FETCH_START time=...
|
||||||
|
DN|WORK_ENQUEUED work_id=... fetch_at=... delay_ms=...
|
||||||
|
```
|
||||||
|
4. **T+~5 minutes**: WorkManager triggers prefetch at `11:20`
|
||||||
|
5. **T+~5 minutes**: Plugin queries `http://10.0.2.2:3000/api/v2/report/plansLastUpdatedBetween`
|
||||||
|
6. **T+~10 minutes**: Notification displays at `11:25`
|
||||||
|
|
||||||
|
### Monitoring Prefetch
|
||||||
|
|
||||||
|
Watch logs for prefetch execution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Filter for prefetch-related logs
|
||||||
|
adb logcat | grep -E "DN|SCHEDULE_FETCH|FETCH_PROJECT|WORK_ENQUEUED"
|
||||||
|
|
||||||
|
# Watch WorkManager queue
|
||||||
|
adb shell dumpsys jobscheduler | grep daily_notification
|
||||||
|
|
||||||
|
# Monitor network requests (if your server logs them)
|
||||||
|
# Check your localhost server logs for POST requests to /api/v2/report/plansLastUpdatedBetween
|
||||||
|
```
|
||||||
|
|
||||||
|
## Localhost API Server Requirements
|
||||||
|
|
||||||
|
Your localhost API server must implement:
|
||||||
|
|
||||||
|
### Endpoint: `POST /api/v2/report/plansLastUpdatedBetween`
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"planIds": ["test_project_1", "test_project_2"],
|
||||||
|
"afterId": "optional_jwt_id_for_pagination"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"planSummary": {
|
||||||
|
"jwtId": "jwt_id_1",
|
||||||
|
"handleId": "test_project_1",
|
||||||
|
"name": "Test Project 1",
|
||||||
|
"description": "Project description",
|
||||||
|
"issuerDid": "did:key:...",
|
||||||
|
"agentDid": "did:key:...",
|
||||||
|
"startTime": "2025-01-01T00:00:00Z",
|
||||||
|
"endTime": "2025-01-31T23:59:59Z"
|
||||||
|
},
|
||||||
|
"previousClaim": {
|
||||||
|
"jwtId": "previous_jwt_id",
|
||||||
|
"claimType": "project_update"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hitLimit": false,
|
||||||
|
"pagination": {
|
||||||
|
"hasMore": false,
|
||||||
|
"nextAfterId": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Headers Required:**
|
||||||
|
- `Authorization: Bearer <JWT_TOKEN>`
|
||||||
|
- `Content-Type: application/json`
|
||||||
|
- `User-Agent: TimeSafari-DailyNotificationPlugin/1.0.0`
|
||||||
|
|
||||||
|
## Quick Test Script
|
||||||
|
|
||||||
|
Create a minimal localhost API server for testing:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// test-api-server.js
|
||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.post('/api/v2/report/plansLastUpdatedBetween', (req, res) => {
|
||||||
|
console.log('📥 Prefetch request received:', {
|
||||||
|
planIds: req.body.planIds,
|
||||||
|
afterId: req.body.afterId,
|
||||||
|
headers: req.headers
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return mock response
|
||||||
|
res.json({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
planSummary: {
|
||||||
|
jwtId: `${Date.now()}_test_123`,
|
||||||
|
handleId: req.body.planIds[0] || "test_project",
|
||||||
|
name: "Localhost Test Project",
|
||||||
|
description: "Testing prefetch from localhost",
|
||||||
|
issuerDid: "did:test:issuer",
|
||||||
|
agentDid: "did:test:agent",
|
||||||
|
startTime: new Date().toISOString(),
|
||||||
|
endTime: new Date(Date.now() + 86400000).toISOString()
|
||||||
|
},
|
||||||
|
previousClaim: {
|
||||||
|
jwtId: "previous_test_jwt",
|
||||||
|
claimType: "project_update"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
hitLimit: false,
|
||||||
|
pagination: {
|
||||||
|
hasMore: false,
|
||||||
|
nextAfterId: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(3000, () => {
|
||||||
|
console.log('🧪 Test API server running on http://localhost:3000');
|
||||||
|
console.log('📡 Android emulator should use: http://10.0.2.2:3000');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with:
|
||||||
|
```bash
|
||||||
|
node test-api-server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Prefetch Not Executing
|
||||||
|
|
||||||
|
1. **Check WorkManager queue:**
|
||||||
|
```bash
|
||||||
|
adb shell dumpsys jobscheduler | grep -A 20 "daily_notification"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Verify prefetch was scheduled:**
|
||||||
|
```bash
|
||||||
|
adb logcat | grep "DN|SCHEDULE_FETCH"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check if WorkManager has constraints:**
|
||||||
|
- Battery optimization might be blocking execution
|
||||||
|
- Network might not be available
|
||||||
|
- Device might be in deep sleep
|
||||||
|
|
||||||
|
### Cannot Connect to Localhost
|
||||||
|
|
||||||
|
1. **Android Emulator**: Use `10.0.2.2` not `localhost` or `127.0.0.1`
|
||||||
|
2. **Firewall**: Check if your firewall is blocking port 3000
|
||||||
|
3. **Server not running**: Verify server is listening on correct port
|
||||||
|
4. **Wrong port**: Match the port in config with your server's port
|
||||||
|
|
||||||
|
### Network Timeout
|
||||||
|
|
||||||
|
Increase timeout in config:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
testing: {
|
||||||
|
timeoutMs: 60000, // Increase from 30000 to 60000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Errors
|
||||||
|
|
||||||
|
Your localhost server should accept the JWT token generated by the plugin. For testing, you can:
|
||||||
|
|
||||||
|
1. **Disable JWT validation** in your localhost server
|
||||||
|
2. **Use a test JWT secret** that matches plugin config
|
||||||
|
3. **Log the JWT** in your server to see what's being sent
|
||||||
|
|
||||||
|
## Switching Between Mock and Localhost
|
||||||
|
|
||||||
|
You can switch modes without rebuilding:
|
||||||
|
|
||||||
|
1. **Mock Mode** (offline testing):
|
||||||
|
```typescript
|
||||||
|
api.serverMode = "mock";
|
||||||
|
testing.enableMockResponses = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Localhost Mode** (real API calls to localhost):
|
||||||
|
```typescript
|
||||||
|
api.serverMode = "localhost";
|
||||||
|
testing.enableMockResponses = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Staging Mode** (real API calls to staging):
|
||||||
|
```typescript
|
||||||
|
api.serverMode = "staging";
|
||||||
|
testing.enableMockResponses = false;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Test prefetch execution with real localhost API
|
||||||
|
- Monitor network traffic to verify requests
|
||||||
|
- Test with different starred plan IDs
|
||||||
|
- Verify prefetch timing (5 minutes before notification)
|
||||||
|
- Test notification delivery after prefetch completes
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ const config: CapacitorConfig = {
|
|||||||
timesafariConfig: {
|
timesafariConfig: {
|
||||||
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
|
activeDid: TEST_USER_ZERO_CONFIG.identity.did,
|
||||||
endpoints: {
|
endpoints: {
|
||||||
projectsLastUpdated: `${TEST_USER_ZERO_CONFIG.api.server}${TEST_USER_ZERO_CONFIG.api.starsEndpoint}`
|
// Use getApiServerUrl() to get correct URL based on serverMode
|
||||||
|
projectsLastUpdated: `${TEST_USER_ZERO_CONFIG.getApiServerUrl()}${TEST_USER_ZERO_CONFIG.api.starsEndpoint}`
|
||||||
},
|
},
|
||||||
starredProjectsConfig: {
|
starredProjectsConfig: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|||||||
@@ -19,8 +19,27 @@ export const TEST_USER_ZERO_CONFIG = {
|
|||||||
|
|
||||||
// API Configuration
|
// API Configuration
|
||||||
api: {
|
api: {
|
||||||
// Use staging API server for testing
|
// API server selection:
|
||||||
server: "https://api-staging.timesafari.com",
|
// - "localhost": Use 10.0.2.2:PORT (Android emulator special alias for host localhost)
|
||||||
|
// - "staging": Use staging API server
|
||||||
|
// - "production": Use production API server
|
||||||
|
// - "mock": Use mock responses (no network calls)
|
||||||
|
// - "custom": Use servers.custom URL
|
||||||
|
serverMode: "mock" as "localhost" | "staging" | "production" | "mock" | "custom",
|
||||||
|
|
||||||
|
// Server URLs for different modes
|
||||||
|
servers: {
|
||||||
|
// Android emulator: use 10.0.2.2 to access host machine's localhost
|
||||||
|
// iOS simulator: use localhost or 127.0.0.1
|
||||||
|
localhost: {
|
||||||
|
android: "http://10.0.2.2:3000", // Default local dev server port
|
||||||
|
ios: "http://localhost:3000",
|
||||||
|
web: "http://localhost:3000"
|
||||||
|
},
|
||||||
|
staging: "https://api-staging.timesafari.com",
|
||||||
|
production: "https://api.timesafari.com",
|
||||||
|
custom: "" // Set this if using custom URL
|
||||||
|
},
|
||||||
|
|
||||||
// Stars querying endpoint (from crowd-master endorserServer.ts)
|
// Stars querying endpoint (from crowd-master endorserServer.ts)
|
||||||
starsEndpoint: "/api/v2/report/plansLastUpdatedBetween",
|
starsEndpoint: "/api/v2/report/plansLastUpdatedBetween",
|
||||||
@@ -29,6 +48,36 @@ export const TEST_USER_ZERO_CONFIG = {
|
|||||||
jwtExpirationMinutes: 1, // Short-lived tokens like crowd-master
|
jwtExpirationMinutes: 1, // Short-lived tokens like crowd-master
|
||||||
jwtAlgorithm: "HS256"
|
jwtAlgorithm: "HS256"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current API server URL based on mode and platform
|
||||||
|
* Call this function instead of accessing api.server directly
|
||||||
|
*/
|
||||||
|
getApiServerUrl(): string {
|
||||||
|
if (TEST_USER_ZERO_CONFIG.api.serverMode === "mock") {
|
||||||
|
return "mock://localhost"; // Not used, but needed for type checking
|
||||||
|
}
|
||||||
|
if (TEST_USER_ZERO_CONFIG.api.serverMode === "localhost") {
|
||||||
|
// Detect platform and use appropriate localhost address
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||||
|
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
|
||||||
|
if (isAndroid) {
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers.localhost.android;
|
||||||
|
} else if (isIOS) {
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers.localhost.ios;
|
||||||
|
} else {
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers.localhost.web;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers.localhost.android; // Default to Android for Capacitor
|
||||||
|
}
|
||||||
|
if (TEST_USER_ZERO_CONFIG.api.serverMode === "custom" && TEST_USER_ZERO_CONFIG.api.servers.custom) {
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers.custom;
|
||||||
|
}
|
||||||
|
return TEST_USER_ZERO_CONFIG.api.servers[TEST_USER_ZERO_CONFIG.api.serverMode] as string;
|
||||||
|
},
|
||||||
|
|
||||||
// Test Starred Projects (mock data for testing)
|
// Test Starred Projects (mock data for testing)
|
||||||
starredProjects: {
|
starredProjects: {
|
||||||
@@ -61,8 +110,23 @@ export const TEST_USER_ZERO_CONFIG = {
|
|||||||
// Testing Configuration
|
// Testing Configuration
|
||||||
testing: {
|
testing: {
|
||||||
// Enable mock responses for offline testing
|
// Enable mock responses for offline testing
|
||||||
|
// When true, API calls return mock data instead of making network requests
|
||||||
enableMockResponses: true,
|
enableMockResponses: true,
|
||||||
|
|
||||||
|
// Localhost testing configuration
|
||||||
|
localhost: {
|
||||||
|
// Port your local dev server runs on
|
||||||
|
port: 3000,
|
||||||
|
|
||||||
|
// Enable HTTPS for localhost (requires self-signed cert in emulator)
|
||||||
|
useHttps: false,
|
||||||
|
|
||||||
|
// Additional headers for localhost requests
|
||||||
|
headers: {
|
||||||
|
"X-Development-Mode": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// Network timeouts
|
// Network timeouts
|
||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
retryAttempts: 3,
|
retryAttempts: 3,
|
||||||
@@ -160,11 +224,22 @@ export class TestUserZeroAPI {
|
|||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private jwt: string;
|
private jwt: string;
|
||||||
|
|
||||||
constructor(baseUrl: string = TEST_USER_ZERO_CONFIG.api.server) {
|
constructor(baseUrl?: string) {
|
||||||
this.baseUrl = baseUrl;
|
// Use provided URL, or get from config based on serverMode
|
||||||
|
this.baseUrl = baseUrl || (TEST_USER_ZERO_CONFIG.api.serverMode === "mock"
|
||||||
|
? "mock://localhost"
|
||||||
|
: TEST_USER_ZERO_CONFIG.getApiServerUrl());
|
||||||
this.jwt = generateTestJWT();
|
this.jwt = generateTestJWT();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API server URL (useful for switching between localhost, staging, etc.)
|
||||||
|
*/
|
||||||
|
setBaseUrl(url: string): void {
|
||||||
|
this.baseUrl = url;
|
||||||
|
console.log("🔧 API base URL updated to:", url);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query starred projects for changes
|
* Query starred projects for changes
|
||||||
* Mimics crowd-master getStarredProjectsWithChanges function
|
* Mimics crowd-master getStarredProjectsWithChanges function
|
||||||
@@ -173,15 +248,19 @@ export class TestUserZeroAPI {
|
|||||||
starredPlanIds: string[],
|
starredPlanIds: string[],
|
||||||
afterId?: string
|
afterId?: string
|
||||||
): Promise<typeof MOCK_STARRED_PROJECTS_RESPONSE> {
|
): Promise<typeof MOCK_STARRED_PROJECTS_RESPONSE> {
|
||||||
if (TEST_USER_ZERO_CONFIG.testing.enableMockResponses) {
|
// Check if we should use mock responses
|
||||||
|
const useMock = TEST_USER_ZERO_CONFIG.testing.enableMockResponses ||
|
||||||
|
TEST_USER_ZERO_CONFIG.api.serverMode === "mock";
|
||||||
|
|
||||||
|
if (useMock) {
|
||||||
// Return mock data for offline testing
|
// Return mock data for offline testing
|
||||||
|
|
||||||
console.log("🧪 Using mock starred projects response");
|
console.log("🧪 Using mock starred projects response");
|
||||||
return MOCK_STARRED_PROJECTS_RESPONSE;
|
return MOCK_STARRED_PROJECTS_RESPONSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real API call (when mock is disabled)
|
// Real API call (when mock is disabled)
|
||||||
const url = `${this.baseUrl}${TEST_USER_ZERO_CONFIG.api.starsEndpoint}`;
|
const baseUrl = this.baseUrl || TEST_USER_ZERO_CONFIG.getApiServerUrl();
|
||||||
|
const url = `${baseUrl}${TEST_USER_ZERO_CONFIG.api.starsEndpoint}`;
|
||||||
const headers = {
|
const headers = {
|
||||||
'Authorization': `Bearer ${this.jwt}`,
|
'Authorization': `Bearer ${this.jwt}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
Reference in New Issue
Block a user