Files
rmyndharis--openwa/test/integration-instance.e2e-spec.ts
wehub-resource-sync 4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:24:08 +08:00

167 lines
6.4 KiB
TypeScript

// 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, ValidationPipe } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from '../src/app.module';
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();
app.setGlobalPrefix('api');
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
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);
});
});