import { describe, it, expect, vi, beforeEach } from 'vitest'; import { commitGroupRide, resetRideIdCounter } from '../lib/create-group-ride'; import type { WebGroupRideInput } from '../lib/group-ride-payload'; const baseRide: WebGroupRideInput = { id: 'ride-temp', title: 'Canyon Dawn Patrol', description: 'Fast sweepers', region: 'Santa Cruz Mountains', scheduledDate: '', scheduledTime: '', paceClassification: 'Brisk Pace', discipline: 'Sport / Canyon', formationProfile: 'Staggered Dual Column [B]', joinPolicy: 'requestToJoin', leadHandle: 'You (Organizer)', leadAvatarText: 'YOU', participantCount: 1, participantLimit: 12, distance: '50.0 Miles', meetingPoint: 'Alice’s Restaurant', routeSnippet: 'Skyline > Hwy 9', userStatus: 'joined', status: 'scheduled', }; function makeDeps(overrides: Partial[1]> = {}) { const setSpy = vi.fn(); const commitSpy = vi.fn().mockResolvedValue(undefined); const deps = { uid: 'rider-uid-1' as string | null | undefined, firestore: {} as unknown, leadDisplayName: 'Rider One', leadHandle: 'rider_one', now: new Date('2026-10-14T12:00:00.000Z'), writeBatch: () => ({ set: setSpy, commit: commitSpy }), docRef: (_fs: any, ...segments: string[]) => ({ path: segments.join('/') }), ...overrides, }; return { deps, setSpy, commitSpy }; } describe('commitGroupRide (WEB Phase 1 — persisted ride creation)', () => { beforeEach(() => { resetRideIdCounter(); }); it('refuses to create for an unauthenticated user (no uid) and never writes', async () => { const { deps, commitSpy } = makeDeps({ uid: undefined }); const res = await commitGroupRide(baseRide, deps); expect(res).toEqual({ ok: false, reason: 'unauthenticated' }); expect(commitSpy).not.toHaveBeenCalled(); }); it('reports unavailable when Firestore is missing and never writes', async () => { const { deps, commitSpy } = makeDeps({ firestore: null }); const res = await commitGroupRide(baseRide, deps); expect(res).toEqual({ ok: false, reason: 'unavailable' }); expect(commitSpy).not.toHaveBeenCalled(); }); it('persists the ride and participant, then returns the real rideId as the join code', async () => { const { deps, setSpy, commitSpy } = makeDeps(); const res = await commitGroupRide(baseRide, deps); expect(res.ok).toBe(true); if (!res.ok) return; expect(res.rideId).toMatch(/^ride_\d+_rider-/); // Two writes: the ride document and the organizer participant. expect(setSpy).toHaveBeenCalledTimes(2); expect(commitSpy).toHaveBeenCalledTimes(1); // The persisted payload never fabricates coordinates. const mp = (res.payload as any).meetingPoint; expect('latitude' in mp).toBe(false); expect('longitude' in mp).toBe(false); expect(mp.displayName).toBe('Alice’s Restaurant'); expect((res.payload as any).organizerUid).toBe('rider-uid-1'); }); it('returns commit-failed (and does not throw) when the Firestore write rejects', async () => { const boom = new Error('network down'); const { deps } = makeDeps({ writeBatch: () => ({ set: vi.fn(), commit: vi.fn().mockRejectedValue(boom) }), }); const res = await commitGroupRide(baseRide, deps); expect(res).toEqual({ ok: false, reason: 'commit-failed', error: boom }); }); it('guarantees unique ride IDs across concurrent submissions at the same millisecond', async () => { const sharedNow = new Date('2026-10-14T12:00:00.000Z'); const { deps: deps1 } = makeDeps({ now: sharedNow }); const { deps: deps2 } = makeDeps({ now: sharedNow }); const [res1, res2] = await Promise.all([ commitGroupRide(baseRide, deps1), commitGroupRide(baseRide, deps2), ]); expect(res1.ok).toBe(true); expect(res2.ok).toBe(true); if (res1.ok && res2.ok) { expect(res1.rideId).not.toBe(res2.rideId); expect(res1.rideId).toMatch(/^ride_\d+_rider-/); expect(res2.rideId).toMatch(/^ride_\d+_rider-/); } }); it('preserves clean timestamps without string suffix corruption on successive rides', async () => { const t1 = new Date('2026-10-14T12:00:00.000Z'); const t2 = new Date('2026-10-14T12:05:00.000Z'); const { deps: deps1 } = makeDeps({ now: t1 }); const { deps: deps2 } = makeDeps({ now: t2 }); const res1 = await commitGroupRide(baseRide, deps1); const res2 = await commitGroupRide(baseRide, deps2); expect(res1.ok).toBe(true); expect(res2.ok).toBe(true); if (res1.ok && res2.ok) { // Must equal exact millisecond timestamp, NOT corrupted with appended counter digits expect(res1.rideId).toBe(`ride_${t1.getTime()}_rider-`); expect(res2.rideId).toBe(`ride_${t2.getTime()}_rider-`); } }); it('handles permission-denied or rejected commit cleanly without throwing', async () => { const permDenied = new Error('permission-denied: caller lacks organizer permission'); const { deps } = makeDeps({ writeBatch: () => ({ set: vi.fn(), commit: vi.fn().mockRejectedValue(permDenied) }), }); const res = await commitGroupRide(baseRide, deps); expect(res.ok).toBe(false); if (!res.ok) { expect(res.reason).toBe('commit-failed'); expect((res as any).error).toBe(permDenied); } }); });