Browse Source
- 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)research/notification-plugin-enhancement
16 changed files with 12331 additions and 48 deletions
@ -0,0 +1,307 @@ |
|||
# Enhanced Test Apps Setup Guide |
|||
|
|||
## Overview |
|||
|
|||
This guide creates minimal Capacitor test apps for validating the Daily Notification Plugin across all target platforms with robust error handling and clear troubleshooting. |
|||
|
|||
## Prerequisites |
|||
|
|||
### Required Software |
|||
- **Node.js 18+**: Download from [nodejs.org](https://nodejs.org/) |
|||
- **npm**: Comes with Node.js |
|||
- **Git**: For version control |
|||
|
|||
### Platform-Specific Requirements |
|||
|
|||
#### Android |
|||
- **Android Studio**: Download from [developer.android.com/studio](https://developer.android.com/studio) |
|||
- **Android SDK**: Installed via Android Studio |
|||
- **Java Development Kit (JDK)**: Version 11 or higher |
|||
|
|||
#### iOS (macOS only) |
|||
- **Xcode**: Install from Mac App Store |
|||
- **Xcode Command Line Tools**: `xcode-select --install` |
|||
- **iOS Simulator**: Included with Xcode |
|||
|
|||
#### Electron |
|||
- **No additional requirements**: Works on any platform with Node.js |
|||
|
|||
## Quick Start |
|||
|
|||
### Option 1: Automated Setup (Recommended) |
|||
|
|||
```bash |
|||
# Navigate to test-apps directory |
|||
cd test-apps |
|||
|
|||
# Setup all platforms (run from test-apps directory) |
|||
./setup-android.sh |
|||
./setup-ios.sh |
|||
./setup-electron.sh |
|||
``` |
|||
|
|||
### Option 2: Manual Setup |
|||
|
|||
#### Android Manual Setup |
|||
```bash |
|||
cd test-apps/android-test |
|||
|
|||
# Install dependencies |
|||
npm install |
|||
|
|||
# Install Capacitor CLI globally |
|||
npm install -g @capacitor/cli |
|||
|
|||
# Initialize Capacitor |
|||
npx cap init "Daily Notification Android Test" "com.timesafari.dailynotification.androidtest" |
|||
|
|||
# Add Android platform |
|||
npx cap add android |
|||
|
|||
# Build web assets |
|||
npm run build |
|||
|
|||
# Sync to native |
|||
npx cap sync android |
|||
``` |
|||
|
|||
#### iOS Manual Setup |
|||
```bash |
|||
cd test-apps/ios-test |
|||
|
|||
# Install dependencies |
|||
npm install |
|||
|
|||
# Install Capacitor CLI globally |
|||
npm install -g @capacitor/cli |
|||
|
|||
# Initialize Capacitor |
|||
npx cap init "Daily Notification iOS Test" "com.timesafari.dailynotification.iostest" |
|||
|
|||
# Add iOS platform |
|||
npx cap add ios |
|||
|
|||
# Build web assets |
|||
npm run build |
|||
|
|||
# Sync to native |
|||
npx cap sync ios |
|||
``` |
|||
|
|||
#### Electron Manual Setup |
|||
```bash |
|||
cd test-apps/electron-test |
|||
|
|||
# Install dependencies |
|||
npm install |
|||
|
|||
# Build web assets |
|||
npm run build-web |
|||
``` |
|||
|
|||
## Common Issues and Solutions |
|||
|
|||
### Issue: "Unknown command: cap" |
|||
**Solution**: Install Capacitor CLI globally |
|||
```bash |
|||
npm install -g @capacitor/cli |
|||
``` |
|||
|
|||
### Issue: "android platform has not been added yet" |
|||
**Solution**: Add the Android platform first |
|||
```bash |
|||
npx cap add android |
|||
``` |
|||
|
|||
### Issue: "Failed to add Android platform" |
|||
**Solutions**: |
|||
1. Install Android Studio and Android SDK |
|||
2. Set `ANDROID_HOME` environment variable |
|||
3. Add Android SDK tools to your PATH |
|||
|
|||
### Issue: "Failed to add iOS platform" |
|||
**Solutions**: |
|||
1. Install Xcode from Mac App Store |
|||
2. Install Xcode Command Line Tools: `xcode-select --install` |
|||
3. Ensure you're running on macOS |
|||
|
|||
### Issue: Build failures |
|||
**Solutions**: |
|||
1. Check Node.js version: `node --version` (should be 18+) |
|||
2. Clear npm cache: `npm cache clean --force` |
|||
3. Delete `node_modules` and reinstall: `rm -rf node_modules && npm install` |
|||
|
|||
## Platform-Specific Setup |
|||
|
|||
### Android Setup Verification |
|||
|
|||
```bash |
|||
# Check Android Studio installation |
|||
which studio |
|||
|
|||
# Check Android SDK |
|||
echo $ANDROID_HOME |
|||
|
|||
# Check Java |
|||
java -version |
|||
``` |
|||
|
|||
**Required Environment Variables**: |
|||
```bash |
|||
export ANDROID_HOME=/path/to/android/sdk |
|||
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools |
|||
``` |
|||
|
|||
### iOS Setup Verification |
|||
|
|||
```bash |
|||
# Check Xcode installation |
|||
xcodebuild -version |
|||
|
|||
# Check iOS Simulator |
|||
xcrun simctl list devices |
|||
|
|||
# Check Command Line Tools |
|||
xcode-select -p |
|||
``` |
|||
|
|||
### Electron Setup Verification |
|||
|
|||
```bash |
|||
# Check Node.js version |
|||
node --version |
|||
|
|||
# Check npm |
|||
npm --version |
|||
|
|||
# Test Electron installation |
|||
npx electron --version |
|||
``` |
|||
|
|||
## Running the Test Apps |
|||
|
|||
### Android |
|||
```bash |
|||
cd test-apps/android-test |
|||
|
|||
# Web development server (for testing) |
|||
npm run dev |
|||
|
|||
# Open in Android Studio |
|||
npx cap open android |
|||
|
|||
# Run on device/emulator |
|||
npx cap run android |
|||
``` |
|||
|
|||
### iOS |
|||
```bash |
|||
cd test-apps/ios-test |
|||
|
|||
# Web development server (for testing) |
|||
npm run dev |
|||
|
|||
# Open in Xcode |
|||
npx cap open ios |
|||
|
|||
# Run on device/simulator |
|||
npx cap run ios |
|||
``` |
|||
|
|||
### Electron |
|||
```bash |
|||
cd test-apps/electron-test |
|||
|
|||
# Run Electron app |
|||
npm start |
|||
|
|||
# Run in development mode |
|||
npm run dev |
|||
|
|||
# Build and run |
|||
npm run electron |
|||
``` |
|||
|
|||
## Testing Workflow |
|||
|
|||
### 1. Web Testing (Recommended First) |
|||
```bash |
|||
# Test each platform's web version first |
|||
cd test-apps/android-test && npm run dev |
|||
cd test-apps/ios-test && npm run dev |
|||
cd test-apps/electron-test && npm start |
|||
``` |
|||
|
|||
### 2. Native Testing |
|||
```bash |
|||
# After web testing succeeds, test native platforms |
|||
cd test-apps/android-test && npx cap run android |
|||
cd test-apps/ios-test && npx cap run ios |
|||
``` |
|||
|
|||
### 3. Integration Testing |
|||
- Test plugin configuration |
|||
- Test notification scheduling |
|||
- Test platform-specific features |
|||
- Test error handling |
|||
- Test performance metrics |
|||
|
|||
## Troubleshooting Checklist |
|||
|
|||
### General Issues |
|||
- [ ] Node.js 18+ installed |
|||
- [ ] npm working correctly |
|||
- [ ] Capacitor CLI installed globally |
|||
- [ ] Dependencies installed (`npm install`) |
|||
|
|||
### Android Issues |
|||
- [ ] Android Studio installed |
|||
- [ ] Android SDK configured |
|||
- [ ] `ANDROID_HOME` environment variable set |
|||
- [ ] Java JDK 11+ installed |
|||
- [ ] Android platform added (`npx cap add android`) |
|||
|
|||
### iOS Issues |
|||
- [ ] Xcode installed (macOS only) |
|||
- [ ] Xcode Command Line Tools installed |
|||
- [ ] iOS Simulator available |
|||
- [ ] iOS platform added (`npx cap add ios`) |
|||
|
|||
### Electron Issues |
|||
- [ ] Node.js working correctly |
|||
- [ ] Dependencies installed |
|||
- [ ] Web assets built (`npm run build-web`) |
|||
|
|||
## Development Tips |
|||
|
|||
### Web Development |
|||
- Use `npm run dev` for hot reloading |
|||
- Test plugin APIs in browser console |
|||
- Use browser dev tools for debugging |
|||
|
|||
### Native Development |
|||
- Use `npx cap sync` after making changes |
|||
- Check native logs for detailed errors |
|||
- Test on both physical devices and simulators |
|||
|
|||
### Debugging |
|||
- Check console logs for errors |
|||
- Use `npx cap doctor` to diagnose issues |
|||
- Verify platform-specific requirements |
|||
|
|||
## Next Steps |
|||
|
|||
1. **Run Setup Scripts**: Execute platform-specific setup |
|||
2. **Test Web Versions**: Validate basic functionality |
|||
3. **Test Native Platforms**: Verify platform-specific features |
|||
4. **Integration Testing**: Test with actual plugin implementation |
|||
5. **Performance Validation**: Monitor metrics and optimizations |
|||
|
|||
## Support |
|||
|
|||
If you encounter issues not covered in this guide: |
|||
|
|||
1. Check the [Capacitor Documentation](https://capacitorjs.com/docs) |
|||
2. Verify platform-specific requirements |
|||
3. Check console logs for detailed error messages |
|||
4. Ensure all prerequisites are properly installed |
@ -0,0 +1,160 @@ |
|||
# Android Test App Gradle Sync Troubleshooting |
|||
|
|||
## Problem: Gradle Sync Failure |
|||
|
|||
**Error Message:** |
|||
``` |
|||
Unable to find method 'org.gradle.api.artifacts.Dependency org.gradle.api.artifacts.dsl.DependencyHandler.module(java.lang.Object)' |
|||
``` |
|||
|
|||
## Root Cause |
|||
|
|||
The Android test app was using **Gradle 9.0-milestone-1** (pre-release) with **Android Gradle Plugin 8.0.0**, causing version incompatibility issues. |
|||
|
|||
## Solution Applied |
|||
|
|||
### 1. Updated Gradle Version |
|||
**File:** `android/gradle/wrapper/gradle-wrapper.properties` |
|||
```properties |
|||
# Changed from: |
|||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip |
|||
|
|||
# To: |
|||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip |
|||
``` |
|||
|
|||
### 2. Updated Android Gradle Plugin |
|||
**File:** `android/build.gradle` |
|||
```gradle |
|||
// Changed from: |
|||
classpath 'com.android.tools.build:gradle:8.0.0' |
|||
|
|||
// To: |
|||
classpath 'com.android.tools.build:gradle:8.13.0' |
|||
``` |
|||
|
|||
### 3. Updated Google Services Plugin |
|||
```gradle |
|||
// Changed from: |
|||
classpath 'com.google.gms:google-services:4.3.15' |
|||
|
|||
// To: |
|||
classpath 'com.google.gms:google-services:4.4.0' |
|||
``` |
|||
|
|||
### 4. Updated AndroidX Dependencies |
|||
**File:** `android/variables.gradle` |
|||
```gradle |
|||
// Updated to latest stable versions: |
|||
androidxAppCompatVersion = '1.7.1' // was 1.6.1 |
|||
androidxActivityVersion = '1.8.2' // was 1.7.0 |
|||
androidxCoreVersion = '1.12.0' // was 1.10.0 |
|||
androidxFragmentVersion = '1.6.2' // was 1.5.6 |
|||
coreSplashScreenVersion = '1.0.1' // was 1.0.0 |
|||
androidxWebkitVersion = '1.8.0' // was 1.6.1 |
|||
compileSdkVersion = 34 // was 33 |
|||
targetSdkVersion = 34 // was 33 |
|||
``` |
|||
|
|||
## Manual Fix Steps |
|||
|
|||
If you encounter this issue again: |
|||
|
|||
### Step 1: Clean Gradle Cache |
|||
```bash |
|||
cd test-apps/android-test/android |
|||
./gradlew clean |
|||
./gradlew --stop |
|||
``` |
|||
|
|||
### Step 2: Clear Gradle Wrapper Cache |
|||
```bash |
|||
rm -rf ~/.gradle/wrapper/dists/gradle-9.0-milestone-1* |
|||
``` |
|||
|
|||
### Step 3: Re-sync Project |
|||
In Android Studio: |
|||
1. Click **File** → **Sync Project with Gradle Files** |
|||
2. Or click the **Sync Now** link in the error banner |
|||
|
|||
### Step 4: If Still Failing |
|||
```bash |
|||
# Delete all Gradle caches |
|||
rm -rf ~/.gradle/caches |
|||
rm -rf ~/.gradle/wrapper |
|||
|
|||
# Re-download Gradle |
|||
cd test-apps/android-test/android |
|||
./gradlew wrapper --gradle-version 8.4 |
|||
``` |
|||
|
|||
## Prevention |
|||
|
|||
### Use Stable Versions |
|||
Always use stable, tested version combinations: |
|||
|
|||
| Android Gradle Plugin | Gradle Version | Status | |
|||
|----------------------|----------------|---------| |
|||
| 8.13.0 | 8.13 | ✅ Latest Stable | |
|||
| 8.1.4 | 8.4 | ✅ Stable | |
|||
| 8.0.0 | 8.0 | ✅ Stable | |
|||
| 7.4.2 | 7.5 | ✅ Stable | |
|||
| 8.0.0 | 9.0-milestone-1 | ❌ Incompatible | |
|||
|
|||
### Version Compatibility Check |
|||
- **Android Gradle Plugin 8.13.0** requires **Gradle 8.0+** |
|||
- **Gradle 8.13** is the latest stable version |
|||
- **AndroidX AppCompat 1.7.1** is the latest stable version |
|||
- Avoid pre-release versions in production |
|||
|
|||
## Additional Troubleshooting |
|||
|
|||
### If Sync Still Fails |
|||
|
|||
1. **Check Java Version** |
|||
```bash |
|||
java -version |
|||
# Should be Java 17+ for AGP 8.1.4 |
|||
``` |
|||
|
|||
2. **Check Android SDK** |
|||
```bash |
|||
echo $ANDROID_HOME |
|||
# Should point to Android SDK location |
|||
``` |
|||
|
|||
3. **Check Local Properties** |
|||
```bash |
|||
# Verify android/local.properties exists |
|||
cat test-apps/android-test/android/local.properties |
|||
``` |
|||
|
|||
4. **Recreate Project** |
|||
```bash |
|||
cd test-apps/android-test |
|||
rm -rf android/ |
|||
npx cap add android |
|||
``` |
|||
|
|||
## Success Indicators |
|||
|
|||
After applying the fix, you should see: |
|||
- ✅ **Gradle sync successful** |
|||
- ✅ **No red error banners** |
|||
- ✅ **Build.gradle file opens without errors** |
|||
- ✅ **Project structure loads correctly** |
|||
|
|||
## Next Steps |
|||
|
|||
Once Gradle sync is successful: |
|||
1. **Build the project**: `./gradlew build` |
|||
2. **Run on device**: `npx cap run android` |
|||
3. **Test plugin functionality**: Use the test API server |
|||
4. **Validate notifications**: Test the Daily Notification Plugin |
|||
|
|||
## Related Issues |
|||
|
|||
- **Build failures**: Usually resolved by Gradle sync fix |
|||
- **Plugin not found**: Check Capacitor plugin installation |
|||
- **Permission errors**: Verify Android manifest permissions |
|||
- **Runtime crashes**: Check plugin initialization code |
File diff suppressed because it is too large
@ -0,0 +1,112 @@ |
|||
#!/bin/bash |
|||
|
|||
# Environment Verification Script for Test Apps |
|||
echo "🔍 Verifying Test Apps Environment..." |
|||
echo "" |
|||
|
|||
# Check Node.js |
|||
echo "📦 Node.js:" |
|||
if command -v node &> /dev/null; then |
|||
node_version=$(node --version) |
|||
echo " ✅ Installed: $node_version" |
|||
|
|||
# Check if version is 18+ |
|||
major_version=$(echo $node_version | cut -d'.' -f1 | sed 's/v//') |
|||
if [ "$major_version" -ge 18 ]; then |
|||
echo " ✅ Version 18+ (compatible)" |
|||
else |
|||
echo " ⚠️ Version $major_version (requires 18+)" |
|||
fi |
|||
else |
|||
echo " ❌ Not installed" |
|||
fi |
|||
|
|||
# Check npm |
|||
echo "" |
|||
echo "📦 npm:" |
|||
if command -v npm &> /dev/null; then |
|||
npm_version=$(npm --version) |
|||
echo " ✅ Installed: $npm_version" |
|||
else |
|||
echo " ❌ Not installed" |
|||
fi |
|||
|
|||
# Check Capacitor CLI |
|||
echo "" |
|||
echo "⚡ Capacitor CLI:" |
|||
if command -v cap &> /dev/null; then |
|||
cap_version=$(cap --version) |
|||
echo " ✅ Installed: $cap_version" |
|||
else |
|||
echo " ❌ Not installed (will be installed by setup scripts)" |
|||
fi |
|||
|
|||
# Check Android (if available) |
|||
echo "" |
|||
echo "📱 Android:" |
|||
if command -v studio &> /dev/null; then |
|||
echo " ✅ Android Studio installed" |
|||
else |
|||
echo " ❌ Android Studio not found" |
|||
fi |
|||
|
|||
if [ ! -z "$ANDROID_HOME" ]; then |
|||
echo " ✅ ANDROID_HOME set: $ANDROID_HOME" |
|||
else |
|||
echo " ❌ ANDROID_HOME not set" |
|||
fi |
|||
|
|||
if command -v java &> /dev/null; then |
|||
java_version=$(java -version 2>&1 | head -n 1) |
|||
echo " ✅ Java: $java_version" |
|||
else |
|||
echo " ❌ Java not found" |
|||
fi |
|||
|
|||
# Check iOS (if on macOS) |
|||
echo "" |
|||
echo "🍎 iOS:" |
|||
if [[ "$OSTYPE" == "darwin"* ]]; then |
|||
if command -v xcodebuild &> /dev/null; then |
|||
xcode_version=$(xcodebuild -version | head -n 1) |
|||
echo " ✅ Xcode: $xcode_version" |
|||
else |
|||
echo " ❌ Xcode not installed" |
|||
fi |
|||
|
|||
if command -v xcrun &> /dev/null; then |
|||
echo " ✅ Xcode Command Line Tools available" |
|||
else |
|||
echo " ❌ Xcode Command Line Tools not installed" |
|||
fi |
|||
else |
|||
echo " ⚠️ iOS development requires macOS" |
|||
fi |
|||
|
|||
# Check Electron |
|||
echo "" |
|||
echo "⚡ Electron:" |
|||
if command -v npx &> /dev/null; then |
|||
electron_version=$(npx electron --version 2>/dev/null) |
|||
if [ $? -eq 0 ]; then |
|||
echo " ✅ Electron available: $electron_version" |
|||
else |
|||
echo " ⚠️ Electron not installed (will be installed by setup)" |
|||
fi |
|||
else |
|||
echo " ❌ npx not available" |
|||
fi |
|||
|
|||
echo "" |
|||
echo "📋 Summary:" |
|||
echo " - Node.js 18+: $(command -v node &> /dev/null && node --version | cut -d'.' -f1 | sed 's/v//' | awk '{if($1>=18) print "✅"; else print "❌"}' || echo "❌")" |
|||
echo " - npm: $(command -v npm &> /dev/null && echo "✅" || echo "❌")" |
|||
echo " - Android Studio: $(command -v studio &> /dev/null && echo "✅" || echo "❌")" |
|||
echo " - Xcode: $(command -v xcodebuild &> /dev/null && echo "✅" || echo "❌")" |
|||
echo " - Electron: $(command -v npx &> /dev/null && npx electron --version &> /dev/null && echo "✅" || echo "❌")" |
|||
|
|||
echo "" |
|||
echo "🚀 Next Steps:" |
|||
echo " 1. Install missing prerequisites" |
|||
echo " 2. Run setup scripts: ./setup-*.sh" |
|||
echo " 3. See SETUP_GUIDE.md for detailed instructions" |
@ -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 |
@ -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. |
@ -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 |
@ -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); |
|||
} |
|||
} |
|||
}; |
File diff suppressed because it is too large
@ -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" |
|||
} |
|||
} |
@ -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); |
|||
}); |
@ -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); |
|||
}); |
|||
}); |
Loading…
Reference in new issue