Spaces:
Runtime error
Runtime error
| // 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); | |
| }); | |
| }); | |