357 lines
12 KiB
JavaScript
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);
|
|
});
|
|
});
|