mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 14:06:27 +00:00
refactor(addons): addon catalog and photo providers through repositories
This commit is contained in:
@@ -30,7 +30,7 @@ import { PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.s
|
||||
import { createPluginRuntime } from '../../helpers/plugin-host';
|
||||
import { parseManifest, ManifestError } from '../../../src/nest/plugins/install/manifest';
|
||||
import { makeHostAllow } from '../../../src/nest/plugins/runtime/egress-policy';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
|
||||
function install(id: string, operatorEgress: boolean, perms: string[] = ['http:outbound:gotify.net']) {
|
||||
testDb.prepare(
|
||||
@@ -174,7 +174,7 @@ describe('the admin list surfaces operator egress (so the chip can be shown)', (
|
||||
// one over the same DB. These fixtures declare no dependencies, so it is never consulted.
|
||||
const listPlugins = async () => {
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
return (await new PluginsService(dbs, new AddonsService(dbs)).list()).plugins;
|
||||
return (await new PluginsService(dbs, await createTestAddonsService(testDb, dbs)).list()).plugins;
|
||||
};
|
||||
|
||||
const before = await listPlugins();
|
||||
|
||||
@@ -30,7 +30,8 @@ import { PluginsController } from '../../../src/nest/plugins/plugins.controller'
|
||||
import { PluginConsentRequired, type PluginRuntimeService } from '../../../src/nest/plugins/plugin-runtime.service';
|
||||
import type { PluginRegistryService } from '../../../src/nest/plugins/registry/registry.service';
|
||||
import type { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import type { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
import { createPluginRuntime } from '../../helpers/plugin-host';
|
||||
import { discoverPlugins } from '../../../src/nest/plugins/install/discovery';
|
||||
|
||||
@@ -52,10 +53,8 @@ function declareField(pluginId: string, key: string, scope: 'instance' | 'user',
|
||||
.run(pluginId, key, key, opts.required ? 1 : 0, opts.secret ? 1 : 0, scope, opts.options ?? null, opts.sortOrder ?? 0);
|
||||
}
|
||||
|
||||
const svc = () => {
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
return new PluginsService(dbs, new AddonsService(dbs));
|
||||
};
|
||||
let addonsService: AddonsService;
|
||||
const svc = () => new PluginsService(new DatabaseService(dbConn), addonsService);
|
||||
|
||||
/**
|
||||
* Installs a real plugin through the manifest -> discovery pipeline (not a direct
|
||||
@@ -74,9 +73,10 @@ async function installFixturePlugin(opts: { settings: Array<Record<string, unkno
|
||||
await discoverPlugins(testDb);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
addonsService = await createTestAddonsService(testDb, new DatabaseService(dbConn));
|
||||
});
|
||||
beforeEach(() => {
|
||||
testDb.prepare('DELETE FROM plugins').run();
|
||||
|
||||
@@ -17,7 +17,7 @@ const { getDb } = vi.hoisted(() => ({ getDb: { current: null as unknown } }));
|
||||
vi.mock('../../../src/db/database', () => ({ get db() { return getDb.current; } }));
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { PluginsService } from '../../../src/nest/plugins/plugins.service';
|
||||
@@ -43,10 +43,10 @@ describe('per-user plugin settings', () => {
|
||||
let svc: PluginsService;
|
||||
// AddonsService only feeds PluginsService.list(), which no case here calls, but it is a
|
||||
// real collaborator on the same connection rather than a stand-in.
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
getDb.current = freshDb();
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
svc = new PluginsService(dbs, new AddonsService(dbs));
|
||||
svc = new PluginsService(dbs, await createTestAddonsService(getDb.current as Database.Database, dbs));
|
||||
});
|
||||
|
||||
it('lists only the user-scope fields, in order', async () => {
|
||||
@@ -95,10 +95,10 @@ describe('manifest defaults reach the runtime reads', () => {
|
||||
// through ctx.settings.get() / the channel dispatch's readAll — otherwise a plugin
|
||||
// that ships a sensible default serves nobody until every user opens the form.
|
||||
let svc: PluginsService;
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
getDb.current = freshDb();
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
svc = new PluginsService(dbs, new AddonsService(dbs));
|
||||
svc = new PluginsService(dbs, await createTestAddonsService(getDb.current as Database.Database, dbs));
|
||||
const ins = (getDb.current as import('better-sqlite3').Database).prepare(
|
||||
'INSERT INTO plugin_settings_fields (plugin_id, field_key, input_type, required, secret, scope, sort_order, default_value) VALUES (?,?,?,?,?,?,?,?)',
|
||||
);
|
||||
@@ -138,10 +138,10 @@ describe('hasRequired applies the same "filled" rule as the save gate', () => {
|
||||
// it disagreed with assertRequiredFilled() a save the form accepted could still leave
|
||||
// the user "not configured" (or the reverse).
|
||||
let svc: PluginsService;
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
getDb.current = freshDb();
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
svc = new PluginsService(dbs, new AddonsService(dbs));
|
||||
svc = new PluginsService(dbs, await createTestAddonsService(getDb.current as Database.Database, dbs));
|
||||
(getDb.current as import('better-sqlite3').Database)
|
||||
.prepare('INSERT INTO plugin_settings_fields (plugin_id, field_key, input_type, required, secret, scope, sort_order) VALUES (?,?,?,?,?,?,?)')
|
||||
.run('p', 'consent', 'checkbox', 1, 0, 'user', 9);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* The read-side plugin service + controller (#plugins, M0). Lists installed
|
||||
* plugins and reports whether the runtime is enabled (TREK_PLUGINS_ENABLED).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
const { testDb } = vi.hoisted(() => {
|
||||
const Database = require('better-sqlite3');
|
||||
@@ -20,12 +20,21 @@ vi.mock('../../../src/db/database', () => ({ db: testDb }));
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import type { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
import { PluginsService } from '../../../src/nest/plugins/plugins.service';
|
||||
import { PluginsController } from '../../../src/nest/plugins/plugins.controller';
|
||||
import { PluginsFeedController } from '../../../src/nest/plugins/plugins-feed.controller';
|
||||
import type { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
|
||||
|
||||
// AddonsService only feeds PluginsService.list()'s required-addon-dependency
|
||||
// resolution; none of the fixtures below declare any, so it is built once
|
||||
// (over the same connection) rather than reconstructed at every call site.
|
||||
let addonsService: AddonsService;
|
||||
beforeAll(async () => {
|
||||
addonsService = await createTestAddonsService(testDb, new DatabaseService(dbConn));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testDb.exec('DELETE FROM plugins');
|
||||
testDb.exec('DELETE FROM plugin_settings_fields');
|
||||
@@ -42,7 +51,7 @@ describe('PluginsService.list', () => {
|
||||
.run('flight', 'Flight', 'desc', 'widget', 'inactive', '1.0.0');
|
||||
process.env.TREK_PLUGINS_ENABLED = 'true';
|
||||
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.enabled).toBe(true);
|
||||
expect(out.plugins).toHaveLength(1);
|
||||
expect(out.plugins[0]).toMatchObject({ id: 'flight', name: 'Flight', status: 'inactive' });
|
||||
@@ -63,7 +72,7 @@ describe('PluginsService.list', () => {
|
||||
it('reports the switch off and an outgrown plugin as hostIncompatible by default', async () => {
|
||||
process.env.APP_VERSION = '4.1.0';
|
||||
seed();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.ignoreTrekRange).toBe(false);
|
||||
expect(out.plugins[0]).toMatchObject({ dependencyStatus: 'hostIncompatible', trekRangeBypassed: null });
|
||||
});
|
||||
@@ -72,7 +81,7 @@ describe('PluginsService.list', () => {
|
||||
process.env.APP_VERSION = '4.1.0';
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
seed();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.ignoreTrekRange).toBe(true);
|
||||
expect(out.plugins[0]).toMatchObject({
|
||||
dependencyStatus: 'ok',
|
||||
@@ -84,7 +93,7 @@ describe('PluginsService.list', () => {
|
||||
process.env.APP_VERSION = '3.5.0';
|
||||
process.env.TREK_PLUGINS_IGNORE_TREK_RANGE = '1';
|
||||
seed();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.plugins[0]).toMatchObject({ dependencyStatus: 'ok', trekRangeBypassed: null });
|
||||
});
|
||||
});
|
||||
@@ -97,7 +106,7 @@ describe('PluginsService.list', () => {
|
||||
.prepare("INSERT INTO plugins (id, name, type, status, version) VALUES ('free','Free','widget','inactive','1.0.0')")
|
||||
.run();
|
||||
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.plugins.find((p) => p.id === 'held')).toMatchObject({ updateHold: true });
|
||||
expect(out.plugins.find((p) => p.id === 'free')).toMatchObject({ updateHold: false });
|
||||
});
|
||||
@@ -106,7 +115,7 @@ describe('PluginsService.list', () => {
|
||||
testDb
|
||||
.prepare("INSERT INTO plugins (id, name, type, status, version, update_hold) VALUES ('held','Held','widget','inactive','1.0.0',1)")
|
||||
.run();
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn)));
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), addonsService);
|
||||
|
||||
expect(await svc.resumeUpdates('held')).toBe(true);
|
||||
expect(testDb.prepare("SELECT update_hold FROM plugins WHERE id='held'").get()).toMatchObject({ update_hold: 0 });
|
||||
@@ -118,14 +127,14 @@ describe('PluginsService.list', () => {
|
||||
.prepare('INSERT INTO plugins (id, name, description, type, status, version) VALUES (?,?,?,?,?,?)')
|
||||
.run('flight', 'Flight', 'desc', 'widget', 'inactive', '1.0.0');
|
||||
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.enabled).toBe(true);
|
||||
expect(out.plugins).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reports disabled when the kill switch is off (TREK_PLUGINS_ENABLED=false)', async () => {
|
||||
process.env.TREK_PLUGINS_ENABLED = 'false';
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list();
|
||||
const out = await new PluginsService(new DatabaseService(dbConn), addonsService).list();
|
||||
expect(out.enabled).toBe(false);
|
||||
expect(out.plugins).toEqual([]);
|
||||
});
|
||||
@@ -145,7 +154,7 @@ describe('PluginsService.list', () => {
|
||||
const key = 'RWTvBn0aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789abcd';
|
||||
insert('signed-one', 'acme/signed-one', key);
|
||||
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list()).plugins[0];
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), addonsService).list()).plugins[0];
|
||||
expect(p.signed).toBe(true);
|
||||
// Short head…tail, for eyeballing against what the author reads out over the
|
||||
// phone. NOT a confidentiality measure — the key is public, and the re-trust
|
||||
@@ -155,7 +164,7 @@ describe('PluginsService.list', () => {
|
||||
|
||||
it('reports unsigned for a registry plugin with no pinned key', async () => {
|
||||
insert('plain', 'acme/plain', null);
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list()).plugins[0];
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), addonsService).list()).plugins[0];
|
||||
expect(p.signed).toBe(false);
|
||||
expect(p.keyFingerprint).toBeNull();
|
||||
});
|
||||
@@ -163,7 +172,7 @@ describe('PluginsService.list', () => {
|
||||
it('reports unsigned for a sideloaded and a dev-linked plugin (they carry no key)', async () => {
|
||||
insert('uploaded', 'local:upload', null);
|
||||
insert('linked', 'local:link', null);
|
||||
const plugins = (await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list()).plugins;
|
||||
const plugins = (await new PluginsService(new DatabaseService(dbConn), addonsService).list()).plugins;
|
||||
expect(plugins.map((p) => [p.id, p.signed, p.source_repo])).toEqual([
|
||||
['linked', false, 'local:link'],
|
||||
['uploaded', false, 'local:upload'],
|
||||
@@ -177,7 +186,7 @@ describe('PluginsService.list', () => {
|
||||
.prepare('UPDATE plugins SET update_block_code = ?, update_block_detail = ?, update_block_version = ? WHERE id = ?')
|
||||
.run('SIGNATURE_KEY_CHANGED', 'the key changed', '2.0.0', 'blocked');
|
||||
|
||||
const byId = Object.fromEntries((await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list()).plugins.map((p) => [p.id, p]));
|
||||
const byId = Object.fromEntries((await new PluginsService(new DatabaseService(dbConn), addonsService).list()).plugins.map((p) => [p.id, p]));
|
||||
expect(byId.blocked.updateBlock).toEqual({ code: 'SIGNATURE_KEY_CHANGED', detail: 'the key changed', version: '2.0.0' });
|
||||
expect(byId.fine.updateBlock).toBeNull();
|
||||
});
|
||||
@@ -187,7 +196,7 @@ describe('PluginsService.list', () => {
|
||||
// PluginListItem is an interface, so it has no implicit index signature and cannot
|
||||
// be narrowed to a record directly. Widening through unknown is what lets this case
|
||||
// probe for a key the contract deliberately does not declare.
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).list())
|
||||
const p = (await new PluginsService(new DatabaseService(dbConn), addonsService).list())
|
||||
.plugins[0] as unknown as Record<string, unknown>;
|
||||
expect(p.author_pubkey).toBeUndefined();
|
||||
});
|
||||
@@ -314,7 +323,7 @@ describe('PluginsService instance config', () => {
|
||||
testDb.prepare("INSERT INTO plugin_settings_fields (plugin_id, field_key, scope, secret) VALUES ('x','api_key','instance',1)").run();
|
||||
testDb.prepare("INSERT INTO plugin_settings_fields (plugin_id, field_key, scope, secret) VALUES ('x','server','instance',0)").run();
|
||||
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn)));
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), addonsService);
|
||||
const masked = await svc.updateInstanceConfig('x', { api_key: 'super-secret', server: 'https://h' });
|
||||
// client gets the masked view
|
||||
expect(masked.api_key).toBe('••••••••');
|
||||
@@ -338,7 +347,7 @@ describe('PluginsService instance config', () => {
|
||||
testDb.prepare("INSERT INTO plugins (id, name, status, config) VALUES ('y','Y','inactive','{}')").run();
|
||||
testDb.prepare("INSERT INTO plugin_settings_fields (plugin_id, field_key, scope, secret) VALUES ('y','server','instance',0)").run();
|
||||
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn)));
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), addonsService);
|
||||
const masked = await svc.updateInstanceConfig('y', { server: 'https://h', smuggled: 'nope' });
|
||||
|
||||
expect(masked.server).toBe('https://h');
|
||||
@@ -348,8 +357,8 @@ describe('PluginsService instance config', () => {
|
||||
});
|
||||
|
||||
it('throws for an unknown plugin', async () => {
|
||||
await expect(new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).updateInstanceConfig('nope', {})).rejects.toThrow(/not found/);
|
||||
await expect(new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn))).getInstanceConfig('nope')).rejects.toThrow(/not found/);
|
||||
await expect(new PluginsService(new DatabaseService(dbConn), addonsService).updateInstanceConfig('nope', {})).rejects.toThrow(/not found/);
|
||||
await expect(new PluginsService(new DatabaseService(dbConn), addonsService).getInstanceConfig('nope')).rejects.toThrow(/not found/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -357,7 +366,7 @@ describe('PluginsService error log', () => {
|
||||
beforeEach(() => testDb.exec('DELETE FROM plugin_error_log'));
|
||||
it('lists and clears a plugin error log', async () => {
|
||||
testDb.prepare("INSERT INTO plugin_error_log (plugin_id, level, message) VALUES ('p','error','boom')").run();
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), new AddonsService(new DatabaseService(dbConn)));
|
||||
const svc = new PluginsService(new DatabaseService(dbConn), addonsService);
|
||||
expect(await svc.errors('p')).toEqual([{ ts: '2026-01-01', level: 'error', message: 'boom' }]);
|
||||
await svc.clearErrors('p');
|
||||
expect(await svc.errors('p')).toEqual([]);
|
||||
|
||||
@@ -27,7 +27,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { createSnapshotTestDb } from '../../helpers/db-mock';
|
||||
import { createUser } from '../../helpers/factories';
|
||||
import { DatabaseService } from '../../../src/nest/database/database.service';
|
||||
import { AddonsService } from '../../../src/nest/addons/addons.service';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
import { PermissionsService } from '../../../src/nest/permissions/permissions.service';
|
||||
import { PluginGuards } from '../../../src/nest/plugins/host/plugin-guards.service';
|
||||
import { PluginSupervisor } from '../../../src/nest/plugins/supervisor/plugin-supervisor';
|
||||
@@ -64,7 +64,7 @@ beforeAll(async () => {
|
||||
t = await createTestOrm(testDb, { allowGlobalContext: false });
|
||||
const dbs = new DatabaseService(testDb);
|
||||
permissions = new PermissionsService(t.repo(AppSettings) as AppSettingsRepository, new UnitOfWork(t.em));
|
||||
guards = new PluginGuards(dbs, permissions, new AddonsService(dbs));
|
||||
guards = new PluginGuards(dbs, permissions, await createTestAddonsService(testDb, dbs));
|
||||
userId = createUser(testDb, { role: 'user' }).user.id;
|
||||
// An admin has tightened trip_create from its 'everybody' default.
|
||||
testDb.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('perm_trip_create', 'admin')").run();
|
||||
|
||||
@@ -20,7 +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';
|
||||
import { createTestAddonsService } from '../../helpers/test-addons';
|
||||
vi.mock('../../../src/config', () => ({ JWT_SECRET: 'x'.repeat(40), ENCRYPTION_KEY: 'a'.repeat(64), updateJwtSecret: () => {} }));
|
||||
|
||||
import { createTables } from '../../../src/db/schema';
|
||||
@@ -140,7 +140,7 @@ describe('a plugin channel label is bounded by the host', () => {
|
||||
const rt = new PluginRuntimeService(
|
||||
new DatabaseService(dbConn),
|
||||
new AuditService(t.repo(AuditLog) as AuditLogRepository, t.repo(Users) as UsersRepository),
|
||||
new AddonsService(new DatabaseService(dbConn)),
|
||||
await createTestAddonsService(dbConn),
|
||||
userSettings(),
|
||||
);
|
||||
// Stand the plugin up as a granted, active notificationChannel provider.
|
||||
|
||||
Reference in New Issue
Block a user