Spaces:
Runtime error
Runtime error
File size: 6,498 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 | // archiver v8 is ESM-only; stub it so ts-jest can load the module graph.
jest.mock('archiver', () => ({ TarArchive: jest.fn() }));
// Seed the well-known dev-admin-key BEFORE AppModule is imported (mirrors mcp-auth.e2e-spec.ts's
// pattern of setting the enabling env var ahead of the import).
process.env.ALLOW_DEV_API_KEY = 'true';
process.env.BASE_URL = 'https://api.example.com';
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
import { applyGlobalValidation } from '../src/config/app-validation';
import { PluginLoaderService } from '../src/core/plugins/plugin-loader.service';
// A stub ingress-capable plugin so the capability check passes without a real plugin on disk.
const INGRESS_PLUGIN = {
manifest: { id: 'chatwoot', ingress: [{ route: 'chatwoot' }], permissions: ['webhook:ingress', 'conversation:send'] },
};
const NON_INGRESS_PLUGIN = { manifest: { id: 'plain', permissions: [] } };
interface InstanceViewBody {
secret: string;
enabled: boolean;
sessionScope: string | null;
ingressUrls: Array<{ route: string; url: string }>;
}
describe('IntegrationInstanceController (e2e)', () => {
let app: INestApplication<App>;
const key = 'dev-admin-key';
const base = '/api/integration/plugins';
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(PluginLoaderService)
.useValue({
getPlugin: (id: string) =>
id === 'chatwoot' ? INGRESS_PLUGIN : id === 'plain' ? NON_INGRESS_PLUGIN : undefined,
dispatchWebhookForInstance: jest.fn(),
// EngineFactory.onModuleInit registers the built-in whatsapp-web.js/baileys engine plugins
// and auto-enables the configured one through these at boot; no-ops keep app boot working
// without a real plugin registry (enablePlugin failing is caught+logged, not fatal, but a
// no-op keeps the test log free of that expected-but-noisy error).
registerBuiltInPlugin: jest.fn(),
enablePlugin: jest.fn().mockResolvedValue(undefined),
onModuleInit: jest.fn(),
onModuleDestroy: jest.fn(),
})
.compile();
app = moduleRef.createNestApplication();
applyGlobalValidation(app);
await app.init();
});
afterAll(async () => {
try {
await app?.close();
} catch {
/* ignore teardown-only multi-datasource quirk */
}
});
it('requires an API key (401 without one)', async () => {
await request(app.getHttpServer()).get(`${base}/chatwoot/instances`).expect(401);
});
it('mints an instance and returns the secret + ingress URLs exactly once, then masks on read', async () => {
const mint = await request(app.getHttpServer())
.post(`${base}/chatwoot/instances`)
.set('X-API-Key', key)
.send({ instanceId: 'acct1', sessionScope: 'sess-1' })
.expect(201);
const minted = mint.body as InstanceViewBody;
expect(minted.secret).toMatch(/^[0-9a-f]{64}$/);
expect(minted.ingressUrls).toEqual([
{ route: 'chatwoot', url: 'https://api.example.com/api/ingress/chatwoot/acct1/chatwoot' },
]);
const list = await request(app.getHttpServer()).get(`${base}/chatwoot/instances`).set('X-API-Key', key).expect(200);
expect((list.body as InstanceViewBody[])[0].secret).toBe('***');
const one = await request(app.getHttpServer())
.get(`${base}/chatwoot/instances/acct1`)
.set('X-API-Key', key)
.expect(200);
expect((one.body as InstanceViewBody).secret).toBe('***');
});
it('rejects minting on a non-ingress plugin (400) and a duplicate (409)', async () => {
await request(app.getHttpServer())
.post(`${base}/plain/instances`)
.set('X-API-Key', key)
.send({ instanceId: 'x' })
.expect(400);
await request(app.getHttpServer())
.post(`${base}/chatwoot/instances`)
.set('X-API-Key', key)
.send({ instanceId: 'acct1' })
.expect(409);
});
it('rejects minting on an unknown plugin (404)', async () => {
await request(app.getHttpServer())
.post(`${base}/nonexistent/instances`)
.set('X-API-Key', key)
.send({ instanceId: 'x' })
.expect(404);
});
it('rejects an invalid instanceId charset (400)', async () => {
await request(app.getHttpServer())
.post(`${base}/chatwoot/instances`)
.set('X-API-Key', key)
.send({ instanceId: 'bad:id' })
.expect(400);
});
it('regenerates the secret and returns it once, then masks again on the next read', async () => {
const regen = await request(app.getHttpServer())
.post(`${base}/chatwoot/instances/acct1/regenerate-secret`)
.set('X-API-Key', key)
.expect(200);
expect((regen.body as InstanceViewBody).secret).toMatch(/^[0-9a-f]{64}$/);
const one = await request(app.getHttpServer())
.get(`${base}/chatwoot/instances/acct1`)
.set('X-API-Key', key)
.expect(200);
expect((one.body as InstanceViewBody).secret).toBe('***');
});
it('patches enabled + sessionScope and returns a masked view', async () => {
const patched = await request(app.getHttpServer())
.patch(`${base}/chatwoot/instances/acct1`)
.set('X-API-Key', key)
.send({ enabled: false, sessionScope: 'sess-2' })
.expect(200);
const body = patched.body as InstanceViewBody;
expect(body.enabled).toBe(false);
expect(body.sessionScope).toBe('sess-2');
expect(body.secret).toBe('***');
});
it('404s GET/PATCH/DELETE for a missing instanceId on an ingress-capable plugin', async () => {
await request(app.getHttpServer())
.get(`${base}/chatwoot/instances/does-not-exist`)
.set('X-API-Key', key)
.expect(404);
await request(app.getHttpServer())
.patch(`${base}/chatwoot/instances/does-not-exist`)
.set('X-API-Key', key)
.send({ enabled: false })
.expect(404);
await request(app.getHttpServer())
.delete(`${base}/chatwoot/instances/does-not-exist`)
.set('X-API-Key', key)
.expect(404);
});
it('deletes the instance (204) and audits it', async () => {
await request(app.getHttpServer()).delete(`${base}/chatwoot/instances/acct1`).set('X-API-Key', key).expect(204);
await request(app.getHttpServer()).get(`${base}/chatwoot/instances/acct1`).set('X-API-Key', key).expect(404);
});
});
|