Files
cve-dashboard/backend/__tests__/jira-ticket-queue-items.test.js
Jordan Ramos a003091b6a Add backend team enforcement via requireTeam() middleware
Introduce server-side team-scoped data access enforcement:

- Add TEAM_TO_IVANTI/IVANTI_TO_TEAM mapping to helpers/teams.js
- Add requireTeam() middleware to middleware/auth.js
  - Admin bypass (req.teamScope = null)
  - 403 for users with no team assignment
  - Populates req.teamScope with short and ivanti name arrays
- Ivanti findings: replace client ?teams= param with req.teamScope filtering
  on GET /, /counts, /counts/history, /fp-workflow-counts, POST /sync
  - Override and note endpoints verify finding is in team scope
- Compliance: add requireTeam() router-level, validate ?team= param against scope
  on GET /items and GET /summary
- CARD: validate teamName param on GET /teams/:teamName/assets
- Todo queue: verify findings belong to user's teams on POST /batch
- Clarify IVANTI_BU_FILTER comment (sync-level vs query-time filtering)
- Update 14 test files to include requireTeam in auth middleware mocks
2026-06-24 11:36:25 -06:00

216 lines
7.3 KiB
JavaScript

/**
* Unit Tests: POST /api/jira-tickets/:id/queue-items
*
* Feature: multi-item-jira-ticket
*
* Tests the junction endpoint that links queue items to a Jira ticket.
* Validates: Requirements 5.3, 6.1, 6.2
*/
const http = require('http');
const express = require('express');
// Mock the auth middleware so routes don't require real sessions/cookies.
jest.mock('../middleware/auth', () => ({
requireTeam: () => (req, res, next) => { req.teamScope = null; next(); },
requireAuth: () => (req, res, next) => {
req.user = { id: 1, username: 'test', group: 'Admin' };
next();
},
requireGroup: (...groups) => (req, res, next) => next(),
}));
// Mock the audit log helper to be a no-op.
jest.mock('../helpers/auditLog', () => jest.fn());
// Mock the jiraApi helper
jest.mock('../helpers/jiraApi', () => ({
isConfigured: false,
getRateLimitStatus: jest.fn(() => ({
burst: { remaining: 60, limit: 60 },
daily: { remaining: 1440, limit: 1440 },
})),
}));
const pool = require('../db');
jest.mock('../db', () => ({
query: jest.fn(),
}));
const createJiraTicketsRouter = require('../routes/jiraTickets');
/**
* Helper: send an HTTP request to the test server and return { statusCode, body }.
*/
function request(server, method, path, body) {
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 parsed;
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
resolve({ statusCode: res.statusCode, body: parsed });
});
});
req.on('error', reject);
if (body) {
req.write(JSON.stringify(body));
}
req.end();
});
}
describe('POST /api/jira-tickets/:id/queue-items', () => {
let app;
let server;
beforeAll((done) => {
app = express();
app.use(express.json());
app.use('/api/jira-tickets', createJiraTicketsRouter());
server = app.listen(0, '127.0.0.1', done);
});
afterAll((done) => {
server.close(done);
});
beforeEach(() => {
pool.query.mockReset();
});
// -------------------------------------------------------------------------
// Validation tests
// -------------------------------------------------------------------------
it('returns 400 when queue_item_ids is missing', async () => {
const res = await request(server, 'POST', '/api/jira-tickets/1/queue-items', {});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('queue_item_ids must be a non-empty array of integers');
});
it('returns 400 when queue_item_ids is an empty array', async () => {
const res = await request(server, 'POST', '/api/jira-tickets/1/queue-items', {
queue_item_ids: [],
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('queue_item_ids must be a non-empty array of integers');
});
it('returns 400 when queue_item_ids is not an array', async () => {
const res = await request(server, 'POST', '/api/jira-tickets/1/queue-items', {
queue_item_ids: 'not-an-array',
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('queue_item_ids must be a non-empty array of integers');
});
it('returns 400 when queue_item_ids contains non-integers', async () => {
const res = await request(server, 'POST', '/api/jira-tickets/1/queue-items', {
queue_item_ids: [1, 2.5, 3],
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('queue_item_ids must be a non-empty array of integers');
});
it('returns 400 when queue_item_ids contains strings', async () => {
const res = await request(server, 'POST', '/api/jira-tickets/1/queue-items', {
queue_item_ids: [1, 'abc', 3],
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('queue_item_ids must be a non-empty array of integers');
});
// -------------------------------------------------------------------------
// Ticket existence check
// -------------------------------------------------------------------------
it('returns 404 when jira ticket does not exist', async () => {
pool.query.mockResolvedValueOnce({ rows: [] }); // ticket lookup
const res = await request(server, 'POST', '/api/jira-tickets/999/queue-items', {
queue_item_ids: [1, 2, 3],
});
expect(res.statusCode).toBe(404);
expect(res.body.error).toBe('Jira ticket not found');
});
// -------------------------------------------------------------------------
// Queue item existence check
// -------------------------------------------------------------------------
it('returns 400 when some queue items do not exist', async () => {
pool.query
.mockResolvedValueOnce({ rows: [{ id: 42 }] }) // ticket exists
.mockResolvedValueOnce({ rows: [{ id: 1 }, { id: 2 }] }); // only 2 of 3 exist
const res = await request(server, 'POST', '/api/jira-tickets/42/queue-items', {
queue_item_ids: [1, 2, 3],
});
expect(res.statusCode).toBe(400);
expect(res.body.error).toBe('One or more queue items not found');
});
// -------------------------------------------------------------------------
// Successful linking
// -------------------------------------------------------------------------
it('returns 201 with linked_count on success', async () => {
pool.query
.mockResolvedValueOnce({ rows: [{ id: 42 }] }) // ticket exists
.mockResolvedValueOnce({ rows: [{ id: 12 }, { id: 15 }, { id: 18 }] }) // all queue items exist
.mockResolvedValueOnce({ rowCount: 3 }); // insert result
const res = await request(server, 'POST', '/api/jira-tickets/42/queue-items', {
queue_item_ids: [12, 15, 18],
});
expect(res.statusCode).toBe(201);
expect(res.body.message).toBe('Queue items linked to ticket');
expect(res.body.ticket_id).toBe(42);
expect(res.body.linked_count).toBe(3);
});
it('returns linked_count reflecting ON CONFLICT DO NOTHING (duplicates ignored)', async () => {
pool.query
.mockResolvedValueOnce({ rows: [{ id: 42 }] }) // ticket exists
.mockResolvedValueOnce({ rows: [{ id: 12 }, { id: 15 }] }) // all queue items exist
.mockResolvedValueOnce({ rowCount: 1 }); // only 1 new row (1 was duplicate)
const res = await request(server, 'POST', '/api/jira-tickets/42/queue-items', {
queue_item_ids: [12, 15],
});
expect(res.statusCode).toBe(201);
expect(res.body.linked_count).toBe(1);
});
// -------------------------------------------------------------------------
// Error handling
// -------------------------------------------------------------------------
it('returns 500 on database error', async () => {
pool.query
.mockResolvedValueOnce({ rows: [{ id: 42 }] }) // ticket exists
.mockResolvedValueOnce({ rows: [{ id: 12 }] }) // queue items exist
.mockRejectedValueOnce(new Error('Connection lost')); // insert fails
const res = await request(server, 'POST', '/api/jira-tickets/42/queue-items', {
queue_item_ids: [12],
});
expect(res.statusCode).toBe(500);
expect(res.body.error).toContain('Connection lost');
});
});