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.
100 lines
4.7 KiB
TypeScript
100 lines
4.7 KiB
TypeScript
/**
|
|
* The three tenant-free plugin methods: weather, categories and exchange rates.
|
|
*
|
|
* They are grouped because what matters about them is the same property, and it is
|
|
* the one a migration could quietly lose: they carry no tenant data, so they work
|
|
* with NO acting user. Everything else in the plugin surface refuses that.
|
|
*/
|
|
import { createTestPluginRegistry } from '../../../src/nest-rpc/rpc-kit/testing';
|
|
import { BudgetModule } from '../../../src/nest/budget/budget.module';
|
|
import { ExchangeRatesRpc } from '../../../src/nest/budget/exchange-rates.rpc';
|
|
import type { ExchangeRatesService } from '../../../src/nest/budget/exchange-rates.service';
|
|
import { CategoriesModule } from '../../../src/nest/categories/categories.module';
|
|
import { CategoriesRpc } from '../../../src/nest/categories/categories.rpc';
|
|
import type { CategoriesService } from '../../../src/nest/categories/categories.service';
|
|
import { PluginRpcHost } from '../../../src/nest/plugins/host/rpc-host';
|
|
import type { RpcRequest, RpcError } from '../../../src/nest/plugins/protocol/envelope';
|
|
import { WeatherModule } from '../../../src/nest/weather/weather.module';
|
|
import { WeatherRpc } from '../../../src/nest/weather/weather.rpc';
|
|
import type { WeatherService } from '../../../src/nest/weather/weather.service';
|
|
import { expectRegisteredProvider } from '../../helpers/module-providers';
|
|
import { makeDeps } from '../../helpers/rpc-host-deps';
|
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
const req = (method: string, params: Record<string, unknown> = {}): RpcRequest => ({
|
|
k: 'req',
|
|
id: 'x',
|
|
method,
|
|
params,
|
|
});
|
|
|
|
function build() {
|
|
const categories = { list: vi.fn(() => [{ id: 1, name: 'Food' }]) } as unknown as CategoriesService &
|
|
Record<string, ReturnType<typeof vi.fn>>;
|
|
const weather = { get: vi.fn(async () => ({ temp: 20 })) } as unknown as WeatherService &
|
|
Record<string, ReturnType<typeof vi.fn>>;
|
|
const rates = {
|
|
getRates: vi.fn(async (base: string) => ({ [base]: 1, USD: 1.08 })),
|
|
} as unknown as ExchangeRatesService & Record<string, ReturnType<typeof vi.fn>>;
|
|
const registry = createTestPluginRegistry([
|
|
new CategoriesRpc(categories),
|
|
new WeatherRpc(weather),
|
|
new ExchangeRatesRpc(rates),
|
|
]);
|
|
const host = (...grants: string[]) => new PluginRpcHost('p', new Set(grants), makeDeps(), registry);
|
|
return { categories, weather, rates, host };
|
|
}
|
|
|
|
describe('tenant-free plugin methods', () => {
|
|
it('TENANTFREE-001 weather.get and categories.list work without a user', async () => {
|
|
const f = build();
|
|
expect((await f.host('weather:read').dispatch(req('weather.get', { lat: 48, lng: 11 }), undefined)).ok).toBe(true);
|
|
expect((await f.host('db:read:categories').dispatch(req('categories.list', {}), undefined)).ok).toBe(true);
|
|
});
|
|
|
|
it('TENANTFREE-002 rates.get works without a user and needs rates:read', async () => {
|
|
const f = build();
|
|
expect((await f.host('rates:read').dispatch(req('rates.get', { base: 'EUR' }), undefined)).ok).toBe(true);
|
|
expect(f.rates.getRates).toHaveBeenCalledWith('EUR');
|
|
const denied = (await f.host().dispatch(req('rates.get', { base: 'EUR' }), undefined)) as RpcError;
|
|
expect(denied.error.code).toBe('PERMISSION_DENIED');
|
|
});
|
|
|
|
it('TENANTFREE-003 weather coordinates are coerced, and the language stays pinned to en', async () => {
|
|
const f = build();
|
|
// Numeric strings are accepted, as the wire contract has always allowed.
|
|
await f
|
|
.host('weather:read')
|
|
.dispatch(req('weather.get', { lat: '48.1', lng: '11.5', date: '2027-01-01' }), undefined);
|
|
expect(f.weather.get).toHaveBeenCalledWith('48.1', '11.5', '2027-01-01', 'en');
|
|
});
|
|
|
|
it('TENANTFREE-004 a non-numeric coordinate is BAD_PARAMS', async () => {
|
|
const res = (await build()
|
|
.host('weather:read')
|
|
.dispatch(req('weather.get', { lat: 'north', lng: 11 }), undefined)) as RpcError;
|
|
expect(res.error.code).toBe('BAD_PARAMS');
|
|
expect(res.error.message).toBe('lat must be a number');
|
|
});
|
|
|
|
it('TENANTFREE-005 a non-string base is BAD_PARAMS', async () => {
|
|
const res = (await build()
|
|
.host('rates:read')
|
|
.dispatch(req('rates.get', { base: 42 }), undefined)) as RpcError;
|
|
expect(res.error.code).toBe('BAD_PARAMS');
|
|
});
|
|
|
|
it('TENANTFREE-006 an absent date is passed through as undefined, not as a string', async () => {
|
|
const f = build();
|
|
await f.host('weather:read').dispatch(req('weather.get', { lat: 1, lng: 2 }), undefined);
|
|
expect(f.weather.get).toHaveBeenCalledWith('1', '2', undefined, 'en');
|
|
});
|
|
|
|
it('TENANTFREE-007 each class is listed in its module providers', () => {
|
|
expectRegisteredProvider(CategoriesModule, CategoriesRpc);
|
|
expectRegisteredProvider(WeatherModule, WeatherRpc);
|
|
expectRegisteredProvider(BudgetModule, ExchangeRatesRpc);
|
|
});
|
|
});
|