Files
cloud-server/src/services/cache.manual-test.md
kappa 3a8dd705e6 refactor: comprehensive code review fixes (security, performance, QA)
## Security Improvements
- Fix timing attack in verifyApiKey with fixed 256-byte buffer
- Fix sortOrder SQL injection with whitelist validation
- Fix rate limiting bypass for non-Cloudflare traffic (fail-closed)
- Remove stack trace exposure in error responses
- Add request_id for audit trail (X-Request-ID header)
- Sanitize origin header to prevent log injection
- Add content-length validation for /sync endpoint (10KB limit)
- Replace Math.random() with crypto.randomUUID() for sync IDs
- Expand sensitive data masking patterns (8 → 18)

## Performance Improvements
- Reduce rate limiter KV reads from 3 to 1 per request (66% reduction)
- Increase sync batch size from 100 to 500 (80% fewer batches)
- Fix health check N+1 query with efficient JOINs
- Fix COUNT(*) Cartesian product with COUNT(DISTINCT)
- Implement shared logger cache pattern across repositories
- Add CacheService singleton pattern in recommend.ts
- Add composite index for recommendation queries
- Implement Anvil pricing query batching (100 per chunk)

## QA Improvements
- Add BATCH_SIZE bounds validation (1-1000)
- Add pagination bounds (page >= 1, MAX_OFFSET = 100000)
- Add min/max range consistency validation
- Add DB reference validation for singleton services
- Add type guards for database result validation
- Add timeout mechanism for external API calls (10-60s)
- Use SUPPORTED_PROVIDERS constant instead of hardcoded list

## Removed
- Remove Vault integration (using Wrangler secrets)
- Remove 6-hour pricing cron (daily sync only)

## Configuration
- Add idx_instance_types_specs_filter composite index
- Add CORS Access-Control-Expose-Headers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 23:50:37 +09:00

9.8 KiB

/**

  • Manual Test Instructions for CacheService
  • Since Cloudflare Workers Cache API is only available in the Workers runtime,
  • these tests must be run in a Cloudflare Workers environment or using Miniflare.
  • Test 1: Basic Cache Operations

    1. Deploy to Cloudflare Workers development environment
    1. Initialize cache: const cache = new CacheService(300);
    1. Set data: await cache.set('test-key', { foo: 'bar' }, 60);
    1. Get data: const result = await cache.get('test-key');
    1. Expected: result.data.foo === 'bar', cache_age_seconds ≈ 0
  • Test 2: Cache Key Generation

    1. Generate key: const key = cache.generateKey({ provider: 'linode', region: 'us-east' });
    1. Expected: key === 'https://cache.internal/instances?provider=linode&region=us-east'
    1. Verify sorting: cache.generateKey({ z: 1, a: 2 }) should have 'a' before 'z'
  • Test 3: Cache Miss

    1. Request non-existent key: const result = await cache.get('non-existent');
    1. Expected: result === null
  • Test 4: Cache Expiration

    1. Set with short TTL: await cache.set('expire-test', { data: 'test' }, 2);
    1. Immediate get: await cache.get('expire-test') → should return data
    1. Wait 3 seconds
    1. Get again: await cache.get('expire-test') → should return null (expired)
  • Test 5: Cache Age Tracking

    1. Set data: await cache.set('age-test', { data: 'test' }, 300);
    1. Wait 5 seconds
    1. Get data: const result = await cache.get('age-test');
    1. Expected: result.cache_age_seconds ≈ 5
  • Test 6: Cache Deletion

    1. Set data: await cache.set('delete-test', { data: 'test' }, 300);
    1. Delete: const deleted = await cache.delete('delete-test');
    1. Expected: deleted === true
    1. Get data: const result = await cache.get('delete-test');
    1. Expected: result === null
  • Test 7: Error Handling (Graceful Degradation)

    1. Test with invalid cache response (manual mock required)
    1. Expected: No errors thrown, graceful null return
    1. Verify logs show error message
  • Test 8: Integration with Instance API

    1. Create cache instance in instance endpoint handler
    1. Generate key from query params: cache.generateKey(query)
    1. Check cache: const cached = await cache.get<InstanceData[]>(key);
    1. If cache hit: return cached.data with cache metadata
    1. If cache miss: fetch from database, cache result, return data
    1. Verify cache hit on second request
  • Performance Validation:

    1. Measure database query time (first request)
    1. Measure cache hit time (second request)
    1. Expected: Cache hit 10-50x faster than database query
    1. Verify cache age increases on subsequent requests
  • TTL Strategy Validation:

  • Filtered queries (5 min TTL):
    • cache.set(key, data, 300)
    • Verify expires after 5 minutes
  • Full dataset (1 hour TTL):
    • cache.set(key, data, 3600)
    • Verify expires after 1 hour
  • Post-sync invalidation:
    • After sync operation, call cache.delete(key) for all relevant keys
    • Verify next request fetches fresh data from database
  • Test 9: clearAll Method

    1. Set multiple cache entries:
    • await cache.set('key1', { data: 'test1' }, 300)
    • await cache.set('key2', { data: 'test2' }, 300)
    • await cache.set('key3', { data: 'test3' }, 300)
    1. Call clearAll: const count = await cache.clearAll()
    1. Expected: count === 0 (enumeration not supported)
    1. Expected: Log message about TTL-based expiration
    1. Note: Individual entries will expire based on TTL
  • Test 10: clearAll with Prefix

    1. Set entries with different prefixes:
    1. Call with prefix: await cache.clearAll('https://cache.internal/instances')
    1. Expected: count === 0, log shows prefix parameter
    1. Note: Prefix is logged but enumeration not supported by Cache API
  • Test 11: clearByEndpoint Method

    1. Set endpoint cache: await cache.set('https://cache.internal/instances', data, 300)
    1. Clear by endpoint: const deleted = await cache.clearByEndpoint('instances')
    1. Expected: deleted === true if cache entry existed
    1. Get data: const result = await cache.get('https://cache.internal/instances')
    1. Expected: result === null (entry deleted)
    1. Note: Only exact matches deleted, parameterized queries remain cached
  • Test 12: clearByEndpoint with Non-existent Endpoint

    1. Clear non-existent: const deleted = await cache.clearByEndpoint('non-existent')
    1. Expected: deleted === false
    1. Expected: Log message about non-existent endpoint
  • Test 13: Cache Invalidation Strategy

    1. Set parameterized cache entries:
    • cache.generateKey({ provider: 'linode', region: 'us-east' })
    • cache.generateKey({ provider: 'linode', region: 'eu-west' })
    • cache.generateKey({ provider: 'vultr', region: 'us-east' })
    1. After schema change or full sync, call clearAll()
    1. Verify entries expire based on TTL
    1. For production: Consider using KV-backed cache index for enumeration
  • Test 14: Error Handling in clearAll

    1. Mock cache.delete to throw error
    1. Call clearAll: await cache.clearAll()
    1. Expected: Error is logged and re-thrown
    1. Verify error message includes context
  • Test 15: Error Handling in clearByEndpoint

    1. Mock cache.delete to throw error
    1. Call clearByEndpoint: await cache.clearByEndpoint('instances')
    1. Expected: Returns false, error logged
    1. Application continues without crashing */

import { CacheService } from './cache'; import type { InstanceData } from '../types';

/**

  • Example: Using CacheService in API endpoint */ async function exampleInstanceEndpointWithCache( queryParams: Record<string, unknown>, fetchFromDatabase: () => Promise<InstanceData[]> ): Promise<{ data: InstanceData[]; cached?: boolean; cache_age?: number }> { const cache = new CacheService(300); // 5 minutes default TTL

// Generate cache key from query parameters const cacheKey = cache.generateKey(queryParams);

// Try to get from cache const cached = await cache.get<InstanceData[]>(cacheKey);

if (cached) { console.log([API] Cache hit (age: ${cached.cache_age_seconds}s)); return { data: cached.data, cached: true, cache_age: cached.cache_age_seconds, }; }

// Cache miss - fetch from database console.log('[API] Cache miss - fetching from database'); const data = await fetchFromDatabase();

// Determine TTL based on query complexity const hasFilters = Object.keys(queryParams).length > 0; const ttl = hasFilters ? 300 : 3600; // 5 min for filtered, 1 hour for full

// Store in cache await cache.set(cacheKey, data, ttl);

return { data, cached: false, }; }

/**

  • Example: Cache invalidation after sync */ async function exampleCacheInvalidationAfterSync( syncedProviders: string[] ): Promise { const cache = new CacheService();

// Invalidate all instance caches for synced providers for (const provider of syncedProviders) { // Note: Since Cloudflare Workers Cache API doesn't support pattern matching, // you need to maintain a list of active cache keys or use KV for indexing const key = cache.generateKey({ provider }); await cache.delete(key); console.log([Sync] Invalidated cache for provider: ${provider}); }

console.log('[Sync] Cache invalidation complete'); }

/**

  • Example: Using clearAll after schema changes */ async function exampleClearAllAfterSchemaChange(): Promise { const cache = new CacheService();

// After major schema changes or data migrations console.log('[Migration] Clearing all cache entries'); const count = await cache.clearAll();

console.log([Migration] Cache clear requested. Entries will expire based on TTL.); console.log('[Migration] Consider using KV-backed cache index for enumeration in production.'); }

/**

  • Example: Using clearByEndpoint for targeted invalidation */ async function exampleClearByEndpointAfterUpdate(endpoint: string): Promise { const cache = new CacheService();

// Clear cache for specific endpoint after data update console.log([Update] Clearing cache for endpoint: ${endpoint}); const deleted = await cache.clearByEndpoint(endpoint);

if (deleted) { console.log([Update] Successfully cleared ${endpoint} cache); } else { console.log([Update] No cache entry found for ${endpoint}); } }

/**

  • Example: Force cache refresh strategy */ async function exampleForceCacheRefresh( endpoint: string, fetchFunction: () => Promise ): Promise { const cache = new CacheService();

// Strategy 1: Clear specific endpoint await cache.clearByEndpoint(endpoint);

// Strategy 2: Clear with prefix (logged but not enumerated) await cache.clearAll(https://cache.internal/${endpoint});

// Strategy 3: Fetch fresh data and update cache const freshData = await fetchFunction(); const cacheKey = https://cache.internal/${endpoint}; await cache.set(cacheKey, freshData, 3600);

console.log([Refresh] Cache refreshed for ${endpoint}); }

export { exampleInstanceEndpointWithCache, exampleCacheInvalidationAfterSync, exampleClearAllAfterSchemaChange, exampleClearByEndpointAfterUpdate, exampleForceCacheRefresh };