import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock jose before importing worker vi.mock('jose', () => ({ createRemoteJWKSet: vi.fn(() => vi.fn()), jwtVerify: vi.fn(async (token: string) => { if (token === 'valid_token') { return { payload: { sub: 'test_user_123', exp: Math.floor(Date.now() / 1000) + 3600, firebase: { sign_in_provider: 'password' }, }, }; } if (token === 'anon_token') { return { payload: { sub: 'anon_user_456', exp: Math.floor(Date.now() / 1000) + 3600, firebase: { sign_in_provider: 'anonymous' }, }, }; } throw new Error('Invalid token'); }), })); import worker, { type Env } from './index'; describe('Worker /r2/verify endpoint', () => { let mockEnv: Env; let mockR2Head: ReturnType; beforeEach(() => { vi.restoreAllMocks(); mockR2Head = vi.fn(); mockEnv = { R2_BUCKET: { head: mockR2Head, } as any, FIREBASE_PROJECT_ID: 'motoapp-test', ENVIRONMENT: 'test', GOOGLE_SERVICE_ACCOUNT_EMAIL: 'sa@test.iam.gserviceaccount.com', GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: 'test-key', GOOGLE_PLAY_PACKAGE_NAME: 'app.motorsignal.motorcycle', }; }); function mockValidFirestore(uid = 'test_user_123', rideId = 'ride_abc_123') { mockFirestore(200, { fields: { ownerId: { stringValue: uid }, id: { stringValue: rideId }, telemetryBundlePath: { stringValue: `users/${uid}/rides/${rideId}/telemetry.zip`, }, }, }); } function mockFirestore(status: number, data?: any) { vi.stubGlobal('fetch', vi.fn(async (url: string) => { if (url.includes('firestore.googleapis.com')) { return new Response(JSON.stringify(data || {}), { status }); } return new Response('Not Found', { status: 404 }); })); } it('1. returns CORS headers on OPTIONS preflight request', async () => { const req = new Request('https://worker.local/r2/verify?rideId=ride_1', { method: 'OPTIONS', }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(200); expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*'); }); it('2. returns 404 Endpoint not found for unknown paths', async () => { const req = new Request('https://worker.local/unknown-path', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(404); const body = await res.json() as any; expect(body.error).toBe('Endpoint not found'); }); it('3. rejects /r2/verify with 401 when Authorization header is missing', async () => { const req = new Request('https://worker.local/r2/verify?rideId=ride_1', { method: 'GET', }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(401); const body = await res.json() as any; expect(body.error).toContain('Missing or malformed Authorization header'); }); it('4. rejects /r2/verify with 401 when Authorization header lacks Bearer prefix', async () => { const req = new Request('https://worker.local/r2/verify?rideId=ride_1', { method: 'GET', headers: { Authorization: 'Basic dXNlcjpwYXNz' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(401); }); it('5. rejects /r2/verify with 401 when Firebase token verification fails', async () => { const req = new Request('https://worker.local/r2/verify?rideId=ride_1', { method: 'GET', headers: { Authorization: 'Bearer invalid_or_expired_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(401); const body = await res.json() as any; expect(body.error).toContain('Invalid, expired, or unauthorized'); }); it('6. rejects /r2/verify with 400 when rideId query parameter is missing', async () => { const req = new Request('https://worker.local/r2/verify', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(400); const body = await res.json() as any; expect(body.error).toContain('Invalid rideId parameter'); }); it('7. rejects /r2/verify with 400 when rideId contains invalid characters or path traversal', async () => { const req = new Request('https://worker.local/r2/verify?rideId=../bad/traversal', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(400); const body = await res.json() as any; expect(body.error).toContain('Invalid rideId parameter'); }); it('8. returns 403 UNAUTHORIZED when Firestore returns 403', async () => { mockFirestore(403); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(403); const body = await res.json() as any; expect(body.code).toBe('UNAUTHORIZED'); }); it('9. returns 404 NOT_FOUND when Firestore ride document does not exist', async () => { mockFirestore(404); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(404); const body = await res.json() as any; expect(body.code).toBe('NOT_FOUND'); }); it('10. returns 409 INVALID_METADATA when Firestore document has invalid or missing bundle path', async () => { mockFirestore(200, { fields: { ownerId: { stringValue: 'test_user_123' }, id: { stringValue: 'ride_abc_123' }, telemetryBundlePath: { stringValue: 'invalid/bundle/path.zip' }, }, }); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(409); const body = await res.json() as any; expect(body.code).toBe('INVALID_METADATA'); }); it('11. returns 404 OBJECT_NOT_FOUND when bundle does not exist in R2', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue(null); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(404); const body = await res.json() as any; expect(body.code).toBe('OBJECT_NOT_FOUND'); expect(mockR2Head).toHaveBeenCalledWith('users/test_user_123/rides/ride_abc_123/telemetry.zip'); }); it('12. returns 200 with verification details on GET when object exists in R2', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue({ size: 4096, httpEtag: '"etag-12345"', customMetadata: { sha256: 'abc123def456', appVersion: '1.1.0', uploadedAt: '2026-10-12T10:00:00.000Z', }, }); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(200); const body = await res.json() as any; expect(body.verified).toBe(true); expect(body.remoteRideId).toBe('ride_abc_123'); expect(body.objectKey).toBe('users/test_user_123/rides/ride_abc_123/telemetry.zip'); expect(body.size).toBe(4096); expect(body.etag).toBe('"etag-12345"'); expect(body.sha256).toBe('abc123def456'); expect(body.appVersion).toBe('1.1.0'); expect(body.uploadedAt).toBe('2026-10-12T10:00:00.000Z'); }); it('13. sets verification properties in JSON body on GET and headers on HEAD', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue({ size: 8192, httpEtag: '"etag-8192"', customMetadata: { sha256: 'sha_test_789' }, }); const getReq = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const getRes = await worker.fetch(getReq, mockEnv); const getBody = await getRes.json() as any; expect(getBody.etag).toBe('"etag-8192"'); expect(getBody.remoteRideId).toBe('ride_abc_123'); expect(getBody.sha256).toBe('sha_test_789'); expect(getBody.size).toBe(8192); const headReq = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'HEAD', headers: { Authorization: 'Bearer valid_token' }, }); const headRes = await worker.fetch(headReq, mockEnv); expect(headRes.headers.get('etag')).toBe('"etag-8192"'); expect(headRes.headers.get('X-MotorSignal-Ride-Id')).toBe('ride_abc_123'); expect(headRes.headers.get('X-MotorSignal-Sha256')).toBe('sha_test_789'); expect(headRes.headers.get('X-MotorSignal-Size')).toBe('8192'); }); it('14. handles HEAD request on /r2/verify with 200 and headers but null body', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue({ size: 4096, httpEtag: '"etag-head"', customMetadata: { sha256: 'sha_head' }, }); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'HEAD', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(200); expect(res.body).toBeNull(); expect(res.headers.get('X-MotorSignal-Ride-Id')).toBe('ride_abc_123'); expect(res.headers.get('X-MotorSignal-Size')).toBe('4096'); }); it('15. handles HEAD request on /r2/verify when bundle not found in R2 with 404', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue(null); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'HEAD', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(404); const body = await res.json() as any; expect(body.code).toBe('OBJECT_NOT_FOUND'); }); it('16. correctly returns undefined sha256 for historical bundles without checksum metadata', async () => { mockValidFirestore('test_user_123', 'ride_legacy_001'); mockR2Head.mockResolvedValue({ size: 1024, httpEtag: '"etag-legacy"', customMetadata: undefined, }); const req = new Request('https://worker.local/r2/verify?rideId=ride_legacy_001', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(200); const body = await res.json() as any; expect(body.verified).toBe(true); expect(body.sha256).toBeUndefined(); expect(res.headers.has('X-MotorSignal-Sha256')).toBe(false); }); it('17. targets user-scoped R2 object key matching authenticated user', async () => { mockValidFirestore('test_user_123', 'ride_custom_id'); mockR2Head.mockResolvedValue({ size: 512, httpEtag: '"etag-scoped"', }); const req = new Request('https://worker.local/r2/verify?rideId=ride_custom_id', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); await worker.fetch(req, mockEnv); expect(mockR2Head).toHaveBeenCalledWith('users/test_user_123/rides/ride_custom_id/telemetry.zip'); }); it('18. returns 500 when unexpected internal worker exception occurs', async () => { mockValidFirestore('test_user_123', 'ride_err_1'); mockR2Head.mockRejectedValue(new Error('R2 cluster unreachable')); const req = new Request('https://worker.local/r2/verify?rideId=ride_err_1', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(500); const body = await res.json() as any; expect(body.error).toBe('Internal Worker Exception'); }); it('19. returns 503 TEMPORARY_ERROR when Firestore ownership lookup fails with 429 or 503', async () => { mockFirestore(503, { error: 'Service Unavailable' }); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(503); const body = await res.json() as any; expect(body.code).toBe('TEMPORARY_ERROR'); expect(body.error).toContain('Firestore temporary lookup error'); }); it('20. sets X-MotorSignal-* headers on GET as well as HEAD', async () => { mockValidFirestore(); mockR2Head.mockResolvedValue({ size: 2048, httpEtag: '"etag-2048"', customMetadata: { sha256: 'sha256_get_test' }, }); const req = new Request('https://worker.local/r2/verify?rideId=ride_abc_123', { method: 'GET', headers: { Authorization: 'Bearer valid_token' }, }); const res = await worker.fetch(req, mockEnv); expect(res.status).toBe(200); expect(res.headers.get('X-MotorSignal-Ride-Id')).toBe('ride_abc_123'); expect(res.headers.get('X-MotorSignal-Sha256')).toBe('sha256_get_test'); expect(res.headers.get('X-MotorSignal-Size')).toBe('2048'); expect(res.headers.get('etag')).toBe('"etag-2048"'); }); });