Spaces:
Runtime error
Runtime error
File size: 7,671 Bytes
46252cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | import { DataSource } from 'typeorm';
import { PluginInstance } from './entities/plugin-instance.entity';
import { PluginInstanceService, InstanceExistsError } from './plugin-instance.service';
import { AddIntegrationFabric1781900000000 } from '../../database/migrations/1781900000000-AddIntegrationFabric';
import type { PluginConfigSchema } from '../../core/plugins/plugin.interfaces';
describe('PluginInstanceService', () => {
let ds: DataSource;
let service: PluginInstanceService;
beforeEach(async () => {
ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [PluginInstance], migrations: [] });
await ds.initialize();
const runner = ds.createQueryRunner();
await new AddIntegrationFabric1781900000000().up(runner);
await runner.release();
service = new PluginInstanceService(ds.getRepository(PluginInstance));
});
afterEach(async () => {
if (ds.isInitialized) await ds.destroy();
});
it('mints a 64-hex-char secret and stores a composite id', async () => {
const inst = await service.mint('chatwoot', 'acct1', { sessionScope: 'sess-1' });
expect(inst.id).toBe('chatwoot:acct1');
expect(inst.secret).toMatch(/^[0-9a-f]{64}$/);
});
it('masks the secret on the operator-facing view', async () => {
const inst = await service.mint('chatwoot', 'acct1', {});
expect(service.maskedView(inst).secret).toBe('***');
});
it('masks secret:true config fields (apiToken) on masked reads', () => {
const schema = {
type: 'object',
properties: { apiToken: { type: 'string', secret: true }, accountId: { type: 'number' } },
} as PluginConfigSchema;
const inst = {
id: 'p:i',
secret: 'x',
config: { apiToken: 'live-token', accountId: 3 },
} as unknown as PluginInstance;
const masked = service.maskedView(inst, schema);
const config = masked.config as Record<string, unknown>;
expect(masked.secret).toBe('***');
expect(config.apiToken).toBe('***');
expect(config.accountId).toBe(3);
});
it('masks the ENTIRE config when the schema is unavailable, e.g. the plugin is unloaded (fail-closed)', () => {
const inst = {
id: 'p:i',
secret: 'x',
config: { apiToken: 'live-token', accountId: 3 },
} as unknown as PluginInstance;
const config = service.maskedView(inst, undefined).config as Record<string, unknown>;
expect(config.apiToken).toBe('***');
expect(config.accountId).toBe('***');
});
it('resolves an existing instance and returns null for an unknown one', async () => {
await service.mint('chatwoot', 'acct1', {});
expect((await service.resolve('chatwoot', 'acct1'))?.id).toBe('chatwoot:acct1');
expect(await service.resolve('chatwoot', 'nope')).toBeNull();
});
it('accepts a valid operator secret, rejects short/empty, else auto-generates', async () => {
const ok = await service.create('chatwoot-adapter', 'a1', { secret: 'cw-secret-16chars!' });
expect(ok.secret).toBe('cw-secret-16chars!');
await expect(service.create('chatwoot-adapter', 'a2', { secret: ' ' })).rejects.toThrow(/secret/i);
await expect(service.create('chatwoot-adapter', 'a3', { secret: 'short' })).rejects.toThrow(/16/);
const gen = await service.create('chatwoot-adapter', 'a4', {});
expect(gen.secret).toMatch(/^[0-9a-f]{64}$/);
});
it('masks a NESTED secret:true config field on masked reads (recursive redaction)', () => {
const schema = {
type: 'object',
properties: {
provider: {
type: 'object',
properties: { apiToken: { type: 'string', secret: true }, region: { type: 'string' } },
},
},
} as PluginConfigSchema;
const inst = {
id: 'p:i',
secret: 'x',
config: { provider: { apiToken: 'live-token', region: 'us' } },
} as unknown as PluginInstance;
const config = service.maskedView(inst, schema).config as { provider: Record<string, unknown> };
expect(config.provider.apiToken).toBe('***');
expect(config.provider.region).toBe('us');
});
it('update restores a masked (sentinel) secret to the stored value instead of persisting "***"', async () => {
const schema = {
type: 'object',
properties: { apiToken: { type: 'string', secret: true }, accountId: { type: 'number' } },
} as PluginConfigSchema;
await service.create('chatwoot', 'acct1', { config: { apiToken: 'real-token', accountId: 1 } });
// Dashboard round-trips the masked config back with an edited non-secret field.
const updated = await service.update('chatwoot', 'acct1', { config: { apiToken: '***', accountId: 2 } }, schema);
expect(updated?.config).toEqual({ apiToken: 'real-token', accountId: 2 });
});
});
describe('PluginInstanceService provisioning', () => {
let ds: DataSource;
let service: PluginInstanceService;
beforeEach(async () => {
ds = new DataSource({ type: 'better-sqlite3', database: ':memory:', entities: [PluginInstance], migrations: [] });
await ds.initialize();
const runner = ds.createQueryRunner();
await new AddIntegrationFabric1781900000000().up(runner);
await runner.release();
service = new PluginInstanceService(ds.getRepository(PluginInstance));
});
afterEach(async () => {
if (ds.isInitialized) await ds.destroy();
});
it('create mints a new instance and rejects a duplicate with InstanceExistsError', async () => {
const inst = await service.create('chatwoot', 'acct1', { sessionScope: 'sess-1' });
expect(inst.id).toBe('chatwoot:acct1');
expect(inst.secret).toMatch(/^[0-9a-f]{64}$/);
await expect(service.create('chatwoot', 'acct1', {})).rejects.toBeInstanceOf(InstanceExistsError);
});
it('list returns all instances for a plugin', async () => {
await service.create('chatwoot', 'acct1', {});
await service.create('chatwoot', 'acct2', {});
await service.create('other', 'x', {});
const list = await service.list('chatwoot');
expect(list.map(i => i.instanceId).sort()).toEqual(['acct1', 'acct2']);
});
it('regenerateSecret replaces the secret with a new value', async () => {
const created = await service.create('chatwoot', 'acct1', {});
const rotated = await service.regenerateSecret('chatwoot', 'acct1');
expect(rotated.secret).toMatch(/^[0-9a-f]{64}$/);
expect(rotated.secret).not.toBe(created.secret);
});
it('setEnabled toggles enabled; update patches scope/config; remove deletes', async () => {
await service.create('chatwoot', 'acct1', { sessionScope: 'a' });
expect((await service.setEnabled('chatwoot', 'acct1', false))?.enabled).toBe(false);
const patched = await service.update('chatwoot', 'acct1', { sessionScope: 'b', config: { k: 1 } });
expect(patched?.sessionScope).toBe('b');
expect(patched?.config).toEqual({ k: 1 });
expect(await service.remove('chatwoot', 'acct1')).toBe(true);
expect(await service.resolve('chatwoot', 'acct1')).toBeNull();
expect(await service.remove('chatwoot', 'acct1')).toBe(false);
});
it('normalizes an empty sessionScope to null (all-sessions) on mint/create/update, never a literal ""', async () => {
// '' would break outbound send (falsy sessionId) and be unrecoverable from the UI; store null instead.
const minted = await service.mint('chatwoot', 'm1', { sessionScope: '' });
expect(minted.sessionScope).toBeNull();
const created = await service.create('chatwoot', 'acct1', { sessionScope: '' });
expect(created.sessionScope).toBeNull();
const patched = await service.update('chatwoot', 'acct1', { sessionScope: '' });
expect(patched?.sessionScope).toBeNull();
});
});
|