/** * 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 = {}): RpcRequest => ({ k: 'req', id: 'x', method, params, }); function build() { const categories = { list: vi.fn(() => [{ id: 1, name: 'Food' }]) } as unknown as CategoriesService & Record>; const weather = { get: vi.fn(async () => ({ temp: 20 })) } as unknown as WeatherService & Record>; const rates = { getRates: vi.fn(async (base: string) => ({ [base]: 1, USD: 1.08 })), } as unknown as ExchangeRatesService & Record>; 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); }); });