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); }); }); });