mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
124 lines
5.3 KiB
TypeScript
124 lines
5.3 KiB
TypeScript
/**
|
|
* The addon route gate.
|
|
*
|
|
* Three hand-written guards (collections, airtrail, journey) did the same thing
|
|
* and differed only in the addon id they read and the word leading the 404. They
|
|
* are one guard plus a @RequireAddon decorator now.
|
|
*
|
|
* The last describe matters as much as the first: with the addon id in a
|
|
* decorator, nothing in a controller's own tests notices if the decorator is
|
|
* dropped or points at the wrong addon. These assertions are what keeps the
|
|
* three route groups gated, and gated on the right thing.
|
|
*/
|
|
import { ADDON_IDS } from '../../../src/addons';
|
|
import { AddonGuard } from '../../../src/nest/addons/addon.guard';
|
|
import type { AddonsService } from '../../../src/nest/addons/addons.service';
|
|
import { RequireAddon, REQUIRE_ADDON, type RequireAddonMeta } from '../../../src/nest/addons/require-addon.decorator';
|
|
import { CollectionsController } from '../../../src/nest/collections/collections.controller';
|
|
import { AirtrailController } from '../../../src/nest/integrations/airtrail.controller';
|
|
import { JourneyController } from '../../../src/nest/journey/journey.controller';
|
|
import { ReservationImportController } from '../../../src/nest/reservation-import/reservation-import.controller';
|
|
import { HttpException } from '@nestjs/common';
|
|
import type { ExecutionContext } from '@nestjs/common';
|
|
import { Reflector } from '@nestjs/core';
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
/** A context whose handler/class carry the decorator under test. */
|
|
function ctxFor(handler: object, cls: object = class {}): ExecutionContext {
|
|
return {
|
|
getHandler: () => handler,
|
|
getClass: () => cls,
|
|
} as unknown as ExecutionContext;
|
|
}
|
|
|
|
function addons(enabled: boolean): AddonsService {
|
|
return { isAddonEnabled: vi.fn(() => enabled) } as unknown as AddonsService;
|
|
}
|
|
|
|
async function thrown(fn: () => unknown) {
|
|
try {
|
|
await fn();
|
|
return undefined;
|
|
} catch (err) {
|
|
return err instanceof HttpException ? { status: err.getStatus(), body: err.getResponse() } : err;
|
|
}
|
|
}
|
|
|
|
/** Apply @RequireAddon to a throwaway class the way a controller would. */
|
|
function decorated(addonId: string, label: string) {
|
|
class Gated {}
|
|
RequireAddon(addonId, label)(Gated);
|
|
return Gated;
|
|
}
|
|
|
|
describe('AddonGuard', () => {
|
|
it('ADDON-GUARD-001: 404s with the decorator label when the addon is disabled', async () => {
|
|
const cls = decorated(ADDON_IDS.JOURNEY, 'Journey');
|
|
const guard = new AddonGuard(addons(false), new Reflector());
|
|
|
|
expect(await thrown(() => guard.canActivate(ctxFor(() => {}, cls)))).toEqual({
|
|
status: 404,
|
|
body: { error: 'Journey addon is not enabled' },
|
|
});
|
|
});
|
|
|
|
it('ADDON-GUARD-002: passes when the addon is enabled, asking for the declared id', async () => {
|
|
const cls = decorated(ADDON_IDS.COLLECTIONS, 'Collections');
|
|
const svc = addons(true);
|
|
const guard = new AddonGuard(svc, new Reflector());
|
|
|
|
expect(await guard.canActivate(ctxFor(() => {}, cls))).toBe(true);
|
|
expect(svc.isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.COLLECTIONS);
|
|
});
|
|
|
|
it('ADDON-GUARD-003: is a no-op on a route that never asked for an addon', async () => {
|
|
const svc = addons(false);
|
|
const guard = new AddonGuard(svc, new Reflector());
|
|
|
|
expect(await guard.canActivate(ctxFor(() => {}, class {}))).toBe(true);
|
|
expect(svc.isAddonEnabled).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('ADDON-GUARD-004: a handler-level decorator overrides the controller-level one', async () => {
|
|
const cls = decorated(ADDON_IDS.JOURNEY, 'Journey');
|
|
const handler = () => {};
|
|
RequireAddon(ADDON_IDS.AIRTRAIL, 'AirTrail')(handler);
|
|
const svc = addons(false);
|
|
|
|
expect(await thrown(() => new AddonGuard(svc, new Reflector()).canActivate(ctxFor(handler, cls)))).toEqual({
|
|
status: 404,
|
|
body: { error: 'AirTrail addon is not enabled' },
|
|
});
|
|
expect(svc.isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.AIRTRAIL);
|
|
});
|
|
});
|
|
|
|
describe('the gated route groups still declare their addon', () => {
|
|
const metaOf = (cls: object): RequireAddonMeta | undefined => Reflect.getMetadata(REQUIRE_ADDON, cls);
|
|
const guardsOf = (cls: object): unknown[] => Reflect.getMetadata('__guards__', cls) ?? [];
|
|
|
|
it.each([
|
|
['JourneyController', JourneyController, ADDON_IDS.JOURNEY, 'Journey'],
|
|
['CollectionsController', CollectionsController, ADDON_IDS.COLLECTIONS, 'Collections'],
|
|
['AirtrailController', AirtrailController, ADDON_IDS.AIRTRAIL, 'AirTrail'],
|
|
])('ADDON-GUARD-005: %s gates on the right addon, with AddonGuard first', (_name, cls, addonId, label) => {
|
|
expect(metaOf(cls)).toEqual({ addonId, label });
|
|
// Order is the contract: a disabled addon has to answer 404 to anonymous
|
|
// callers, so the addon check must run before the 401.
|
|
expect(guardsOf(cls)[0]).toBe(AddonGuard);
|
|
});
|
|
|
|
it('ADDON-GUARD-006: the merged import controller gates AirTrail per handler, not per class', () => {
|
|
// A class-level requirement here would 404 the four file-upload routes
|
|
// whenever the AirTrail addon happens to be off. AddonGuard still runs first
|
|
// in the chain, so the 404 keeps beating the 401.
|
|
expect(metaOf(ReservationImportController)).toBeUndefined();
|
|
expect(Reflect.getMetadata(REQUIRE_ADDON, ReservationImportController.prototype.importAirtrail)).toEqual({
|
|
addonId: ADDON_IDS.AIRTRAIL,
|
|
label: 'AirTrail',
|
|
});
|
|
expect(guardsOf(ReservationImportController)[0]).toBe(AddonGuard);
|
|
});
|
|
});
|