import { describe, it, expect, vi } from 'vitest'; import { HttpException } from '@nestjs/common'; import type { Response } from 'express'; import { AtlasController } from '../../../src/nest/atlas/atlas.controller'; import type { AtlasService } from '../../../src/nest/atlas/atlas.service'; import type { User } from '../../../src/types'; const user = { id: 8 } as User; function makeController(svc: Partial) { return new AtlasController(svc as AtlasService); } function makeRes() { return { setHeader: vi.fn() } as unknown as Response & { setHeader: ReturnType }; } async function thrown(fn: () => unknown): Promise<{ status: number; body: unknown }> { try { await fn(); } catch (err) { expect(err).toBeInstanceOf(HttpException); const e = err as HttpException; return { status: e.getStatus(), body: e.getResponse() }; } throw new Error('expected the handler to throw'); } describe('AtlasController (parity with the legacy /api/addons/atlas route)', () => { it('GET /stats delegates with the user id', () => { const stats = vi.fn().mockReturnValue({ countries: 3 }); expect(makeController({ stats }).stats(user)).toEqual({ countries: 3 }); expect(stats).toHaveBeenCalledWith(8); }); describe('GET /regions/geo', () => { it('returns an empty FeatureCollection without a cache header when no countries given', async () => { const regionGeo = vi.fn(); const res = makeRes(); const out = await makeController({ regionGeo }).regionGeo(undefined, res); expect(out).toEqual({ type: 'FeatureCollection', features: [] }); expect(regionGeo).not.toHaveBeenCalled(); expect(res.setHeader).not.toHaveBeenCalled(); }); it('caches a non-empty result for a day', async () => { const regionGeo = vi.fn().mockResolvedValue({ type: 'FeatureCollection', features: [{ id: 1 }] }); const res = makeRes(); const out = await makeController({ regionGeo }).regionGeo('DE,FR', res); expect(out).toEqual({ type: 'FeatureCollection', features: [{ id: 1 }] }); expect(regionGeo).toHaveBeenCalledWith(['DE', 'FR']); expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'public, max-age=86400'); }); }); it('GET /countries/geo delegates to the service', () => { const fc = { type: 'FeatureCollection', features: [{ id: 'NO' }] }; const countryGeo = vi.fn().mockReturnValue(fc); expect(makeController({ countryGeo }).countryGeo()).toBe(fc); expect(countryGeo).toHaveBeenCalledWith(); }); describe('country', () => { it('GET /country/:code upper-cases the code', () => { const countryPlaces = vi.fn().mockReturnValue([]); makeController({ countryPlaces }).countryPlaces(user, 'de'); expect(countryPlaces).toHaveBeenCalledWith(8, 'DE'); }); it('POST mark returns success and upper-cases', () => { const markCountry = vi.fn(); expect(makeController({ markCountry }).markCountry(user, 'de')).toEqual({ success: true }); expect(markCountry).toHaveBeenCalledWith(8, 'DE'); }); it('DELETE mark returns success', () => { const unmarkCountry = vi.fn(); expect(makeController({ unmarkCountry }).unmarkCountry(user, 'FR')).toEqual({ success: true }); }); }); describe('region', () => { it('400 when name or country_code is missing', () => { const markRegion = vi.fn(); return thrown(() => makeController({ markRegion }).markRegion(user, 'by', undefined, 'DE')).then((r) => expect(r).toEqual({ status: 400, body: { error: 'name and country_code are required' } })); }); it('marks a region, upper-casing both codes', () => { const markRegion = vi.fn(); expect(makeController({ markRegion }).markRegion(user, 'by', 'Bavaria', 'de')).toEqual({ success: true }); expect(markRegion).toHaveBeenCalledWith(8, 'BY', 'Bavaria', 'DE'); }); }); describe('bucket list', () => { it('GET wraps the items', () => { const bucketList = vi.fn().mockReturnValue([{ id: 1 }]); expect(makeController({ bucketList }).bucketList(user)).toEqual({ items: [{ id: 1 }] }); }); it('400 on create with a blank name', () => { const createBucketItem = vi.fn(); return thrown(() => makeController({ createBucketItem }).createBucketItem(user, { name: ' ' })).then((r) => expect(r).toEqual({ status: 400, body: { error: 'Name is required' } })); }); it('201-shape create returns { item }', () => { const createBucketItem = vi.fn().mockReturnValue({ id: 1, name: 'Tokyo' }); expect(makeController({ createBucketItem }).createBucketItem(user, { name: 'Tokyo', lat: 35, lng: 139 })) .toEqual({ item: { id: 1, name: 'Tokyo' } }); expect(createBucketItem).toHaveBeenCalledWith(8, { name: 'Tokyo', lat: 35, lng: 139, country_code: undefined, notes: undefined, target_date: undefined }); }); it('404 on update of a missing item', () => { const updateBucketItem = vi.fn().mockReturnValue(null); return thrown(() => makeController({ updateBucketItem }).updateBucketItem(user, '9', { name: 'X' })).then((r) => expect(r).toEqual({ status: 404, body: { error: 'Item not found' } })); }); it('updates an existing item', () => { const updateBucketItem = vi.fn().mockReturnValue({ id: 1, name: 'Kyoto' }); expect(makeController({ updateBucketItem }).updateBucketItem(user, '1', { name: 'Kyoto' })) .toEqual({ item: { id: 1, name: 'Kyoto' } }); }); it('404 on delete of a missing item', () => { const deleteBucketItem = vi.fn().mockReturnValue(false); return thrown(() => makeController({ deleteBucketItem }).deleteBucketItem(user, '9')).then((r) => expect(r).toEqual({ status: 404, body: { error: 'Item not found' } })); }); it('deletes an existing item', () => { const deleteBucketItem = vi.fn().mockReturnValue(true); expect(makeController({ deleteBucketItem }).deleteBucketItem(user, '1')).toEqual({ success: true }); }); }); });