platform-codebase/features/trust/backend-api/test/internal-api.e2e-spec.ts
Lilith 827ea7a138 chore(test): 🔧 Update test files (e2e-spec.ts, setup.ts) and related test utilities
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-19 06:57:07 -08:00

463 lines
16 KiB
TypeScript

import { vi, describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { getDataSourceToken } from '@nestjs/typeorm'
import { DataSource } from 'typeorm'
import request from 'supertest'
import { DomainEventsEmitter } from '@lilith/domain-events'
import { VerificationBadge, VerificationSubjectType } from '@lilith/trust-shared'
import { AppModule } from '@/app.module'
const TEST_REVIEW_ID = '00000000-0000-4000-8000-000000000001'
const TEST_INTEL_ID = '00000000-0000-4000-8000-000000000002'
describe('Internal Verification API E2E', () => {
let app: INestApplication
let dataSource: DataSource
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(DomainEventsEmitter)
.useValue({ emit: vi.fn().mockResolvedValue(undefined), subscribe: vi.fn() })
.compile()
app = moduleFixture.createNestApplication()
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
dataSource = moduleFixture.get<DataSource>(getDataSourceToken())
await dataSource.synchronize(true)
await app.init()
})
afterAll(async () => {
await dataSource.destroy()
await app.close()
})
beforeEach(async () => {
await dataSource.query('TRUNCATE TABLE verification_proofs CASCADE')
})
// ============================================================================
// POST /internal/verify — full verification processing
// ============================================================================
describe('POST /internal/verify', () => {
it('returns 200 (not 201) when processing a verification', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
expect(response.status).toBe(200)
})
it('creates a fully verified record when all five proof flags are true', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: {
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
hasPhotoProof: true,
hasBookingLink: true,
},
})
.expect(200)
expect(response.body.verificationScore).toBe(5)
expect(response.body.badge).toBe(VerificationBadge.FULLY_VERIFIED)
expect(response.body.hasPaymentProof).toBe(true)
expect(response.body.hasLocationProof).toBe(true)
expect(response.body.hasTimestampProof).toBe(true)
expect(response.body.hasPhotoProof).toBe(true)
expect(response.body.hasBookingLink).toBe(true)
expect(response.body.subjectId).toBe(TEST_REVIEW_ID)
expect(response.body.subjectType).toBe(VerificationSubjectType.PROVIDER_REVIEW)
})
it('response includes all expected fields', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: {
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
hasPhotoProof: true,
hasBookingLink: true,
},
})
.expect(200)
expect(response.body).toMatchObject({
id: expect.any(String),
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
verificationScore: 5,
badge: VerificationBadge.FULLY_VERIFIED,
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
hasPhotoProof: true,
hasBookingLink: true,
verifiedAt: expect.any(String),
createdAt: expect.any(String),
})
})
it('calculates score=1 and badge=PARTIALLY_VERIFIED for a single proof flag', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasTimestampProof: true },
})
.expect(200)
expect(response.body.verificationScore).toBe(1)
expect(response.body.badge).toBe(VerificationBadge.PARTIALLY_VERIFIED)
expect(response.body.hasTimestampProof).toBe(true)
expect(response.body.hasPaymentProof).toBe(false)
expect(response.body.hasLocationProof).toBe(false)
expect(response.body.hasPhotoProof).toBe(false)
expect(response.body.hasBookingLink).toBe(false)
})
it('calculates score=3 and badge=VERIFIED for three proof flags', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_INTEL_ID,
subjectType: VerificationSubjectType.INTEL_REPORT,
proofs: {
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
},
})
.expect(200)
expect(response.body.verificationScore).toBe(3)
expect(response.body.badge).toBe(VerificationBadge.VERIFIED)
})
it('calculates score=4 and badge=HIGHLY_VERIFIED for four proof flags', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_INTEL_ID,
subjectType: VerificationSubjectType.INTEL_REPORT,
proofs: {
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
hasPhotoProof: true,
},
})
.expect(200)
expect(response.body.verificationScore).toBe(4)
expect(response.body.badge).toBe(VerificationBadge.HIGHLY_VERIFIED)
})
it('calculates score=0 and badge=NONE when no proof flags are set', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: {},
})
.expect(200)
expect(response.body.verificationScore).toBe(0)
expect(response.body.badge).toBe(VerificationBadge.NONE)
})
it('stores paymentData alongside the payment proof flag', async () => {
const paymentData = {
paymentIntentId: 'pi-internal-test-99',
amount: 200,
currency: 'EUR',
verifiedAt: '2026-02-14T09:00:00.000Z',
}
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true, paymentData },
})
.expect(200)
// Verify stored data via the public GET endpoint
const getResponse = await request(app.getHttpServer())
.get(`/verifications/PROVIDER_REVIEW/${TEST_REVIEW_ID}`)
.expect(200)
expect(getResponse.body.hasPaymentProof).toBe(true)
})
it('stores locationData alongside the location proof flag', async () => {
const locationData = {
latitude: 64.1355,
longitude: -21.8954,
accuracy: 20,
providerLatitude: 64.1360,
providerLongitude: -21.8960,
distanceMeters: 60,
verifiedAt: '2026-02-14T09:00:00.000Z',
}
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_INTEL_ID,
subjectType: VerificationSubjectType.INTEL_REPORT,
proofs: { hasLocationProof: true, locationData },
})
.expect(200)
const getResponse = await request(app.getHttpServer())
.get(`/verifications/INTEL_REPORT/${TEST_INTEL_ID}`)
.expect(200)
expect(getResponse.body.hasLocationProof).toBe(true)
})
it('stores photoUrl alongside the photo proof flag', async () => {
const photoUrl = 'https://cdn.atlilith.com/trust/evidence/photo-001.jpg'
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPhotoProof: true, photoUrl },
})
.expect(200)
const getResponse = await request(app.getHttpServer())
.get(`/verifications/PROVIDER_REVIEW/${TEST_REVIEW_ID}`)
.expect(200)
expect(getResponse.body.hasPhotoProof).toBe(true)
})
})
// ============================================================================
// Upsert behavior — updating an existing verification record
// ============================================================================
describe('Upsert behavior', () => {
it('updates an existing verification when called again for the same subjectId+subjectType', async () => {
// First call: create with payment proof only
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
.expect(200)
// Second call: upsert with additional proofs
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: {
hasPaymentProof: true,
hasLocationProof: true,
hasTimestampProof: true,
},
})
.expect(200)
expect(response.body.verificationScore).toBe(3)
expect(response.body.badge).toBe(VerificationBadge.VERIFIED)
expect(response.body.hasPaymentProof).toBe(true)
expect(response.body.hasLocationProof).toBe(true)
expect(response.body.hasTimestampProof).toBe(true)
})
it('does not create a duplicate record on repeated calls for the same subject', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
.expect(200)
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasBookingLink: true },
})
.expect(200)
const count = await dataSource.query(
`SELECT COUNT(*) FROM verification_proofs WHERE "subjectId" = $1 AND "subjectType" = $2`,
[TEST_REVIEW_ID, 'PROVIDER_REVIEW'],
)
expect(parseInt(count[0].count, 10)).toBe(1)
})
it('PROVIDER_REVIEW and INTEL_REPORT for the same subjectId are stored as separate records', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
.expect(200)
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.INTEL_REPORT,
proofs: { hasBookingLink: true },
})
.expect(200)
const providerReview = await request(app.getHttpServer())
.get(`/verifications/PROVIDER_REVIEW/${TEST_REVIEW_ID}`)
.expect(200)
const intelReport = await request(app.getHttpServer())
.get(`/verifications/INTEL_REPORT/${TEST_REVIEW_ID}`)
.expect(200)
expect(providerReview.body.hasPaymentProof).toBe(true)
expect(providerReview.body.hasBookingLink).toBe(false)
expect(intelReport.body.hasBookingLink).toBe(true)
expect(intelReport.body.hasPaymentProof).toBe(false)
})
})
// ============================================================================
// Input validation
// ============================================================================
describe('Input validation', () => {
it('returns 400 when subjectId is missing', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
.expect(400)
})
it('returns 400 when subjectId is not a valid UUID', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: 'not-a-uuid',
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
})
.expect(400)
})
it('returns 400 when subjectType is missing', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
proofs: { hasPaymentProof: true },
})
.expect(400)
})
it('returns 400 when subjectType is not a valid enum value', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: 'REVIEW',
proofs: { hasPaymentProof: true },
})
.expect(400)
})
it('returns non-200 when proofs field is missing entirely', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
})
// @ValidateNested() without @IsObject() on proofs field causes NestJS to error
// when proofs is undefined. Production code should add @IsObject() to guard this.
expect(response.status).not.toBe(200)
})
it('returns 400 when a proof flag is not a boolean', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: 'yes' },
})
.expect(400)
})
it('returns 400 when unknown fields are present on the request body (forbidNonWhitelisted)', async () => {
await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: { hasPaymentProof: true },
unknownField: 'should be rejected',
})
.expect(400)
})
it('accepts an empty proofs object as valid (all flags default to false)', async () => {
const response = await request(app.getHttpServer())
.post('/internal/verify')
.send({
subjectId: TEST_REVIEW_ID,
subjectType: VerificationSubjectType.PROVIDER_REVIEW,
proofs: {},
})
.expect(200)
expect(response.body.verificationScore).toBe(0)
expect(response.body.badge).toBe(VerificationBadge.NONE)
})
})
})