Files
TREK/server/tests/unit/plugins/rpc-coverage.test.ts
Maurice 661777f81f feat(plugins): let a plugin attach a photo to a journal entry
A journal importer can create the journey, create its entries and write their
stories, and then it stops: there was no way to give an entry a picture. The
seven journal.* methods cover text and nothing else, so an importer reading an
export archive could only upload its photos as trip files, which live on a trip
and never reach the journey's gallery.

journal.addEntryPhoto takes the bytes. Deliberately not a link to an existing
gallery photo and not a provider asset: an importer holding an archive has
neither, it has a file it just unpacked.

The access decision stays where it already was. addPhoto answers null unless the
acting user may edit the journey the entry belongs to, and null becomes
RESOURCE_FORBIDDEN here, so a plugin reaches exactly the entries its acting user
does. Everything the REST upload refuses, this refuses too: images only and no
SVG, the operator's allowed-file-types setting, 10MB decoded, demo mode. The
stored filename is the host's, never the plugin's, because the gallery dedupes
on file_path. The object is written before any row points at it and removed
again when the link is refused.

JournalRpc moves into its own module for this. It now needs StorageService and
the EXIF backfill out of MemoriesModule, and JourneyDomainModule exists
precisely so places, assignments and the plugin host can reach a few journey
functions without dragging both photo providers into their graphs.

Discussion #1365
2026-08-28 18:04:25 +02:00

78 lines
3.7 KiB
TypeScript

/**
* The coverage ledger: the anchor for the whole decorator rollout.
*
* Every wire method is owned by exactly one decorated *.rpc.ts class in the registry.
* Both sets are derived by construction, so no list is maintained by hand and the test
* cannot drift.
*
* The rollout is finished, so the legacy set is empty and the ledger's job flips from
* "how far along is the move" to "nothing fell out of it": a method deleted without a
* replacement, or a new *.rpc.ts class that no one listed in allRpcControllers().
*/
import { describe, it, expect } from 'vitest';
import { PluginRpcHost } from '../../../src/nest/plugins/host/rpc-host';
import { createTestPluginRegistry } from '../../../src/nest/plugins/host/rpc-kit/testing';
import { PluginRpcRegistryService } from '../../../src/nest/plugins/host/rpc-kit/registry.service';
import {
KNOWN_METHODS,
KNOWN_PERMISSIONS,
UNCONDITIONAL_METHODS,
} from '../../../src/nest/plugins/protocol/envelope';
import { allRpcControllers, makeDeps } from '../../helpers/rpc-host-deps';
/** Reads the router's private dispatch map. That map IS the thing under test. */
const boundMethods = (host: PluginRpcHost): Set<string> =>
new Set((host as unknown as { methods: Map<string, unknown> }).methods.keys());
describe('plugin RPC coverage ledger', () => {
const registry = createTestPluginRegistry(allRpcControllers());
// Every permission granted, so the router registers everything it can.
const host = new PluginRpcHost('probe', new Set<string>(KNOWN_PERMISSIONS), makeDeps(), registry);
const all = boundMethods(host);
const fromRegistry = registry.methodNames();
const vocabulary = new Set<string>([...KNOWN_METHODS, ...UNCONDITIONAL_METHODS]);
it('RPCLEDGER-001 every bound method comes from the registry, none from anywhere else', () => {
expect([...all].filter((m) => !fromRegistry.has(m))).toEqual([]);
});
it('RPCLEDGER-002 every registered method is one the wire protocol knows', () => {
expect([...all].filter((m) => !vocabulary.has(m))).toEqual([]);
});
it('RPCLEDGER-003 no method in the vocabulary is orphaned', () => {
expect([...vocabulary].filter((m) => !all.has(m))).toEqual([]);
});
it('RPCLEDGER-004 the registry covers the vocabulary exactly', () => {
expect(all).toEqual(vocabulary);
expect(fromRegistry.size).toBe(KNOWN_METHODS.length + UNCONDITIONAL_METHODS.length);
});
it('RPCLEDGER-005 the surface is the 114 methods the protocol declares', () => {
// Pinned rather than derived: a method DELETED from KNOWN_METHODS together with
// its handler would keep every test above green, and this is the line that makes
// that show up in the diff.
expect(KNOWN_METHODS.length).toBe(111);
expect(UNCONDITIONAL_METHODS.length).toBe(3);
expect(fromRegistry.size).toBe(114);
});
it('RPCLEDGER-006 boot-time total coverage is armed in production', () => {
// The rollout's closing move: the registry service now FAILS APP BOOT when a
// KNOWN_METHOD has no decorated owner, instead of letting it surface as a runtime
// PERMISSION_DENIED nobody can explain.
const options = (new PluginRpcRegistryService({} as never, {} as never) as unknown as {
options: { requireTotalCoverage?: boolean };
}).options;
expect(options.requireTotalCoverage).toBe(true);
});
it('RPCLEDGER-007 a registry missing a method fails validate() rather than booting', () => {
// Drops TagsRpc, a method owner. Dropping the last entry would drop PluginHooks
// instead, and the failure would be about hooks rather than methods.
const short = allRpcControllers().slice(1);
expect(() => createTestPluginRegistry(short, { requireTotalCoverage: true })).toThrow(/has no handler/);
});
});