59 lines
3.9 KiB
TypeScript
59 lines
3.9 KiB
TypeScript
// @vitest-environment node
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { NextRequest } from 'next/server';
|
|
import { createHash } from 'node:crypto';
|
|
import { zipSync, strToU8 } from 'fflate';
|
|
const mocks = vi.hoisted(() => ({ verify: vi.fn(), get: vi.fn(), doc: vi.fn() }));
|
|
vi.mock('../lib/firebase-admin', () => ({ getAdminAuth: () => ({ verifyIdToken: mocks.verify }), getAdminFirestore: () => ({ doc: mocks.doc }) }));
|
|
import { GET } from '../app/api/account/rides/[rideId]/telemetry/route';
|
|
const rideId = 'ride_sanitized';
|
|
const gpx = '<gpx><trk><trkseg><trkpt lat="40" lon="-74"><time>2026-09-03T20:00:00Z</time></trkpt><trkpt lat="40.001" lon="-73.999"><time>2026-09-03T20:00:01Z</time></trkpt></trkseg></trk></gpx>';
|
|
const bundle = zipSync({ 'telemetry.gpx': strToU8(gpx), 'ride_summary.json': strToU8('{}') });
|
|
const digest = createHash('sha256').update(bundle).digest('hex');
|
|
const call = (authorization?: string) => GET(new NextRequest(`https://motorsignal.app/api/account/rides/${rideId}/telemetry`, {headers: authorization ? {authorization} : {}}), {params: Promise.resolve({rideId})});
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
mocks.verify.mockResolvedValue({uid:'sanitized-owner'});
|
|
mocks.doc.mockReturnValue({get:mocks.get});
|
|
mocks.get.mockResolvedValue({exists:true,data:()=>({id:rideId,ownerId:'sanitized-owner',telemetryBundlePath:`users/sanitized-owner/rides/${rideId}/telemetry.zip`})});
|
|
vi.stubGlobal('fetch',vi.fn().mockResolvedValue(new Response(bundle, {status:200,headers:{'x-motorsignal-ride-id':rideId,'x-motorsignal-sha256':digest}})));
|
|
});
|
|
describe('owned recorded telemetry API', () => {
|
|
it('fails closed without authentication before any file/network/Firestore lookup',async()=>{
|
|
expect((await call()).status).toBe(401);
|
|
expect(mocks.get).not.toHaveBeenCalled();
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
it('rejects invalid tokens and a mismatched owner',async()=>{
|
|
mocks.verify.mockRejectedValueOnce(Error('invalid'));
|
|
expect((await call('Bearer invalid')).status).toBe(401);
|
|
mocks.get.mockResolvedValueOnce({exists:true,data:()=>({ownerId:'another-owner'})});
|
|
expect((await call('Bearer valid')).status).toBe(403);
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
it('downloads and verifies the selected R2 bundle and returns its actual GPS',async()=>{
|
|
const response = await call('Bearer valid');
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.rideId).toBe(rideId);
|
|
expect(body.source).toBe('r2-gpx');
|
|
expect(body.points.map((p:{latitude:number;longitude:number})=>[p.latitude,p.longitude])).toEqual([[40,-74],[40.001,-73.999]]);
|
|
expect(mocks.doc).toHaveBeenCalledWith(`users/sanitized-owner/rides/${rideId}`);
|
|
expect(fetch).toHaveBeenCalledWith(expect.stringContaining(`rideId=${rideId}`),expect.objectContaining({cache:'no-store',headers:expect.objectContaining({Authorization:'Bearer valid'})}));
|
|
expect(response.headers.get('cache-control')).toContain('no-store');
|
|
});
|
|
it('rejects a different ride or modified bundle rather than using demo/fallback data',async()=>{
|
|
vi.mocked(fetch).mockResolvedValueOnce(new Response(bundle,{headers:{'x-motorsignal-ride-id':'ride_wrong','x-motorsignal-sha256':digest}}));
|
|
expect((await call('Bearer valid')).status).toBe(422);
|
|
vi.mocked(fetch).mockResolvedValueOnce(new Response(bundle,{headers:{'x-motorsignal-ride-id':rideId,'x-motorsignal-sha256':'0'.repeat(64)}}));
|
|
expect((await call('Bearer valid')).status).toBe(422);
|
|
});
|
|
it('keeps inline telemetry separate from R2 and checks ownership for both',async()=>{
|
|
mocks.get.mockResolvedValueOnce({exists:true,data:()=>({id:rideId,ownerId:'sanitized-owner',gpxContent:gpx})});
|
|
const response=await call('Bearer valid');
|
|
expect(response.status).toBe(200);
|
|
expect((await response.json()).source).toBe('inline-gpx');
|
|
expect(fetch).not.toHaveBeenCalled();
|
|
});
|
|
});
|