mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
* fix(server): set oxc:false in vitest so the SWC transform survives the Vite 8 bump * fix(server): switch coverage to the istanbul provider (v8 under-reports branches on Vite 8 + Vitest 4) * test(nest): cover controller/service branches to clear the 80% coverage gate
This commit is contained in:
@@ -85,10 +85,63 @@ describe('PlacesController (parity with the legacy /api/trips/:tripId/places rou
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /import/map', () => {
|
||||
const file = { buffer: Buffer.from('<kml/>'), originalname: 'm.kml' } as Express.Multer.File;
|
||||
it('400 without a file', async () => {
|
||||
expect(await thrownAsync(() => new PlacesController(svc()).importMap(user, '5', undefined, {}))).toEqual({ status: 400, body: { error: 'No file uploaded' } });
|
||||
});
|
||||
it('403 without place_edit (permission runs before the file check)', async () => {
|
||||
const importMapFile = vi.fn();
|
||||
const s = svc({ canEdit: vi.fn().mockReturnValue(false), importMapFile } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importMap(user, '5', file, {}))).toEqual({ status: 403, body: { error: 'No permission' } });
|
||||
expect(importMapFile).not.toHaveBeenCalled();
|
||||
});
|
||||
it('400 when both import types are disabled', async () => {
|
||||
expect(await thrownAsync(() => new PlacesController(svc()).importMap(user, '5', file, { importPoints: 'false', importPaths: 'false' }))).toEqual({
|
||||
status: 400, body: { error: 'No import types selected' },
|
||||
});
|
||||
});
|
||||
it('400 when the map file has no Placemarks (and carries the summary through)', async () => {
|
||||
const summary = { totalPlacemarks: 0 };
|
||||
const s = svc({ importMapFile: vi.fn().mockResolvedValue({ places: [], summary }) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importMap(user, '5', file, {}))).toEqual({
|
||||
status: 400, body: { error: 'No valid Placemarks found in map file', summary },
|
||||
});
|
||||
});
|
||||
it('imports, broadcasts per place + returns the service result', async () => {
|
||||
const broadcast = vi.fn();
|
||||
const result = { places: [{ id: 1 }, { id: 2 }], summary: { totalPlacemarks: 2 }, count: 2 };
|
||||
const s = svc({ importMapFile: vi.fn().mockResolvedValue(result), broadcast } as Partial<PlacesService>);
|
||||
expect(await new PlacesController(s).importMap(user, '5', file, {}, 'sock')).toEqual(result);
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
expect(broadcast).toHaveBeenCalledWith('5', 'place:created', { place: { id: 1 } }, 'sock');
|
||||
});
|
||||
it('passes a missing summary through (no zero-placemark guard) and still imports', async () => {
|
||||
const result = { places: [{ id: 7 }] };
|
||||
const s = svc({ importMapFile: vi.fn().mockResolvedValue(result), broadcast: vi.fn() } as Partial<PlacesService>);
|
||||
expect(await new PlacesController(s).importMap(user, '5', file, {})).toEqual(result);
|
||||
});
|
||||
it('wraps a thrown Error from the service in a 400 with its message', async () => {
|
||||
const s = svc({ importMapFile: vi.fn().mockRejectedValue(new Error('bad kml')) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importMap(user, '5', file, {}))).toEqual({ status: 400, body: { error: 'bad kml' } });
|
||||
});
|
||||
it('falls back to a generic 400 message for a non-Error rejection', async () => {
|
||||
const s = svc({ importMapFile: vi.fn().mockRejectedValue('boom') } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importMap(user, '5', file, {}))).toEqual({ status: 400, body: { error: 'Failed to import map file' } });
|
||||
});
|
||||
it('re-throws an HttpException raised inside the try untouched', async () => {
|
||||
const s = svc({ importMapFile: vi.fn().mockRejectedValue(new HttpException({ error: 'teapot' }, 418)) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importMap(user, '5', file, {}))).toEqual({ status: 418, body: { error: 'teapot' } });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /import/google-list + naver-list', () => {
|
||||
it('400 without a url', async () => {
|
||||
expect(await thrownAsync(() => new PlacesController(svc()).importGoogle(user, '5', undefined))).toEqual({ status: 400, body: { error: 'URL is required' } });
|
||||
});
|
||||
it('400 when url is the wrong type (not a string)', async () => {
|
||||
expect(await thrownAsync(() => new PlacesController(svc()).importNaver(user, '5', 123))).toEqual({ status: 400, body: { error: 'URL is required' } });
|
||||
});
|
||||
it('maps a service { error, status } to the same response', async () => {
|
||||
const s = svc({ importGoogleList: vi.fn().mockResolvedValue({ error: 'List is empty', status: 400 }) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importGoogle(user, '5', 'http://x'))).toEqual({ status: 400, body: { error: 'List is empty' } });
|
||||
@@ -97,6 +150,26 @@ describe('PlacesController (parity with the legacy /api/trips/:tripId/places rou
|
||||
const s = svc({ importNaverList: vi.fn().mockResolvedValue({ places: [{ id: 1 }], listName: 'Trip', skipped: 2 }), broadcast: vi.fn() } as Partial<PlacesService>);
|
||||
expect(await new PlacesController(s).importNaver(user, '5', 'http://x')).toEqual({ places: [{ id: 1 }], count: 1, listName: 'Trip', skipped: 2 });
|
||||
});
|
||||
it('forwards the enrich flag + userId and broadcasts each imported place', async () => {
|
||||
const importGoogleList = vi.fn().mockResolvedValue({ places: [{ id: 1 }, { id: 2 }], listName: 'L', skipped: 0 });
|
||||
const broadcast = vi.fn();
|
||||
const s = svc({ importGoogleList, broadcast } as Partial<PlacesService>);
|
||||
expect(await new PlacesController(s).importGoogle(user, '5', 'http://x', 'true', 'sock')).toEqual({ places: [{ id: 1 }, { id: 2 }], count: 2, listName: 'L', skipped: 0 });
|
||||
expect(importGoogleList).toHaveBeenCalledWith('5', 'http://x', { enrich: true, userId: 1 });
|
||||
expect(broadcast).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('wraps a thrown Error in the provider-specific 400 (Google)', async () => {
|
||||
const s = svc({ importGoogleList: vi.fn().mockRejectedValue(new Error('network down')) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importGoogle(user, '5', 'http://x'))).toEqual({
|
||||
status: 400, body: { error: 'Failed to import Google Maps list. Make sure the list is shared publicly.' },
|
||||
});
|
||||
});
|
||||
it('wraps a non-Error rejection in the provider-specific 400 (Naver)', async () => {
|
||||
const s = svc({ importNaverList: vi.fn().mockRejectedValue('weird') } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(s).importNaver(user, '5', 'http://x'))).toEqual({
|
||||
status: 400, body: { error: 'Failed to import Naver Maps list. Make sure the list is shared publicly.' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /bulk-delete', () => {
|
||||
@@ -117,8 +190,10 @@ describe('PlacesController (parity with the legacy /api/trips/:tripId/places rou
|
||||
});
|
||||
});
|
||||
|
||||
it('GET /:id 404 when missing', () => {
|
||||
it('GET /:id returns the place when found, 404 when missing', () => {
|
||||
expect(thrown(() => new PlacesController(svc({ get: vi.fn().mockReturnValue(undefined) } as Partial<PlacesService>)).get(user, '5', '9'))).toEqual({ status: 404, body: { error: 'Place not found' } });
|
||||
const s = svc({ get: vi.fn().mockReturnValue({ id: 9 }) } as Partial<PlacesService>);
|
||||
expect(new PlacesController(s).get(user, '5', '9')).toEqual({ place: { id: 9 } });
|
||||
});
|
||||
|
||||
it('PUT /:id 404 when missing, else updates + hooks', () => {
|
||||
@@ -143,4 +218,11 @@ describe('PlacesController (parity with the legacy /api/trips/:tripId/places rou
|
||||
const e = svc({ searchImage: vi.fn().mockResolvedValue({ error: 'No key', status: 400 }) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(e).image(user, '5', '9'))).toEqual({ status: 400, body: { error: 'No key' } });
|
||||
});
|
||||
|
||||
it('GET /:id/image turns an unexpected throw into a 500, but re-throws an HttpException as-is', async () => {
|
||||
const boom = svc({ searchImage: vi.fn().mockRejectedValue(new Error('Unsplash down')) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(boom).image(user, '5', '9'))).toEqual({ status: 500, body: { error: 'Error searching for image' } });
|
||||
const http = svc({ searchImage: vi.fn().mockRejectedValue(new HttpException({ error: 'rate limited' }, 429)) } as Partial<PlacesService>);
|
||||
expect(await thrownAsync(() => new PlacesController(http).image(user, '5', '9'))).toEqual({ status: 429, body: { error: 'rate limited' } });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user