chore: update Android test app dependencies to latest stable versions

- Update AndroidX AppCompat from 1.6.1 to 1.7.1 (latest stable)
- Update AndroidX Activity from 1.7.0 to 1.8.2
- Update AndroidX Core from 1.10.0 to 1.12.0
- Update AndroidX Fragment from 1.5.6 to 1.6.2
- Update Core Splash Screen from 1.0.0 to 1.0.1
- Update AndroidX WebKit from 1.6.1 to 1.8.0
- Update compile/target SDK from 33 to 34
- Update Gradle troubleshooting guide with latest versions

Dependency updates:
- androidx.appcompat:appcompat: 1.6.1 → 1.7.1
- androidx.activity:activity: 1.7.0 → 1.8.2
- androidx.core:core: 1.10.0 → 1.12.0
- androidx.fragment:fragment: 1.5.6 → 1.6.2
- androidx.core:core-splashscreen: 1.0.0 → 1.0.1
- androidx.webkit:webkit: 1.6.1 → 1.8.0
- compileSdkVersion: 33 → 34
- targetSdkVersion: 33 → 34

Documentation updates:
- Updated Gradle troubleshooting guide with latest versions
- Added dependency update section
- Updated version compatibility table
- Added AndroidX dependency update examples

Files: 2 modified
- Modified: android/variables.gradle (updated all AndroidX versions)
- Modified: GRADLE_TROUBLESHOOTING.md (updated documentation)
This commit is contained in:
Matthew Raymer
2025-09-09 08:34:16 +00:00
parent 956abff320
commit a2d1fb33a6
16 changed files with 12330 additions and 47 deletions

152
test-apps/test-api/.gitignore vendored Normal file
View File

@@ -0,0 +1,152 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories
jspm_packages/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env.local
.env.production
# parcel-bundler cache
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
public
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage
.grunt
# Bower dependency directory
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@@ -0,0 +1,282 @@
# Test API Server
A mock REST API server for testing the Daily Notification Plugin's network functionality, ETag support, and error handling capabilities.
## Features
- **Content Endpoints**: Generate mock notification content for different time slots
- **ETag Support**: Full HTTP caching with conditional requests (304 Not Modified)
- **Error Simulation**: Test various error scenarios (timeout, server error, rate limiting)
- **Metrics**: Monitor API usage and performance
- **CORS Enabled**: Cross-origin requests supported for web testing
## Quick Start
```bash
# Install dependencies
npm install
# Start server
npm start
# Development mode with auto-restart
npm run dev
```
## API Endpoints
### Health Check
```http
GET /health
```
**Response:**
```json
{
"status": "healthy",
"timestamp": 1703123456789,
"version": "1.0.0",
"endpoints": {
"content": "/api/content/:slotId",
"health": "/health",
"metrics": "/api/metrics",
"error": "/api/error/:type"
}
}
```
### Get Notification Content
```http
GET /api/content/:slotId
```
**Parameters:**
- `slotId`: Slot identifier in format `slot-HH:MM` (e.g., `slot-08:00`)
**Headers:**
- `If-None-Match`: ETag for conditional requests
**Response (200 OK):**
```json
{
"id": "abc12345",
"slotId": "slot-08:00",
"title": "Daily Update - 08:00",
"body": "Your personalized content for 08:00. Content ID: abc12345",
"timestamp": 1703123456789,
"priority": "high",
"category": "daily",
"actions": [
{ "id": "view", "title": "View Details" },
{ "id": "dismiss", "title": "Dismiss" }
],
"metadata": {
"source": "test-api",
"version": "1.0.0",
"generated": "2023-12-21T08:00:00.000Z"
}
}
```
**Response (304 Not Modified):**
When `If-None-Match` header matches current ETag.
### Update Content
```http
PUT /api/content/:slotId
```
**Body:**
```json
{
"content": {
"title": "Custom Title",
"body": "Custom body content"
}
}
```
### Clear All Content
```http
DELETE /api/content
```
### Simulate Errors
```http
GET /api/error/:type
```
**Error Types:**
- `timeout` - Simulates request timeout (15 seconds)
- `server-error` - Returns 500 Internal Server Error
- `not-found` - Returns 404 Not Found
- `rate-limit` - Returns 429 Rate Limit Exceeded
- `unauthorized` - Returns 401 Unauthorized
### API Metrics
```http
GET /api/metrics
```
**Response:**
```json
{
"timestamp": 1703123456789,
"contentStore": {
"size": 5,
"slots": ["slot-08:00", "slot-12:00", "slot-18:00"]
},
"etagStore": {
"size": 5,
"etags": [["slot-08:00", "\"abc123\""]]
},
"uptime": 3600,
"memory": {
"rss": 50331648,
"heapTotal": 20971520,
"heapUsed": 15728640,
"external": 1048576
}
}
```
## Usage Examples
### Basic Content Fetch
```bash
curl http://localhost:3001/api/content/slot-08:00
```
### ETag Conditional Request
```bash
# First request
curl -v http://localhost:3001/api/content/slot-08:00
# Second request with ETag (should return 304)
curl -v -H "If-None-Match: \"abc123\"" http://localhost:3001/api/content/slot-08:00
```
### Error Testing
```bash
# Test timeout
curl http://localhost:3001/api/error/timeout
# Test server error
curl http://localhost:3001/api/error/server-error
# Test rate limiting
curl http://localhost:3001/api/error/rate-limit
```
## Integration with Test Apps
### Android Test App
```typescript
// In your Android test app
const API_BASE_URL = 'http://10.0.2.2:3001'; // Android emulator localhost
const fetchContent = async (slotId: string) => {
const response = await fetch(`${API_BASE_URL}/api/content/${slotId}`);
return response.json();
};
```
### iOS Test App
```typescript
// In your iOS test app
const API_BASE_URL = 'http://localhost:3001'; // iOS simulator localhost
const fetchContent = async (slotId: string) => {
const response = await fetch(`${API_BASE_URL}/api/content/${slotId}`);
return response.json();
};
```
### Electron Test App
```typescript
// In your Electron test app
const API_BASE_URL = 'http://localhost:3001';
const fetchContent = async (slotId: string) => {
const response = await fetch(`${API_BASE_URL}/api/content/${slotId}`);
return response.json();
};
```
## Configuration
### Environment Variables
- `PORT`: Server port (default: 3001)
- `NODE_ENV`: Environment mode (development/production)
### CORS Configuration
The server is configured to allow cross-origin requests from any origin for testing purposes.
## Testing Scenarios
### 1. Basic Content Fetching
- Test successful content retrieval
- Verify content structure and format
- Check timestamp accuracy
### 2. ETag Caching
- Test conditional requests with `If-None-Match`
- Verify 304 Not Modified responses
- Test cache invalidation
### 3. Error Handling
- Test timeout scenarios
- Test server error responses
- Test rate limiting behavior
- Test network failure simulation
### 4. Performance Testing
- Test concurrent requests
- Monitor memory usage
- Test long-running scenarios
## Development
### Running in Development Mode
```bash
npm run dev
```
This uses `nodemon` for automatic server restart on file changes.
### Adding New Endpoints
1. Add route handler in `server.js`
2. Update health check endpoint list
3. Add documentation to this README
4. Add test cases if applicable
### Testing
```bash
npm test
```
## Troubleshooting
### Common Issues
1. **Port Already in Use**
```bash
# Kill process using port 3001
lsof -ti:3001 | xargs kill -9
```
2. **CORS Issues**
- Server is configured to allow all origins
- Check browser console for CORS errors
3. **Network Connectivity**
- Android emulator: Use `10.0.2.2` instead of `localhost`
- iOS simulator: Use `localhost` or `127.0.0.1`
- Physical devices: Use your computer's IP address
### Logs
The server logs all requests with timestamps and response codes for debugging.
## License
MIT License - See LICENSE file for details.

View File

@@ -0,0 +1,76 @@
# Test API Server Setup
## Overview
The Test API Server provides mock endpoints for testing the Daily Notification Plugin's network functionality, including ETag support, error handling, and content fetching.
## Quick Setup
```bash
# Navigate to test-api directory
cd test-apps/test-api
# Install dependencies
npm install
# Start server
npm start
```
## Integration with Test Apps
### Update Test App Configuration
Add the API base URL to your test app configuration:
```typescript
// In your test app's config
const API_CONFIG = {
baseUrl: 'http://localhost:3001', // Adjust for platform
endpoints: {
content: '/api/content',
health: '/health',
error: '/api/error',
metrics: '/api/metrics'
}
};
```
### Platform-Specific URLs
- **Web/Electron**: `http://localhost:3001`
- **Android Emulator**: `http://10.0.2.2:3001`
- **iOS Simulator**: `http://localhost:3001`
- **Physical Devices**: `http://[YOUR_IP]:3001`
## Testing Workflow
1. **Start API Server**: `npm start` in `test-apps/test-api/`
2. **Start Test App**: Run your platform-specific test app
3. **Test Scenarios**: Use the test app to validate plugin functionality
4. **Monitor API**: Check `/api/metrics` for usage statistics
## Available Test Scenarios
### Content Fetching
- Basic content retrieval
- ETag conditional requests
- Content updates and caching
### Error Handling
- Network timeouts
- Server errors
- Rate limiting
- Authentication failures
### Performance Testing
- Concurrent requests
- Memory usage monitoring
- Long-running scenarios
## Next Steps
1. Start the API server
2. Configure your test apps to use the API
3. Run through the test scenarios
4. Validate plugin functionality across platforms

View File

@@ -0,0 +1,305 @@
/**
* Test API Client for Daily Notification Plugin
*
* Demonstrates how to integrate with the test API server
* for validating plugin functionality.
*
* @author Matthew Raymer
* @version 1.0.0
*/
export interface TestAPIConfig {
baseUrl: string;
timeout: number;
}
export interface NotificationContent {
id: string;
slotId: string;
title: string;
body: string;
timestamp: number;
priority: string;
category: string;
actions: Array<{ id: string; title: string }>;
metadata: {
source: string;
version: string;
generated: string;
};
}
export interface APIResponse<T> {
data?: T;
error?: string;
status: number;
etag?: string;
fromCache: boolean;
}
export class TestAPIClient {
private config: TestAPIConfig;
private etagCache = new Map<string, string>();
constructor(config: TestAPIConfig) {
this.config = config;
}
/**
* Fetch notification content for a specific slot
* @param slotId - Slot identifier (e.g., 'slot-08:00')
* @returns Promise<APIResponse<NotificationContent>>
*/
async fetchContent(slotId: string): Promise<APIResponse<NotificationContent>> {
const url = `${this.config.baseUrl}/api/content/${slotId}`;
const headers: Record<string, string> = {};
// Add ETag for conditional request if we have cached content
const cachedETag = this.etagCache.get(slotId);
if (cachedETag) {
headers['If-None-Match'] = cachedETag;
}
try {
const response = await fetch(url, {
method: 'GET',
headers,
signal: AbortSignal.timeout(this.config.timeout)
});
const etag = response.headers.get('ETag');
const fromCache = response.status === 304;
if (fromCache) {
return {
status: response.status,
fromCache: true,
etag: cachedETag
};
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// Cache ETag for future conditional requests
if (etag) {
this.etagCache.set(slotId, etag);
}
return {
data,
status: response.status,
etag,
fromCache: false
};
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 0,
fromCache: false
};
}
}
/**
* Test error scenarios
* @param errorType - Type of error to simulate
* @returns Promise<APIResponse<any>>
*/
async testError(errorType: string): Promise<APIResponse<any>> {
const url = `${this.config.baseUrl}/api/error/${errorType}`;
try {
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(this.config.timeout)
});
const data = await response.json();
return {
data,
status: response.status,
fromCache: false
};
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 0,
fromCache: false
};
}
}
/**
* Get API health status
* @returns Promise<APIResponse<any>>
*/
async getHealth(): Promise<APIResponse<any>> {
const url = `${this.config.baseUrl}/health`;
try {
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(this.config.timeout)
});
const data = await response.json();
return {
data,
status: response.status,
fromCache: false
};
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 0,
fromCache: false
};
}
}
/**
* Get API metrics
* @returns Promise<APIResponse<any>>
*/
async getMetrics(): Promise<APIResponse<any>> {
const url = `${this.config.baseUrl}/api/metrics`;
try {
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(this.config.timeout)
});
const data = await response.json();
return {
data,
status: response.status,
fromCache: false
};
} catch (error) {
return {
error: error instanceof Error ? error.message : 'Unknown error',
status: 0,
fromCache: false
};
}
}
/**
* Clear ETag cache
*/
clearCache(): void {
this.etagCache.clear();
}
/**
* Get cached ETags
* @returns Map of slotId to ETag
*/
getCachedETags(): Map<string, string> {
return new Map(this.etagCache);
}
}
/**
* Platform-specific API configuration
*/
export const getAPIConfig = (): TestAPIConfig => {
// Detect platform and set appropriate base URL
if (typeof window !== 'undefined') {
// Web/Electron
return {
baseUrl: 'http://localhost:3001',
timeout: 12000 // 12 seconds
};
}
// Default configuration
return {
baseUrl: 'http://localhost:3001',
timeout: 12000
};
};
/**
* Usage examples for test apps
*/
export const TestAPIExamples = {
/**
* Basic content fetching example
*/
async basicFetch() {
const client = new TestAPIClient(getAPIConfig());
console.log('Testing basic content fetch...');
const result = await client.fetchContent('slot-08:00');
if (result.error) {
console.error('Error:', result.error);
} else {
console.log('Success:', result.data);
console.log('ETag:', result.etag);
console.log('From cache:', result.fromCache);
}
},
/**
* ETag caching example
*/
async etagCaching() {
const client = new TestAPIClient(getAPIConfig());
console.log('Testing ETag caching...');
// First request
const result1 = await client.fetchContent('slot-08:00');
console.log('First request:', result1.fromCache ? 'From cache' : 'Fresh content');
// Second request (should be from cache)
const result2 = await client.fetchContent('slot-08:00');
console.log('Second request:', result2.fromCache ? 'From cache' : 'Fresh content');
},
/**
* Error handling example
*/
async errorHandling() {
const client = new TestAPIClient(getAPIConfig());
console.log('Testing error handling...');
const errorTypes = ['timeout', 'server-error', 'not-found', 'rate-limit'];
for (const errorType of errorTypes) {
const result = await client.testError(errorType);
console.log(`${errorType}:`, result.status, result.error || 'Success');
}
},
/**
* Health check example
*/
async healthCheck() {
const client = new TestAPIClient(getAPIConfig());
console.log('Testing health check...');
const result = await client.getHealth();
if (result.error) {
console.error('Health check failed:', result.error);
} else {
console.log('API is healthy:', result.data);
}
}
};

4799
test-apps/test-api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
{
"name": "daily-notification-test-api",
"version": "1.0.0",
"description": "Test API server for Daily Notification Plugin validation",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest",
"demo": "node test-demo.js"
},
"keywords": [
"test",
"api",
"notification",
"capacitor",
"plugin"
],
"author": "Matthew Raymer",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1",
"jest": "^29.7.0",
"node-fetch": "^2.7.0"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@@ -0,0 +1,321 @@
#!/usr/bin/env node
/**
* Test API Server for Daily Notification Plugin
*
* Provides mock content endpoints for testing the plugin's
* network fetching, ETag support, and error handling capabilities.
*
* @author Matthew Raymer
* @version 1.0.0
*/
const express = require('express');
const cors = require('cors');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json());
// In-memory storage for testing
let contentStore = new Map();
let etagStore = new Map();
/**
* Generate mock notification content for a given slot
* @param {string} slotId - The notification slot identifier
* @param {number} timestamp - Current timestamp
* @returns {Object} Mock notification content
*/
function generateMockContent(slotId, timestamp) {
const slotTime = slotId.split('-')[1] || '08:00';
const contentId = crypto.randomUUID().substring(0, 8);
return {
id: contentId,
slotId: slotId,
title: `Daily Update - ${slotTime}`,
body: `Your personalized content for ${slotTime}. Content ID: ${contentId}`,
timestamp: timestamp,
priority: 'high',
category: 'daily',
actions: [
{ id: 'view', title: 'View Details' },
{ id: 'dismiss', title: 'Dismiss' }
],
metadata: {
source: 'test-api',
version: '1.0.0',
generated: new Date(timestamp).toISOString()
}
};
}
/**
* Generate ETag for content
* @param {Object} content - Content object
* @returns {string} ETag value
*/
function generateETag(content) {
const contentString = JSON.stringify(content);
return `"${crypto.createHash('md5').update(contentString).digest('hex')}"`;
}
/**
* Store content with ETag
* @param {string} slotId - Slot identifier
* @param {Object} content - Content object
* @param {string} etag - ETag value
*/
function storeContent(slotId, content, etag) {
contentStore.set(slotId, content);
etagStore.set(slotId, etag);
}
/**
* Get stored content and ETag
* @param {string} slotId - Slot identifier
* @returns {Object} { content, etag } or null
*/
function getStoredContent(slotId) {
const content = contentStore.get(slotId);
const etag = etagStore.get(slotId);
return content && etag ? { content, etag } : null;
}
// Routes
/**
* Health check endpoint
*/
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: Date.now(),
version: '1.0.0',
endpoints: {
content: '/api/content/:slotId',
health: '/health',
metrics: '/api/metrics',
error: '/api/error/:type'
}
});
});
/**
* Get notification content for a specific slot
* Supports ETag conditional requests
*/
app.get('/api/content/:slotId', (req, res) => {
const { slotId } = req.params;
const ifNoneMatch = req.headers['if-none-match'];
const timestamp = Date.now();
console.log(`[${new Date().toISOString()}] GET /api/content/${slotId}`);
console.log(` If-None-Match: ${ifNoneMatch || 'none'}`);
// Validate slotId format
if (!slotId || !slotId.match(/^slot-\d{2}:\d{2}$/)) {
return res.status(400).json({
error: 'Invalid slotId format. Expected: slot-HH:MM',
provided: slotId
});
}
// Check if we have stored content
const stored = getStoredContent(slotId);
if (stored && ifNoneMatch === stored.etag) {
// Content hasn't changed, return 304 Not Modified
console.log(` → 304 Not Modified (ETag match)`);
return res.status(304).end();
}
// Generate new content
const content = generateMockContent(slotId, timestamp);
const etag = generateETag(content);
// Store for future ETag checks
storeContent(slotId, content, etag);
// Set ETag header
res.set('ETag', etag);
res.set('Cache-Control', 'no-cache');
res.set('Last-Modified', new Date(timestamp).toUTCString());
console.log(` → 200 OK (new content, ETag: ${etag})`);
res.json(content);
});
/**
* Simulate network errors for testing error handling
*/
app.get('/api/error/:type', (req, res) => {
const { type } = req.params;
console.log(`[${new Date().toISOString()}] GET /api/error/${type}`);
switch (type) {
case 'timeout':
// Simulate timeout by not responding
setTimeout(() => {
res.status(408).json({ error: 'Request timeout' });
}, 15000); // 15 second timeout
break;
case 'server-error':
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR',
timestamp: Date.now()
});
break;
case 'not-found':
res.status(404).json({
error: 'Content not found',
code: 'NOT_FOUND',
slotId: req.query.slotId || 'unknown'
});
break;
case 'rate-limit':
res.status(429).json({
error: 'Rate limit exceeded',
code: 'RATE_LIMIT',
retryAfter: 60
});
break;
case 'unauthorized':
res.status(401).json({
error: 'Unauthorized',
code: 'UNAUTHORIZED'
});
break;
default:
res.status(400).json({
error: 'Unknown error type',
available: ['timeout', 'server-error', 'not-found', 'rate-limit', 'unauthorized']
});
}
});
/**
* API metrics endpoint
*/
app.get('/api/metrics', (req, res) => {
const metrics = {
timestamp: Date.now(),
contentStore: {
size: contentStore.size,
slots: Array.from(contentStore.keys())
},
etagStore: {
size: etagStore.size,
etags: Array.from(etagStore.entries())
},
uptime: process.uptime(),
memory: process.memoryUsage()
};
res.json(metrics);
});
/**
* Clear stored content (for testing)
*/
app.delete('/api/content', (req, res) => {
contentStore.clear();
etagStore.clear();
res.json({
message: 'All stored content cleared',
timestamp: Date.now()
});
});
/**
* Update content for a specific slot (for testing content changes)
*/
app.put('/api/content/:slotId', (req, res) => {
const { slotId } = req.params;
const { content } = req.body;
if (!content) {
return res.status(400).json({
error: 'Content is required'
});
}
const timestamp = Date.now();
const etag = generateETag(content);
storeContent(slotId, content, etag);
res.set('ETag', etag);
res.json({
message: 'Content updated',
slotId,
etag,
timestamp
});
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(`[${new Date().toISOString()}] Error:`, err);
res.status(500).json({
error: 'Internal server error',
message: err.message,
timestamp: Date.now()
});
});
// 404 handler
app.use((req, res) => {
res.status(404).json({
error: 'Endpoint not found',
path: req.path,
method: req.method,
timestamp: Date.now()
});
});
// Start server
app.listen(PORT, () => {
console.log(`🚀 Test API Server running on port ${PORT}`);
console.log(`📋 Available endpoints:`);
console.log(` GET /health - Health check`);
console.log(` GET /api/content/:slotId - Get notification content`);
console.log(` PUT /api/content/:slotId - Update content`);
console.log(` DELETE /api/content - Clear all content`);
console.log(` GET /api/error/:type - Simulate errors`);
console.log(` GET /api/metrics - API metrics`);
console.log(``);
console.log(`🔧 Environment:`);
console.log(` NODE_ENV: ${process.env.NODE_ENV || 'development'}`);
console.log(` PORT: ${PORT}`);
console.log(``);
console.log(`📝 Usage examples:`);
console.log(` curl http://localhost:${PORT}/health`);
console.log(` curl http://localhost:${PORT}/api/content/slot-08:00`);
console.log(` curl -H "If-None-Match: \\"abc123\\"" http://localhost:${PORT}/api/content/slot-08:00`);
console.log(` curl http://localhost:${PORT}/api/error/timeout`);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down Test API Server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n🛑 Shutting down Test API Server...');
process.exit(0);
});

View File

@@ -0,0 +1,294 @@
#!/usr/bin/env node
/**
* Test API Demo Script
*
* Demonstrates the Test API Server functionality
* and validates all endpoints work correctly.
*
* @author Matthew Raymer
* @version 1.0.0
*/
const fetch = require('node-fetch');
const API_BASE_URL = 'http://localhost:3001';
/**
* Make HTTP request with timeout
* @param {string} url - Request URL
* @param {Object} options - Fetch options
* @returns {Promise<Object>} Response data
*/
async function makeRequest(url, options = {}) {
try {
const response = await fetch(url, {
timeout: 10000,
...options
});
const data = await response.json();
return {
status: response.status,
data,
headers: Object.fromEntries(response.headers.entries())
};
} catch (error) {
return {
status: 0,
error: error.message
};
}
}
/**
* Test health endpoint
*/
async function testHealth() {
console.log('🔍 Testing health endpoint...');
const result = await makeRequest(`${API_BASE_URL}/health`);
if (result.error) {
console.error('❌ Health check failed:', result.error);
return false;
}
console.log('✅ Health check passed');
console.log(' Status:', result.status);
console.log(' Version:', result.data.version);
console.log(' Endpoints:', Object.keys(result.data.endpoints).length);
return true;
}
/**
* Test content fetching
*/
async function testContentFetching() {
console.log('\n📱 Testing content fetching...');
const slotId = 'slot-08:00';
const result = await makeRequest(`${API_BASE_URL}/api/content/${slotId}`);
if (result.error) {
console.error('❌ Content fetch failed:', result.error);
return false;
}
console.log('✅ Content fetch passed');
console.log(' Status:', result.status);
console.log(' Slot ID:', result.data.slotId);
console.log(' Title:', result.data.title);
console.log(' ETag:', result.headers.etag);
return result.headers.etag;
}
/**
* Test ETag caching
*/
async function testETagCaching(etag) {
console.log('\n🔄 Testing ETag caching...');
const slotId = 'slot-08:00';
const result = await makeRequest(`${API_BASE_URL}/api/content/${slotId}`, {
headers: {
'If-None-Match': etag
}
});
if (result.error) {
console.error('❌ ETag test failed:', result.error);
return false;
}
if (result.status === 304) {
console.log('✅ ETag caching works (304 Not Modified)');
return true;
} else {
console.log('⚠️ ETag caching unexpected response:', result.status);
return false;
}
}
/**
* Test error scenarios
*/
async function testErrorScenarios() {
console.log('\n🚨 Testing error scenarios...');
const errorTypes = ['server-error', 'not-found', 'rate-limit', 'unauthorized'];
let passed = 0;
for (const errorType of errorTypes) {
const result = await makeRequest(`${API_BASE_URL}/api/error/${errorType}`);
if (result.error) {
console.log(`${errorType}: ${result.error}`);
} else {
console.log(`${errorType}: ${result.status}`);
passed++;
}
}
console.log(` Passed: ${passed}/${errorTypes.length}`);
return passed === errorTypes.length;
}
/**
* Test metrics endpoint
*/
async function testMetrics() {
console.log('\n📊 Testing metrics endpoint...');
const result = await makeRequest(`${API_BASE_URL}/api/metrics`);
if (result.error) {
console.error('❌ Metrics test failed:', result.error);
return false;
}
console.log('✅ Metrics endpoint works');
console.log(' Content store size:', result.data.contentStore.size);
console.log(' ETag store size:', result.data.etagStore.size);
console.log(' Uptime:', Math.round(result.data.uptime), 'seconds');
return true;
}
/**
* Test content update
*/
async function testContentUpdate() {
console.log('\n✏ Testing content update...');
const slotId = 'slot-08:00';
const newContent = {
content: {
title: 'Updated Test Title',
body: 'This is updated test content',
timestamp: Date.now()
}
};
const result = await makeRequest(`${API_BASE_URL}/api/content/${slotId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newContent)
});
if (result.error) {
console.error('❌ Content update failed:', result.error);
return false;
}
console.log('✅ Content update works');
console.log(' Status:', result.status);
console.log(' New ETag:', result.data.etag);
return true;
}
/**
* Test content clearing
*/
async function testContentClearing() {
console.log('\n🗑 Testing content clearing...');
const result = await makeRequest(`${API_BASE_URL}/api/content`, {
method: 'DELETE'
});
if (result.error) {
console.error('❌ Content clearing failed:', result.error);
return false;
}
console.log('✅ Content clearing works');
console.log(' Status:', result.status);
return true;
}
/**
* Main test runner
*/
async function runTests() {
console.log('🚀 Starting Test API validation...\n');
const tests = [
{ name: 'Health Check', fn: testHealth },
{ name: 'Content Fetching', fn: testContentFetching },
{ name: 'ETag Caching', fn: testETagCaching },
{ name: 'Error Scenarios', fn: testErrorScenarios },
{ name: 'Metrics', fn: testMetrics },
{ name: 'Content Update', fn: testContentUpdate },
{ name: 'Content Clearing', fn: testContentClearing }
];
let passed = 0;
let etag = null;
for (const test of tests) {
try {
if (test.name === 'ETag Caching' && etag) {
const result = await test.fn(etag);
if (result) passed++;
} else {
const result = await test.fn();
if (result) {
passed++;
if (test.name === 'Content Fetching' && typeof result === 'string') {
etag = result;
}
}
}
} catch (error) {
console.error(`${test.name} failed with error:`, error.message);
}
}
console.log(`\n📋 Test Results: ${passed}/${tests.length} passed`);
if (passed === tests.length) {
console.log('🎉 All tests passed! Test API is working correctly.');
} else {
console.log('⚠️ Some tests failed. Check the output above for details.');
}
console.log('\n💡 Next steps:');
console.log(' 1. Start your test app');
console.log(' 2. Configure it to use this API');
console.log(' 3. Test plugin functionality');
console.log(' 4. Monitor API metrics at /api/metrics');
}
// Check if API server is running
async function checkServer() {
try {
const result = await makeRequest(`${API_BASE_URL}/health`);
if (result.error) {
console.error('❌ Cannot connect to Test API Server');
console.error(' Make sure the server is running: npm start');
console.error(' Server should be available at:', API_BASE_URL);
process.exit(1);
}
} catch (error) {
console.error('❌ Cannot connect to Test API Server');
console.error(' Make sure the server is running: npm start');
console.error(' Server should be available at:', API_BASE_URL);
process.exit(1);
}
}
// Run tests
checkServer().then(() => {
runTests().catch(error => {
console.error('❌ Test runner failed:', error.message);
process.exit(1);
});
});