Files
tilbudgivern/tests/api.test.js
T
alex 9f74ec8949 Add initial test suite configuration with Jest and Supertest
- Created package.json for the test suite "tilbudgivern-tests"
- Added scripts for running tests, watching tests, and generating coverage reports
- Included Jest and Supertest as development dependencies
- Configured Jest with a test environment, test match pattern, and coverage collection settings
- Specified coverage output directory
2025-09-26 00:14:38 +02:00

192 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const request = require('supertest');
const app = require('../unified-server.js');
describe('Tilbudgivern API Tests', () => {
describe('Material Search API', () => {
test('GET /api/pricing/materials - should return materials', async () => {
const response = await request(app)
.get('/api/pricing/materials?search=tagsten')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('materials');
expect(Array.isArray(response.body.materials)).toBe(true);
});
test('Material search - case insensitive', async () => {
const response = await request(app)
.get('/api/pricing/materials?search=TAGSTEN')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.materials.length).toBeGreaterThan(0);
});
test('Material search - exact SKU match', async () => {
const response = await request(app)
.get('/api/pricing/materials?search=370100')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.materials.length).toBeGreaterThan(0);
});
test('Material search - SQL injection protection', async () => {
const maliciousQuery = "'; DROP TABLE bygma_products; --";
const response = await request(app)
.get(`/api/pricing/materials?search=${encodeURIComponent(maliciousQuery)}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.materials).toBeDefined();
});
});
describe('Labor API', () => {
test('POST /api/customer-projects/1/labor - valid data', async () => {
const laborData = {
carpenterCount: 2,
hoursPerDay: 8,
totalDays: 3,
hourlyRate: 450
};
const response = await request(app)
.post('/api/customer-projects/1/labor')
.send(laborData)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('laborId');
});
test('POST /api/customer-projects/1/labor - zero workers should fail', async () => {
const laborData = {
carpenterCount: 0,
hoursPerDay: 8,
totalDays: 3,
hourlyRate: 450
};
const response = await request(app)
.post('/api/customer-projects/1/labor')
.send(laborData)
.expect('Content-Type', /json/)
.expect(400);
expect(response.body).toHaveProperty('success', false);
expect(response.body.error).toContain('mindst 1 tømrer');
});
});
describe('Geometry API', () => {
test('POST /api/customer-projects/1/geometry - pitched roof with vindskede', async () => {
const geometryData = {
roofWidth: 10,
roofLength: 12,
roofHeight: 3,
roofType: 'skraat_tag',
notes: 'Test geometry with vindskede calculation'
};
const response = await request(app)
.post('/api/customer-projects/1/geometry')
.send(geometryData)
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveProperty('success', true);
expect(response.body).toHaveProperty('geometryId');
expect(response.body).toHaveProperty('area');
expect(response.body).toHaveProperty('vindskede_lbm');
expect(response.body.vindskede_lbm).toBeGreaterThan(0);
});
test('Vindskede calculation accuracy', async () => {
const geometryData = {
roofWidth: 10,
roofLength: 12,
roofHeight: 3,
roofType: 'skraat_tag'
};
const response = await request(app)
.post('/api/customer-projects/1/geometry')
.send(geometryData)
.expect(200);
// Expected: 2 * sqrt((10/2)² + 3²) = 2 * sqrt(25 + 9) = 2 * sqrt(34) ≈ 11.66
expect(response.body.vindskede_lbm).toBeCloseTo(11.66, 1);
});
test('Flat roof should have zero vindskede', async () => {
const geometryData = {
roofWidth: 10,
roofLength: 12,
roofHeight: 0,
roofType: 'fladt_tag'
};
const response = await request(app)
.post('/api/customer-projects/1/geometry')
.send(geometryData)
.expect(200);
expect(response.body.vindskede_lbm).toBe(0);
});
});
describe('Price Logic Validation', () => {
test('Package price calculation: costPrice × (1 + profit%) = listPrice', async () => {
// Test the price logic through package endpoint
const response = await request(app)
.get('/api/packages')
.expect(200);
if (response.body.packages && response.body.packages.length > 0) {
const pkg = response.body.packages[0];
const expectedListPrice = pkg.cost_price * (1 + pkg.profit_margin / 100);
expect(pkg.list_price).toBeCloseTo(expectedListPrice, 2);
}
});
});
describe('Health Check', () => {
test('GET /health - should return OK', async () => {
const response = await request(app)
.get('/health')
.expect(200);
expect(response.body).toHaveProperty('status', 'OK');
expect(response.body).toHaveProperty('timestamp');
});
});
describe('Error Handling', () => {
test('Invalid project ID should return 404', async () => {
const response = await request(app)
.post('/api/customer-projects/99999/labor')
.send({
carpenterCount: 2,
hoursPerDay: 8,
totalDays: 3,
hourlyRate: 450
})
.expect(400);
expect(response.body.success).toBe(false);
});
test('Missing required fields should return 400', async () => {
const response = await request(app)
.post('/api/customer-projects/1/geometry')
.send({})
.expect(400);
expect(response.body.success).toBe(false);
});
});
});