Select multiple queue items and create a single consolidated Jira ticket with aggregated summary and description. Adds multi-select mode with checkboxes, floating action bar, consolidation modal, and junction table to track which queue items contributed to each ticket. - Migration: jira_ticket_queue_items junction table - POST /api/jira-tickets/:id/queue-items endpoint - GET /api/ivanti/todo-queue/ticket-links endpoint - ConsolidationModal component with aggregation logic - IvantiTodoQueuePage with selection mode and ticket link badges - Pure utility functions for summary/description generation - 34 tests passing (backend + frontend)
138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
/**
|
|
* Unit tests for GET /api/ivanti/todo-queue/ticket-links endpoint
|
|
* Validates: Requirements 6.3, 6.4
|
|
*/
|
|
const http = require('http');
|
|
const express = require('express');
|
|
|
|
// Mock auth middleware
|
|
jest.mock('../middleware/auth', () => ({
|
|
requireAuth: () => (req, _res, next) => {
|
|
req.user = { id: 7, username: 'testuser' };
|
|
next();
|
|
},
|
|
requireGroup: () => (_req, _res, next) => next(),
|
|
}));
|
|
|
|
// Mock audit log
|
|
jest.mock('../helpers/auditLog', () => jest.fn());
|
|
|
|
// Mock the db pool
|
|
jest.mock('../db', () => ({
|
|
query: jest.fn(() => Promise.resolve({ rows: [] })),
|
|
connect: jest.fn(),
|
|
}));
|
|
|
|
const pool = require('../db');
|
|
const createIvantiTodoQueueRouter = require('../routes/ivantiTodoQueue');
|
|
|
|
/**
|
|
* Helper: send an HTTP request and return { statusCode, body }.
|
|
*/
|
|
function request(server, method, path) {
|
|
return new Promise((resolve, reject) => {
|
|
const addr = server.address();
|
|
const options = {
|
|
hostname: '127.0.0.1',
|
|
port: addr.port,
|
|
path,
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
};
|
|
|
|
const req = http.request(options, (res) => {
|
|
const chunks = [];
|
|
res.on('data', (chunk) => chunks.push(chunk));
|
|
res.on('end', () => {
|
|
const raw = Buffer.concat(chunks).toString();
|
|
let body;
|
|
try { body = JSON.parse(raw); } catch { body = raw; }
|
|
resolve({ statusCode: res.statusCode, body });
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
describe('GET /api/ivanti/todo-queue/ticket-links', () => {
|
|
let app;
|
|
let server;
|
|
|
|
beforeAll((done) => {
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use('/api/ivanti/todo-queue', createIvantiTodoQueueRouter());
|
|
server = app.listen(0, '127.0.0.1', done);
|
|
});
|
|
|
|
afterAll((done) => {
|
|
server.close(done);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
it('returns an empty links object when no associations exist', async () => {
|
|
pool.query.mockResolvedValueOnce({ rows: [] });
|
|
|
|
const res = await request(server, 'GET', '/api/ivanti/todo-queue/ticket-links');
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({ links: {} });
|
|
});
|
|
|
|
it('returns a map of queue_item_id to ticket info', async () => {
|
|
pool.query.mockResolvedValueOnce({
|
|
rows: [
|
|
{ queue_item_id: 12, ticket_key: 'VULN-789', jira_url: 'https://jira.example.com/browse/VULN-789' },
|
|
{ queue_item_id: 15, ticket_key: 'VULN-789', jira_url: 'https://jira.example.com/browse/VULN-789' },
|
|
{ queue_item_id: 22, ticket_key: 'VULN-801', jira_url: 'https://jira.example.com/browse/VULN-801' },
|
|
],
|
|
});
|
|
|
|
const res = await request(server, 'GET', '/api/ivanti/todo-queue/ticket-links');
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
expect(res.body).toEqual({
|
|
links: {
|
|
'12': { ticket_key: 'VULN-789', jira_url: 'https://jira.example.com/browse/VULN-789' },
|
|
'15': { ticket_key: 'VULN-789', jira_url: 'https://jira.example.com/browse/VULN-789' },
|
|
'22': { ticket_key: 'VULN-801', jira_url: 'https://jira.example.com/browse/VULN-801' },
|
|
},
|
|
});
|
|
});
|
|
|
|
it('filters by the authenticated user ID', async () => {
|
|
pool.query.mockResolvedValueOnce({ rows: [] });
|
|
|
|
await request(server, 'GET', '/api/ivanti/todo-queue/ticket-links');
|
|
|
|
const [sql, params] = pool.query.mock.calls[0];
|
|
expect(sql).toContain('q.user_id = $1');
|
|
expect(params).toEqual([7]);
|
|
});
|
|
|
|
it('joins jira_ticket_queue_items with jira_tickets and ivanti_todo_queue', async () => {
|
|
pool.query.mockResolvedValueOnce({ rows: [] });
|
|
|
|
await request(server, 'GET', '/api/ivanti/todo-queue/ticket-links');
|
|
|
|
const [sql] = pool.query.mock.calls[0];
|
|
expect(sql).toContain('jira_ticket_queue_items');
|
|
expect(sql).toContain('JOIN jira_tickets');
|
|
expect(sql).toContain('JOIN ivanti_todo_queue');
|
|
});
|
|
|
|
it('returns 500 on database error', async () => {
|
|
pool.query.mockRejectedValueOnce(new Error('DB connection failed'));
|
|
|
|
const res = await request(server, 'GET', '/api/ivanti/todo-queue/ticket-links');
|
|
|
|
expect(res.statusCode).toBe(500);
|
|
expect(res.body).toEqual({ error: 'Internal server error.' });
|
|
});
|
|
});
|