refactor(server): extract addon enablement into the addons domain

This commit is contained in:
jubnl
2026-07-28 20:22:01 +02:00
parent cafff003a3
commit ee2ea1d065
64 changed files with 486 additions and 277 deletions
@@ -20,11 +20,13 @@ vi.mock('../../../src/db/database', () => ({
import { db as dbConn } from '../../../src/db/database';
import { DatabaseService } from '../../../src/nest/database/database.service';
vi.mock('../../../src/services/journeyService', () => ({ canAccessJourney }));
vi.mock('../../../src/services/adminService', () => ({ isAddonEnabled }));
vi.mock('../../../src/nest/plugins/kill-switch', () => ({ pluginsEnabled }));
import { JournalEntryRowsController } from '../../../src/nest/plugins/journal-entry-rows.controller';
import type { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service';
import type { AddonsService } from '../../../src/nest/addons/addons.service';
const addonsStub = { isAddonEnabled } as unknown as AddonsService;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const req = (id?: number) => ({ user: id === undefined ? undefined : { id } }) as any;
@@ -33,7 +35,7 @@ function controller(invoke: (id: string) => unknown, providers = ['p1']) {
providersOf: vi.fn(() => providers),
invokeHook: vi.fn(async (id: string) => invoke(id)),
} as unknown as PluginRuntimeService;
return { c: new JournalEntryRowsController(runtime, new DatabaseService(dbConn)), runtime };
return { c: new JournalEntryRowsController(runtime, new DatabaseService(dbConn), addonsStub), runtime };
}
const row = (over: Record<string, unknown> = {}) => ({ label: 'Distance', value: '12 km', ...over });
@@ -47,7 +47,6 @@ vi.mock('../../../src/db/database', () => {
vi.mock('../../../src/websocket', () => ({ broadcast, broadcastToUser }));
// Addon gate — flip per test to exercise the "addon disabled" branch of the reads.
const { isAddonEnabled } = vi.hoisted(() => ({ isAddonEnabled: vi.fn(() => true as boolean) }));
vi.mock('../../../src/services/adminService', () => ({ isAddonEnabled }));
// DI-native services are constructor-injected into PluginHostDepsFactory below —
// stub instances instead of path mocks (same behaviors as before the DI move).
const budgetStub = {
@@ -324,7 +323,8 @@ import { DatabaseService } from '../../../src/nest/database/database.service';
// The factory under test, wired exactly like PluginsModule does — but with the
// DI-native domain services replaced by the stubs above. The shim keeps the
// ~45 historical call sites unchanged and supplies a default no-op router.
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub);
const addonsStub = { isAddonEnabled } as unknown as import('../../../src/nest/addons/addons.service').AddonsService;
const factory = new PluginHostDepsFactory(budgetStub, reservationsStub, tagsStub, categoriesStub, todoStub, packingStub, oauthStub, dayNotesStub, assignmentsStub, llmConfigStub, new DatabaseService(mockDb), filesStub, collabStub, vacayStub, daysStub, permissionsStub, exchangeRatesStub, addonsStub);
const stubRouter: PluginCallRouter = { callPlugin: async () => undefined, emitPluginEvent: () => {} };
const createRealRpcHost = (id: string, granted: ReadonlySet<string>, router: PluginCallRouter = stubRouter) => factory.create(id, granted, router);
@@ -20,6 +20,7 @@ vi.mock('../../../src/db/database', () => dbMock);
import { db as dbConn } from '../../../src/db/database';
import { DatabaseService } from '../../../src/nest/database/database.service';
import { AuditService } from '../../../src/nest/audit/audit.service';
import { AddonsService } from '../../../src/nest/addons/addons.service';
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'x'.repeat(40), ENCRYPTION_KEY: 'a'.repeat(64), updateJwtSecret: () => {} }));
import { createTables } from '../../../src/db/schema';
@@ -123,7 +124,7 @@ describe('a plugin channel label is bounded by the host', () => {
VALUES ('loud', 'Loud', 'active', 1, '1.0.0', '[]', '[]', ?, '{}')`,
).run(JSON.stringify({ notificationChannel: { title: '🎉'.repeat(5) + 'A'.repeat(500) } }));
const rt = new PluginRuntimeService(new DatabaseService(dbConn), new AuditService(new DatabaseService(dbConn)));
const rt = new PluginRuntimeService(new DatabaseService(dbConn), new AuditService(new DatabaseService(dbConn)), new AddonsService(new DatabaseService(dbConn)));
// Stand the plugin up as a granted, active notificationChannel provider.
(rt as unknown as { supervisor: { running: Map<string, unknown> } }).supervisor.running.set('loud', {
id: 'loud', status: 'active', hooks: ['notificationChannel'], granted: new Set(['hook:notification-channel']),