MotorSignal-App/website/tests/create-group-ride.test.ts

139 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Parameters<typeof commitGroupRide>[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);
}
});
});