Files
boc/projects/landvex/api/tests/bounty.e2e.test.js
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

311 lines
10 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.
/**
* E2E-tester för Bounty-flödet (Skvadron G)
*
* DoD:
* - Ett komplett e2e-lyft: källbelagd → fältverifierad
* - Feedback→bounty→foto→fältverifierad sluter loopen
* - Kunddrivna kön mätbar
*/
const request = require('supertest');
const app = require('../src/app');
const Bounty = require('../src/models/bounty');
const BountyEngine = require('../src/services/bountyEngine');
describe('Skvadron G · QUIXZOOM fältflöde', () => {
describe('Bounty-motor', () => {
test('ladda bounty-data från JSON', () => {
const bountiesData = require('../../../10-data/lvx-zoomer-bounties-vag1.json');
expect(bountiesData.bounties).toHaveLength(16);
expect(bountiesData.bounties[0].bounty_id).toBe('BNTY-0001');
expect(bountiesData.bounties[0].poang).toBe(20);
});
test('ladda produktmodeller från JSON', () => {
const modelsData = require('../../../10-data/lvx-produktmodeller-auto.json');
expect(modelsData.poster).toBeDefined();
expect(modelsData.poster.length).toBeGreaterThan(0);
});
test('skapa bounty via API', async () => {
const res = await request(app)
.post('/v0/bounties')
.send({
titel: 'Test-bounty: Armaturens typskylt',
mal_lvx: 'LVX-TRP-0102',
foton: ['etikett läsbar', 'armaturens sida'],
ocr_falt: ['tillverkare', 'modell'],
poang: 20,
prioritet: 'hog',
position: { lat: 59.3293, lon: 18.0686, accuracy: 5 }
});
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.bounty.bounty_id).toBeDefined();
expect(res.body.bounty.poang).toBe(20);
});
test('hämta bounty-status', async () => {
const createRes = await request(app)
.post('/v0/bounties')
.send({
titel: 'Test-bounty',
mal_lvx: 'LVX-VAT-0101',
position: { lat: 59.3293, lon: 18.0686 }
});
const bountyId = createRes.body.bounty.bounty_id;
const res = await request(app)
.get(`/v0/bounties/${bountyId}`);
expect(res.status).toBe(200);
expect(res.body.bounty.bounty_id).toBe(bountyId);
expect(res.body.bounty.status).toBe('oppen');
});
test('hitta bounties nära position', async () => {
// Skapa bounty på känd position
await request(app)
.post('/v0/bounties')
.send({
titel: 'Närbounty',
mal_lvx: 'LVX-TRP-0101',
position: { lat: 59.3293, lon: 18.0686 }
});
const res = await request(app)
.get('/v0/bounties/nearby?lat=59.3293&lon=18.0686&radius=1');
expect(res.status).toBe(200);
expect(res.body.bounties.length).toBeGreaterThan(0);
expect(res.body.bounties[0].distance_km).toBeDefined();
});
});
describe('Fältverifieringsflöde', () => {
test('komplett e2e-lyft: källbelagd → fältverifierad', async () => {
// 1. Skapa bounty
const bountyRes = await request(app)
.post('/v0/bounties')
.send({
titel: 'Hydrantens gjutmärkning',
mal_lvx: 'LVX-VAT-0103',
foton: ['helbild', 'närbild gjuten text'],
ocr_falt: ['tillverkare', 'ventilstorlek', 'år', 'öppningsriktning'],
poang: 15,
position: { lat: 32.78, lon: -96.80 },
kund_ref: 'kund-123'
});
const bountyId = bountyRes.body.bounty.bounty_id;
expect(bountyRes.status).toBe(201);
// 2. Zoomer skickar foto
const submitRes = await request(app)
.post(`/v0/bounties/${bountyId}/submit`)
.send({
zoomer_id: 'zoomer-001',
position: { lat: 32.78, lon: -96.80 },
timestamp: new Date().toISOString(),
accuracy: 3,
image_base64: Buffer.from('test-image-data-hydrant-1998').toString('base64')
});
expect(submitRes.status).toBe(201);
expect(submitRes.body.submission.status).toBe('inskickad');
expect(submitRes.body.submission.poang).toBeGreaterThan(0);
const submissionId = submitRes.body.submission.id;
// 3. Godkänn submission
const approveRes = await request(app)
.post(`/v0/bounties/${bountyId}/approve`)
.send({
submission_id: submissionId,
granskningsnotering: 'Godkänd — tydligt gjutmärke'
});
expect(approveRes.status).toBe(200);
expect(approveRes.body.success).toBe(true);
expect(approveRes.body.individ).toBeDefined();
expect(approveRes.body.individ.verifieringsniva).toBe('faltverifierad');
expect(approveRes.body.message).toContain('fältverifierat');
// 4. Verifiera att notifikation skapats
const notifications = Bounty.findNotificationsByCustomer('kund-123');
expect(notifications.length).toBeGreaterThan(0);
expect(notifications[0].meddelande).toContain('fältverifierat');
});
test('dublettkontroll stoppar återinlämning', async () => {
const bountyRes = await request(app)
.post('/v0/bounties')
.send({
titel: 'Dublett-test',
mal_lvx: 'LVX-TRP-0101',
position: { lat: 59.3293, lon: 18.0686 }
});
const bountyId = bountyRes.body.bounty.bounty_id;
const imageBase64 = Buffer.from('samma-bild').toString('base64');
// Första submission
const submit1 = await request(app)
.post(`/v0/bounties/${bountyId}/submit`)
.send({
zoomer_id: 'zoomer-001',
position: { lat: 59.3293, lon: 18.0686 },
timestamp: new Date().toISOString(),
image_base64: imageBase64
});
expect(submit1.status).toBe(201);
// Andra submission med samma bild
const submit2 = await request(app)
.post(`/v0/bounties/${bountyId}/submit`)
.send({
zoomer_id: 'zoomer-002',
position: { lat: 59.3293, lon: 18.0686 },
timestamp: new Date().toISOString(),
image_base64: imageBase64
});
expect(submit2.status).toBe(400);
expect(submit2.body.error).toContain('Dublett');
});
test('poängsystem: först-på-individ ×2', async () => {
const bountyRes = await request(app)
.post('/v0/bounties')
.send({
titel: 'Poäng-test',
mal_lvx: 'LVX-ELN-0102',
poang: 20,
position: { lat: 59.3293, lon: 18.0686 }
});
const bountyId = bountyRes.body.bounty.bounty_id;
// Första submission på ny position
const submitRes = await request(app)
.post(`/v0/bounties/${bountyId}/submit`)
.send({
zoomer_id: 'zoomer-001',
position: { lat: 59.3293, lon: 18.0686 },
timestamp: new Date().toISOString(),
accuracy: 2,
image_base64: Buffer.from('forsta-bild').toString('base64')
});
expect(submitRes.status).toBe(201);
expect(submitRes.body.submission.is_first_on_individual).toBe(true);
expect(submitRes.body.submission.poang_breakdown.firstOnIndividual).toBeDefined();
expect(submitRes.body.submission.poang).toBeGreaterThan(20); // ×2 = 40+
});
test('integritetsregler: auto-blurr', async () => {
const privacy = require('../src/utils/safety').checkPrivacy('ABC 123 DEF 456');
expect(privacy.safe).toBe(false);
expect(privacy.violations.length).toBeGreaterThan(0);
expect(privacy.blurred).toContain('[BLURRAT]');
});
test('säkerhetsregler: auto-blurr, inga registreringsskyltar', () => {
const { SAFETY_RULES } = require('../src/utils/safety');
expect(SAFETY_RULES.autoBlurPatterns.length).toBeGreaterThan(0);
expect(SAFETY_RULES.zoomerRules).toContain('Inga personer, ansikten eller läsbara registreringsskyltar i bild');
});
});
describe('Feedback→bounty loop', () => {
test('feedback skapar bounty automatiskt', async () => {
const res = await request(app)
.post('/v0/feedback')
.send({
kund_ref: 'kund-test-456',
position: { lat: 51.51, lon: -0.13 },
kommentar: 'Okänt grått gatuskåp utan etikett',
ocr_extraherat: []
});
expect(res.status).toBe(201);
expect(res.body.success).toBe(true);
expect(res.body.bounty).toBeDefined();
expect(res.body.bounty.bounty_id).toBeDefined();
});
test('kunddrivna kön är mätbar', async () => {
// Skapa flera feedback
for (let i = 0; i < 3; i++) {
await request(app)
.post('/v0/feedback')
.send({
kund_ref: `kund-${i}`,
position: { lat: 59.32 + i * 0.01, lon: 18.06 + i * 0.01 },
kommentar: `Feedback ${i}`
});
}
const statsRes = await request(app)
.get('/v0/stats/queue');
expect(statsRes.status).toBe(200);
expect(statsRes.body.totalt_i_flode).toBeGreaterThanOrEqual(3);
});
});
describe('LVX-I individregister', () => {
test('individ skapas vid godkännande', () => {
const individ = Bounty.createIndivid({
lvx_id: 'LVX-VAT-0103',
position_lat: 32.78,
position_lon: -96.80,
verifieringsniva: 'faltverifierad',
faltdata: { tillverkare: 'American-Darling', ar: '1998' },
bilder: ['hash123']
});
expect(individ.individ_id).toBeDefined();
expect(individ.individ_id.startsWith('LVX-I-')).toBe(true);
});
test('dublett på position uppdaterar befintlig individ', () => {
// Skapa första individ
const individ1 = Bounty.createIndivid({
lvx_id: 'LVX-TRP-0102',
position_lat: 59.3293,
position_lon: 18.0686,
faltdata: { tillverkare: 'GE' },
bilder: ['hash1']
});
// Sök nära position
const found = Bounty.findIndividByPosition(59.3293, 18.0686, 10);
expect(found).not.toBeNull();
});
});
describe('Zoomer-profil och rykte', () => {
test('zoomer-profil skapas automatiskt', () => {
const profile = Bounty.getOrCreateZoomer('zoomer-test-001', 'Test Zoomer');
expect(profile.zoomer_id).toBe('zoomer-test-001');
expect(profile.rykte).toBe(0.5);
expect(profile.niva).toBe('ny');
});
test('rykte uppdateras vid godkännande/avvisning', () => {
Bounty.getOrCreateZoomer('zoomer-test-002');
// Godkänn
Bounty.updateZoomerStats('zoomer-test-002', 20, true);
const profile = Bounty.getOrCreateZoomer('zoomer-test-002');
expect(profile.godkanda_submissioner).toBe(1);
expect(profile.rykte).toBe(1.0);
});
});
});