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:
152
test-apps/test-api/.gitignore
vendored
Normal file
152
test-apps/test-api/.gitignore
vendored
Normal 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
|
||||
282
test-apps/test-api/README.md
Normal file
282
test-apps/test-api/README.md
Normal 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.
|
||||
76
test-apps/test-api/SETUP.md
Normal file
76
test-apps/test-api/SETUP.md
Normal 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
|
||||
305
test-apps/test-api/client.ts
Normal file
305
test-apps/test-api/client.ts
Normal 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
4799
test-apps/test-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
test-apps/test-api/package.json
Normal file
33
test-apps/test-api/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
321
test-apps/test-api/server.js
Normal file
321
test-apps/test-api/server.js
Normal 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);
|
||||
});
|
||||
294
test-apps/test-api/test-demo.js
Normal file
294
test-apps/test-api/test-demo.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user