godcrm/backend/routes/v3/widgets/__tests__/resolveTicketRefController.test.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

212 lines
6.4 KiB
JavaScript

// ADR-0012 §Phase 5 (M3 backend) — integration tests for
// GET /api/v3/widgets/:widgetId/tickets/:ticketId/resolve
//
// Uses supertest against a mini express app that mounts the controller's
// `buildResolveRouter` factory with mocked DB / widget loaders. No real DB.
import { describe, it, expect } from 'vitest';
import express from 'express';
import request from 'supertest';
import { buildResolveRouter } from '../resolveTicketRefController.js';
const FIXED_NOW = () => new Date('2026-04-25T12:00:00.000Z');
function buildApp({ widgets = {}, ticketRows = {}, columns = {} } = {}) {
const app = express();
// mock dbGet — called with first row in `SELECT ... FROM table_rows`.
const dbGetMock = async (_sql, params) => {
const [rowId, tableId] = params;
const key = `${tableId}:${rowId}`;
return ticketRows[key] ?? null;
};
// mock dbAll — called with column lookup `SELECT id, column_name FROM
// table_columns WHERE table_id = ?`. Returns the column array configured
// via `columns[tableId]`.
const dbAllMock = async (_sql, params) => {
const [tableId] = params;
return columns[tableId] ?? [];
};
const loadWidget = async (id) => widgets[id] ?? null;
app.use(buildResolveRouter({
dbGet: dbGetMock,
dbAll: dbAllMock,
loadWidget,
now: FIXED_NOW,
}));
return app;
}
describe('GET /widgets/:widgetId/tickets/:ticketId/resolve', () => {
it('happy path — returns ticket + snapshot', async () => {
const widgets = {
218: {
id: 218,
preset_name: 'documents',
config: { tickets_table_id: 1708 },
},
};
const ticketRows = {
'1708:131090': {
id: 131090,
base_id: 'b-131090',
table_id: 1708,
// jsonb data uses column-id keys; we'll map to names via dbAllMock
data: { 'col-what': 'Hello atom', 'col-state': 24276, 'col-assigned': '16' },
created_at: '2026-04-20T00:00:00.000Z',
updated_at: '2026-04-25T09:00:00.000Z',
},
};
const columns = {
1708: [
{ id: 'col-what', column_name: 'what' },
{ id: 'col-state', column_name: 'state' },
{ id: 'col-assigned', column_name: 'assigned_to' },
],
};
const app = buildApp({ widgets, ticketRows, columns });
const res = await request(app)
.get('/widgets/218/tickets/131090/resolve')
.expect(200);
expect(res.body.success).toBe(true);
expect(res.body.data.ticket.id).toBe(131090);
expect(res.body.data.ticket.data.what).toBe('Hello atom');
expect(res.body.data.ticket.data.state).toBe(24276);
expect(res.body.data.ticket.data.assigned_to).toBe('16');
expect(res.body.data.snapshot).toEqual({
title: 'Hello atom',
status: 24276,
assigned_to: '16',
updated_at: '2026-04-25T09:00:00.000Z',
snapshotted_at: '2026-04-25T12:00:00.000Z',
});
});
it('happy path with column data already named (no map)', async () => {
const widgets = {
218: {
id: 218,
preset_name: 'tickets_list',
config: { tickets_table_id: 1708 },
},
};
const ticketRows = {
'1708:42': {
id: 42,
base_id: 'b-42',
table_id: 1708,
data: { what: 'Direct names', state: 24275 },
created_at: '2026-04-25T08:00:00.000Z',
updated_at: '2026-04-25T08:00:00.000Z',
},
};
const app = buildApp({ widgets, ticketRows, columns: { 1708: [] } });
const res = await request(app)
.get('/widgets/218/tickets/42/resolve')
.expect(200);
expect(res.body.data.ticket.data.what).toBe('Direct names');
expect(res.body.data.snapshot.title).toBe('Direct names');
expect(res.body.data.snapshot.status).toBe(24275);
});
it('non-tickets-linked widget → 400 WIDGET_NOT_TICKETS_LINKED', async () => {
const widgets = {
500: {
id: 500,
preset_name: 'kanban', // not a tickets-linked preset
config: { board_id: 1 },
},
};
const app = buildApp({ widgets });
const res = await request(app)
.get('/widgets/500/tickets/1/resolve')
.expect(400);
expect(res.body.error).toBe('WIDGET_NOT_TICKETS_LINKED');
expect(res.body.code).toBe('WIDGET_NOT_TICKETS_LINKED');
});
it('widget not found → 404', async () => {
const app = buildApp({ widgets: {} });
const res = await request(app)
.get('/widgets/9999/tickets/1/resolve')
.expect(404);
// Uses default response.notFound shape: { success:false, error:{code,message} }
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('NOT_FOUND');
expect(res.body.error.message).toMatch(/Widget/);
});
it('ticket not found in resolved tickets table → 404', async () => {
const widgets = {
218: {
id: 218,
preset_name: 'documents',
config: { tickets_table_id: 1708 },
},
};
const app = buildApp({ widgets, ticketRows: {} });
const res = await request(app)
.get('/widgets/218/tickets/77777/resolve')
.expect(404);
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('NOT_FOUND');
expect(res.body.error.message).toMatch(/Ticket/);
});
it('non-numeric widgetId → 400', async () => {
const app = buildApp({});
const res = await request(app)
.get('/widgets/abc/tickets/1/resolve')
.expect(400);
expect(res.body.success).toBe(false);
});
it('non-numeric ticketId → 400', async () => {
const app = buildApp({});
const res = await request(app)
.get('/widgets/1/tickets/abc/resolve')
.expect(400);
expect(res.body.success).toBe(false);
});
it('documents widget without explicit tickets_table_id falls back to 1708', async () => {
const widgets = {
400: {
id: 400,
preset_name: 'documents',
config: { registry_table_id: 9999 }, // legacy doc widget, no tickets binding
},
};
const ticketRows = {
'1708:131090': {
id: 131090,
base_id: 'b',
table_id: 1708,
data: { what: 'Default-table fallback', state: 24275 },
created_at: '2026-04-25T08:00:00.000Z',
updated_at: '2026-04-25T08:00:00.000Z',
},
};
const app = buildApp({ widgets, ticketRows, columns: { 1708: [] } });
const res = await request(app)
.get('/widgets/400/tickets/131090/resolve')
.expect(200);
expect(res.body.data.snapshot.title).toBe('Default-table fallback');
});
});