mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
8b6a65023f
Brings the native apps branch up to dev (NestJS-only server, MikroORM
repositories, tracked sessions, per-domain locales, the lint ratchets) and
ports its new code onto that architecture:
- server: /api/auth/native moves onto injected services. The session is
minted by the auth domain's new LoginSessionService (login bookkeeping and
SessionsService.issue in one transaction), the one-time codes live behind a
NativeHandoffStore port with a capped in-memory implementation that drops
expired codes on write instead of running a timer, both routes declare
response contracts, and the exchange answers { success: true }. Unit and
e2e suites run on createTestModule and the ORM factories.
- shared: native strings for az, et, sk and th, native wired into every
locale, the address placeholder as a full URL.
- client: the account and export changes go into dev's shared hooks
(useAccountSettings, useTripExport), the native server card is a
desktop/phone pair on useNativeServer, the hand-off call moves out of the
page hook into native/, best-effort native failures go through
reportError instead of the console, and the address screen is known to
knip and lint:i18n-keys.
- package-lock.json regenerated on Linux.
194 lines
7.8 KiB
TypeScript
194 lines
7.8 KiB
TypeScript
import { ADDON_IDS } from '../../../src/addons';
|
|
import { MaintenanceRepository } from '../../../src/db/repositories/MaintenanceRepository';
|
|
import { BookingImportModule } from '../../../src/nest/booking-import/booking-import.module';
|
|
import { KitineraryExtractorModule } from '../../../src/nest/booking-import/kitinerary-extractor.module';
|
|
import { KitineraryExtractorService } from '../../../src/nest/booking-import/kitinerary-extractor.service';
|
|
import { MaintenanceModule } from '../../../src/nest/database/maintenance.module';
|
|
import { FeaturesController } from '../../../src/nest/health/features.controller';
|
|
import { HealthModule } from '../../../src/nest/health/health.module';
|
|
import { ReadinessService } from '../../../src/nest/health/readiness.service';
|
|
import { expectRegisteredController, expectRegisteredProvider } from '../../helpers/module-providers';
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
type Extractor = Pick<KitineraryExtractorService, 'isAvailable'>;
|
|
type Addons = { isAddonEnabled: (id: string) => boolean };
|
|
|
|
function make(available: boolean, aiEnabled: boolean) {
|
|
const extractor = { isAvailable: vi.fn(() => available) };
|
|
const addons = { isAddonEnabled: vi.fn(() => aiEnabled) };
|
|
return {
|
|
extractor,
|
|
addons,
|
|
controller: new FeaturesController(
|
|
extractor as Extractor as KitineraryExtractorService,
|
|
addons as never,
|
|
{} as never,
|
|
new ReadinessService(),
|
|
),
|
|
};
|
|
}
|
|
|
|
describe('FeaturesController (GET /api/health/features)', () => {
|
|
it('FEAT-001: reports both flags on', async () => {
|
|
const { controller } = make(true, true);
|
|
expect(await controller.features()).toEqual({
|
|
bookingImport: true,
|
|
aiParsing: true,
|
|
aiAssistant: true,
|
|
nativeApp: true,
|
|
});
|
|
});
|
|
|
|
it('FEAT-002: reports both flags off', async () => {
|
|
const { controller } = make(false, false);
|
|
expect(await controller.features()).toEqual({
|
|
bookingImport: false,
|
|
aiParsing: false,
|
|
aiAssistant: false,
|
|
nativeApp: true,
|
|
});
|
|
});
|
|
|
|
it('FEAT-003: the two flags are independent', async () => {
|
|
expect(await make(true, false).controller.features()).toEqual({
|
|
bookingImport: true,
|
|
aiParsing: false,
|
|
aiAssistant: false,
|
|
nativeApp: true,
|
|
});
|
|
expect(await make(false, true).controller.features()).toEqual({
|
|
bookingImport: false,
|
|
aiParsing: true,
|
|
aiAssistant: true,
|
|
nativeApp: true,
|
|
});
|
|
});
|
|
|
|
it('FEAT-004: aiParsing asks the addons service for the LLM parsing addon specifically', async () => {
|
|
const { controller, addons } = make(true, true);
|
|
await controller.features();
|
|
expect(addons.isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.LLM_PARSING);
|
|
});
|
|
|
|
it('FEAT-013: aiAssistant follows its own addon, and is off on a managed install whatever the addon says', async () => {
|
|
const { controller, addons } = make(true, true);
|
|
await controller.features();
|
|
expect(addons.isAddonEnabled).toHaveBeenCalledWith(ADDON_IDS.AI_ASSISTANT);
|
|
vi.stubEnv('TREK_MANAGED', 'true');
|
|
try {
|
|
expect((await controller.features()).aiAssistant).toBe(false);
|
|
} finally {
|
|
vi.unstubAllEnvs();
|
|
}
|
|
});
|
|
|
|
it('FEAT-014: always tells the native apps this server speaks their sign-in hand-off', async () => {
|
|
expect((await make(false, false).controller.features()).nativeApp).toBe(true);
|
|
});
|
|
|
|
it('FEAT-005: bookingImport is read live, not cached at construction', async () => {
|
|
const { controller, extractor } = make(false, false);
|
|
await controller.features();
|
|
extractor.isAvailable.mockReturnValue(true);
|
|
expect((await controller.features()).bookingImport).toBe(true);
|
|
expect(extractor.isAvailable).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('FEAT-006: the route lives on the health module, not on booking-import', () => {
|
|
expectRegisteredController(HealthModule, FeaturesController);
|
|
const bookingControllers = Reflect.getMetadata('controllers', BookingImportModule) as unknown[];
|
|
expect(bookingControllers).not.toContain(FeaturesController);
|
|
});
|
|
|
|
it('FEAT-007: the extractor is reachable from health without importing the booking-import domain', () => {
|
|
// The whole point of the leaf module: health depends on the probe, not on
|
|
// llm-parse/reservations/budget/maps/places behind BookingImportModule.
|
|
expectRegisteredProvider(KitineraryExtractorModule, KitineraryExtractorService);
|
|
const healthImports = Reflect.getMetadata('imports', HealthModule) as unknown[];
|
|
expect(Array.isArray(healthImports)).toBe(true);
|
|
expect(healthImports).toEqual(expect.arrayContaining([KitineraryExtractorModule, MaintenanceModule]));
|
|
expect(healthImports).not.toContain(BookingImportModule);
|
|
});
|
|
|
|
it('FEAT-008: the controller keeps its public path', () => {
|
|
expect(Reflect.getMetadata('path', FeaturesController)).toBe('api/health');
|
|
expect(Reflect.getMetadata('path', FeaturesController.prototype.features)).toBe('features');
|
|
});
|
|
|
|
it('FEAT-009: GET /api/health writes the exact probe bytes (legacy parity)', () => {
|
|
const { controller } = make(true, true);
|
|
const res = {
|
|
headers: {} as Record<string, string>,
|
|
body: undefined as unknown,
|
|
setHeader: vi.fn(function (this: { headers: Record<string, string> }, k: string, v: string) {
|
|
this.headers[k] = v;
|
|
}),
|
|
json: vi.fn(function (this: { body: unknown }, b: unknown) {
|
|
this.body = b;
|
|
}),
|
|
};
|
|
controller.health(res as never);
|
|
expect(res.headers['Cache-Control']).toBe('no-store, must-revalidate');
|
|
expect(res.body).toEqual({ status: 'ok' });
|
|
});
|
|
|
|
describe('the readiness probe (GET /api/health/ready)', () => {
|
|
function response() {
|
|
const res = { setHeader: vi.fn(), status: vi.fn(), json: vi.fn() };
|
|
res.status.mockReturnValue(res);
|
|
return res;
|
|
}
|
|
|
|
it('FEAT-010: answers ready while the database answers, and 503 when it does not', async () => {
|
|
const execute = vi.fn().mockResolvedValue([]);
|
|
const em = { getConnection: () => ({ execute }), getContext: () => em };
|
|
const extractor = { isAvailable: vi.fn(() => true) };
|
|
const controller = new FeaturesController(
|
|
extractor as Extractor as KitineraryExtractorService,
|
|
{} as never,
|
|
new MaintenanceRepository(em as never),
|
|
new ReadinessService(),
|
|
);
|
|
|
|
const ok = response();
|
|
await controller.ready(ok as never);
|
|
expect(execute).toHaveBeenCalledWith('SELECT 1');
|
|
expect(ok.json).toHaveBeenCalledWith({ status: 'ready' });
|
|
expect(ok.status).not.toHaveBeenCalled();
|
|
|
|
execute.mockRejectedValueOnce(new Error('connection is not available (restore in progress?)'));
|
|
const down = response();
|
|
await controller.ready(down as never);
|
|
expect(down.status).toHaveBeenCalledWith(503);
|
|
expect(down.json).toHaveBeenCalledWith({ status: 'unavailable' });
|
|
});
|
|
|
|
it('FEAT-011: answers 503 once a shutdown has started, without asking the database', async () => {
|
|
const execute = vi.fn().mockResolvedValue([]);
|
|
const em = { getConnection: () => ({ execute }), getContext: () => em };
|
|
const readiness = new ReadinessService();
|
|
const controller = new FeaturesController(
|
|
{} as never,
|
|
{} as never,
|
|
new MaintenanceRepository(em as never),
|
|
readiness,
|
|
);
|
|
|
|
readiness.markDraining();
|
|
const draining = response();
|
|
await controller.ready(draining as never);
|
|
expect(draining.status).toHaveBeenCalledWith(503);
|
|
expect(draining.json).toHaveBeenCalledWith({ status: 'unavailable' });
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FEAT-012: Nest closing the app marks the process as draining too', () => {
|
|
const readiness = new ReadinessService();
|
|
expect(readiness.isDraining()).toBe(false);
|
|
readiness.beforeApplicationShutdown();
|
|
expect(readiness.isDraining()).toBe(true);
|
|
});
|
|
});
|
|
});
|