import { afterEach, describe, expect, it, vi } from 'vitest' import { publicTransport } from '../../server/utils/transitous' const LILLE = { lat: 50.6292, lon: 3.0573 } const BRUXELLES = { lat: 50.8503, lon: 4.3517 } const leg = (mode: string, distance: number, minutes: number) => ({ mode, distance, duration: minutes * 60, startTime: '', endTime: '', from: LILLE, to: BRUXELLES, }) const journey = (legs: ReturnType[], minutes: number, transfers = 0) => ({ duration: minutes * 60, transfers, legs, }) /** * Le vrai code passe deux requetes distinctes, une par famille de mode. * Le faux doit faire la meme distinction, sinon il renvoie un trajet en car * a la requete ferroviaire et le test valide un comportement impossible. */ function mockApi(byFamily: { rail?: unknown[], urban?: unknown[] }) { const spy = vi.fn().mockImplementation((url: string) => { const rail = url.includes('HIGHSPEED_RAIL') return Promise.resolve({ itineraries: (rail ? byFamily.rail : byFamily.urban) ?? [] }) }) vi.stubGlobal('$fetch', spy) return spy } afterEach(() => vi.unstubAllGlobals()) describe('publicTransport', () => { it('interroge chaque famille de mode separement', async () => { const spy = mockApi({}) await publicTransport(LILLE, BRUXELLES, new Date()) expect(spy).toHaveBeenCalledTimes(2) const urls = spy.mock.calls.map(c => c[0] as string) expect(urls.some(u => u.includes('HIGHSPEED_RAIL'))).toBe(true) expect(urls.some(u => u.includes('COACH') && !u.includes('HIGHSPEED_RAIL'))).toBe(true) }) it('separe le meilleur trajet ferroviaire du meilleur trajet urbain', async () => { mockApi({ rail: [journey([leg('HIGHSPEED_RAIL', 100000, 90)], 90, 1)], urban: [journey([leg('BUS', 95000, 180)], 180, 3)], }) const r = await publicTransport(LILLE, BRUXELLES, new Date()) // 100 km de TGV a 2,4 g contre 95 km de bus a 113 g. expect(r.train?.co2Grams).toBe(240) expect(r.bus?.co2Grams).toBe(10735) expect(r.train?.transfers).toBe(1) }) it('retient le plus rapide quand plusieurs trajets ferroviaires existent', async () => { mockApi({ rail: [ journey([leg('REGIONAL_RAIL', 100000, 180)], 180, 2), journey([leg('HIGHSPEED_RAIL', 100000, 90)], 90, 0), ], }) const r = await publicTransport(LILLE, BRUXELLES, new Date()) expect(r.train?.durationMin).toBe(90) expect(r.train?.transfers).toBe(0) }) it('renvoie null pour un mode absent plutot que d inventer un trajet', async () => { mockApi({ urban: [journey([leg('COACH', 50000, 60)], 60)] }) const r = await publicTransport(LILLE, BRUXELLES, new Date()) expect(r.train).toBeNull() expect(r.bus).not.toBeNull() }) it('ecarte un itineraire entierement pietonnier', async () => { // MOTIS propose parfois de marcher faute de mieux : ce n'est pas // un trajet en transport en commun et il ne doit pas s'afficher. mockApi({ rail: [journey([leg('WALK', 3000, 40)], 40)] }) const r = await publicTransport(LILLE, BRUXELLES, new Date()) expect(r.train).toBeNull() }) it('s identifie aupres de Transitous comme leur politique d usage l exige', async () => { const spy = mockApi({}) await publicTransport(LILLE, BRUXELLES, new Date()) const [url, options] = spy.mock.calls[0] expect(url).toContain('fromPlace=50.6292,3.0573') expect(options.headers['User-Agent']).toContain('lecheminvert.tech') }) })