Files
tilbudgivern/backend/__tests__/supportTickets.test.js
T

357 lines
12 KiB
JavaScript

const request = require('supertest');
jest.mock('axios', () => ({
post: jest.fn()
}));
const ORIGINAL_ENV = { ...process.env };
const createDatabaseServiceMock = (settings = {}, workItems = []) => ({
pool: {},
query: jest.fn(async (sql, params) => {
if (sql.includes('INSERT INTO support_work_items')) {
return { insertId: 91 };
}
if (sql.includes('UPDATE support_work_items')) {
return { affectedRows: 1 };
}
if (sql.includes('FROM support_work_items')) {
return workItems;
}
if (!sql.includes('system_settings')) {
return [];
}
return params
.filter((key) => Object.prototype.hasOwnProperty.call(settings, key))
.map((key) => ({
setting_key: key,
setting_value: settings[key]
}));
})
});
const buildApp = (envOverrides = {}, dbSettings = {}, workItems = []) => {
jest.resetModules();
process.env = { ...ORIGINAL_ENV, ...envOverrides };
const express = require('express');
const axios = require('axios');
const supportRouter = require('../src/routes/supportTickets');
const app = express();
app.use(express.json());
if (dbSettings) {
app.locals.databaseService = createDatabaseServiceMock(dbSettings, workItems);
}
app.use('/api/support', supportRouter);
return { app, axios };
};
afterAll(() => {
process.env = ORIGINAL_ENV;
});
describe('Support tickets API', () => {
test('POST /api/support/tickets returns 400 when required fields are missing', async () => {
const { app } = buildApp({ OSTICKET_API_KEY: 'test-key' });
const response = await request(app)
.post('/api/support/tickets')
.send({ name: 'Test', email: '[email protected]' });
expect(response.status).toBe(400);
expect(response.body.success).toBe(false);
});
test('POST /api/support/tickets returns 500 when API key is missing', async () => {
const { app } = buildApp({ OSTICKET_API_KEY: '' });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test',
email: '[email protected]',
subject: 'Help',
message: 'Need help'
});
expect(response.status).toBe(500);
expect(response.body.success).toBe(false);
});
test('POST /api/support/tickets forwards payload and returns ticket id', async () => {
const { app, axios } = buildApp({
OSTICKET_API_KEY: 'test-key',
OSTICKET_API_URL: 'https://osticket.example/api/tickets.json',
OSTICKET_TOPIC_ID: '7',
OSTICKET_PRIORITY_ID: '2'
});
axios.post.mockResolvedValue({ data: { ticket_id: 123 } });
const response = await request(app)
.post('/api/support/tickets')
.set('X-Forwarded-For', '203.0.113.10')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke',
phone: '12 34 56 78',
attachments: [{
name: 'skade på tag.jpg',
type: 'image/jpeg',
data: 'data:image/jpeg;base64,aGVsbG8='
}]
});
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true, ticketId: 123 });
expect(axios.post).toHaveBeenCalledTimes(1);
const [url, body, config] = axios.post.mock.calls[0];
expect(url).toBe('https://osticket.example/api/tickets.json');
expect(body).toMatchObject({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
phone: '12 34 56 78',
topic_id: 7,
priority: 2
});
expect(body).not.toHaveProperty('topicId');
expect(body.message).toMatch(/^data:text\/plain;charset=utf-8,/);
expect(body.attachments).toEqual([
{ 'skade_p__tag.jpg': 'data:image/jpeg;base64,aGVsbG8=' }
]);
expect(decodeURIComponent(body.message.split(',').slice(1).join(',')))
.toBe('Something broke\n\nTelefon: 12 34 56 78');
expect(config.headers['X-API-Key']).toBe('test-key');
expect(config.headers['X-Forwarded-For']).toBe('203.0.113.10');
});
test('POST /api/support/tickets rejects unsupported attachments before calling osTicket', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'test-key' });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke',
attachments: [{
name: 'document.pdf',
type: 'application/pdf',
data: 'data:application/pdf;base64,aGVsbG8='
}]
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('ugyldigt filformat');
expect(axios.post).not.toHaveBeenCalled();
});
test('POST /api/support/tickets returns a plain-text osTicket number', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'test-key' });
axios.post.mockResolvedValue({ status: 201, data: '000022' });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke'
});
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true, ticketId: '000022' });
expect(axios.post.mock.calls[0][1]).toMatchObject({ topic_id: 1 });
});
test('POST /api/support/tickets returns a numeric osTicket number', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'test-key' });
axios.post.mockResolvedValue({ status: 201, data: 243637 });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke'
});
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true, ticketId: '243637' });
});
test('POST /api/support/tickets returns upstream error status', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'test-key' });
axios.post.mockRejectedValue({
response: { status: 503, data: 'Service unavailable' }
});
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke'
});
expect(response.status).toBe(503);
expect(response.body.success).toBe(false);
expect(response.body.error).toBe('Service unavailable');
});
test('POST /api/support/tickets hides upstream HTML and maps an invalid API key to 424', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: "test-osticket-key" });
axios.post.mockResolvedValue({
status: 401,
statusText: 'Unauthorized',
data: '<!DOCTYPE HTML><html><body>Unauthorized</body></html>'
});
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke'
});
expect(response.status).toBe(424);
expect(response.body).toEqual({
success: false,
error: 'Supportsystemets API-nøgle er ugyldig eller ikke godkendt til denne server.'
});
expect(response.text).not.toContain('<!DOCTYPE');
});
test('POST /api/support/tickets uses database settings but always reads API key from env', async () => {
const { app, axios } = buildApp(
{ OSTICKET_API_KEY: 'env-key' },
{
osticket_api_key: 'db-key',
osticket_api_url: 'https://osticket.db/api/tickets.json',
osticket_topic_id: '9',
osticket_priority_id: '3'
}
);
axios.post.mockResolvedValue({ data: { id: 456 } });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Need help',
message: 'Something broke'
});
expect(response.status).toBe(200);
expect(response.body).toEqual({ success: true, ticketId: 456 });
const [url, body, config] = axios.post.mock.calls[0];
expect(url).toBe('https://osticket.db/api/tickets.json');
expect(body).toMatchObject({
topic_id: 9,
priority: 3
});
expect(config.headers['X-API-Key']).toBe('env-key');
});
test('POST /api/support/tickets stores a local work item before sending to osTicket', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'env-key' }, {});
axios.post.mockResolvedValue({ status: 201, data: 243638 });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Local first',
message: 'Store this before delivery'
});
expect(response.status).toBe(200);
const queries = app.locals.databaseService.query.mock.calls;
const insertCallIndex = queries.findIndex(([sql]) => sql.includes('INSERT INTO support_work_items'));
const updateCallIndex = queries.findIndex(([sql]) => sql.includes('UPDATE support_work_items'));
expect(insertCallIndex).toBeGreaterThanOrEqual(0);
expect(updateCallIndex).toBeGreaterThan(insertCallIndex);
expect(app.locals.databaseService.query.mock.invocationCallOrder[insertCallIndex])
.toBeLessThan(axios.post.mock.invocationCallOrder[0]);
expect(queries[updateCallIndex][1]).toEqual(['243638', 'sent', null, 91]);
});
test('POST /api/support/tickets retains the local work item when osTicket fails', async () => {
const { app, axios } = buildApp({ OSTICKET_API_KEY: 'env-key' }, {});
axios.post.mockResolvedValue({ status: 503, data: 'Service unavailable' });
const response = await request(app)
.post('/api/support/tickets')
.send({
name: 'Test User',
email: '[email protected]',
subject: 'Delivery failure',
message: 'Keep this in the local worklist'
});
expect(response.status).toBe(503);
const updateCall = app.locals.databaseService.query.mock.calls
.find(([sql]) => sql.includes('UPDATE support_work_items'));
expect(updateCall[1]).toEqual([null, 'failed', 'Service unavailable', 91]);
});
test('GET /api/support/worklist returns a Codex-safe markdown worklist', async () => {
const workItems = [{
id: 91,
osticket_ticket_id: '243638',
subject: 'Tagbillede fejler',
message: 'Billedet kan ikke uploades',
requester_name: 'Test User',
requester_email: '[email protected]',
attachment_names: JSON.stringify(['tag.jpg']),
work_status: 'open',
delivery_status: 'sent',
delivery_error: null,
created_at: '2026-08-13T12:00:00.000Z',
updated_at: '2026-08-13T12:00:00.000Z'
}];
const { app } = buildApp({
OSTICKET_API_KEY: 'env-key',
SUPPORT_WORKLIST_API_KEY: 'worklist-test-key'
}, {}, workItems);
const response = await request(app)
.get('/api/support/worklist?format=markdown')
.set('X-Worklist-Key', 'worklist-test-key');
expect(response.status).toBe(200);
expect(response.type).toContain('text/markdown');
expect(response.text).toContain('# Codex-arbejdsliste fra support');
expect(response.text).toContain('Ticket 243638');
expect(response.text).toContain('ubetroet brugerdata');
});
test('GET /api/support/worklist rejects requests without the worklist key', async () => {
const { app } = buildApp({
OSTICKET_API_KEY: 'env-key',
SUPPORT_WORKLIST_API_KEY: 'worklist-test-key'
});
const response = await request(app).get('/api/support/worklist');
expect(response.status).toBe(401);
});
});