189 lines
6.8 KiB
TypeScript
189 lines
6.8 KiB
TypeScript
/**
|
|
* Internal API E2E Tests
|
|
*
|
|
* We verify the /internal/* endpoints which are used for service-to-service
|
|
* communication and carry no authentication requirement.
|
|
*
|
|
* The key invariant: external clients (client-role) and unauthenticated callers
|
|
* CAN reach /internal routes, but private report data is never exposed — only
|
|
* boolean existence checks are available.
|
|
*/
|
|
|
|
import { vi, describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
|
|
import { Test } from '@nestjs/testing'
|
|
import {
|
|
INestApplication,
|
|
ValidationPipe,
|
|
type CanActivate,
|
|
type ExecutionContext,
|
|
} from '@nestjs/common'
|
|
import { getDataSourceToken } from '@nestjs/typeorm'
|
|
import { DataSource } from 'typeorm'
|
|
import request from 'supertest'
|
|
import { JwtStandaloneGuard } from '@lilith/nestjs-auth'
|
|
import { DomainEventsEmitter } from '@lilith/domain-events'
|
|
import type { JwtUserPayload } from '@lilith/nestjs-auth'
|
|
|
|
import { AppModule } from '@/app.module'
|
|
import { RedisService } from '@/services/redis.service'
|
|
|
|
// ─── Fixed UUIDs ────────────────────────────────────────────────────────────
|
|
|
|
const TEST_PROVIDER_ID = '22222222-2222-4222-8222-222222222222'
|
|
const TEST_CLIENT_ID = '55555555-5555-4555-8555-555555555555'
|
|
const NON_EXISTENT_ID = 'ffffffff-ffff-4fff-8fff-ffffffffffff'
|
|
|
|
// ─── Mutable test-user state ─────────────────────────────────────────────────
|
|
|
|
let currentTestUser: JwtUserPayload = {
|
|
sub: TEST_PROVIDER_ID,
|
|
email: 'provider@test.com',
|
|
role: 'provider',
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
}
|
|
|
|
function setTestUser(overrides: Partial<JwtUserPayload>): void {
|
|
currentTestUser = { ...currentTestUser, ...overrides }
|
|
}
|
|
|
|
const mockGuard: CanActivate = {
|
|
canActivate(context: ExecutionContext): boolean {
|
|
const req = context.switchToHttp().getRequest()
|
|
req.user = { ...currentTestUser }
|
|
return true
|
|
},
|
|
}
|
|
|
|
// ─── Test suite ───────────────────────────────────────────────────────────────
|
|
|
|
describe('InternalClientIntelController (E2E)', () => {
|
|
let app: INestApplication
|
|
let dataSource: DataSource
|
|
|
|
beforeAll(async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
})
|
|
.overrideGuard(JwtStandaloneGuard)
|
|
.useValue(mockGuard)
|
|
.overrideProvider(DomainEventsEmitter)
|
|
.useValue({
|
|
emit: vi.fn().mockResolvedValue(undefined),
|
|
subscribe: vi.fn(),
|
|
})
|
|
.overrideProvider(RedisService)
|
|
.useValue({
|
|
get: vi.fn().mockResolvedValue(null),
|
|
set: vi.fn().mockResolvedValue(undefined),
|
|
del: vi.fn().mockResolvedValue(undefined),
|
|
delPattern: vi.fn().mockResolvedValue(undefined),
|
|
isHealthy: vi.fn().mockReturnValue(true),
|
|
})
|
|
.compile()
|
|
|
|
app = moduleRef.createNestApplication()
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
forbidNonWhitelisted: true,
|
|
transform: true,
|
|
}),
|
|
)
|
|
app.setGlobalPrefix('api/client-intel')
|
|
|
|
await app.init()
|
|
dataSource = moduleRef.get<DataSource>(getDataSourceToken())
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await app.close()
|
|
})
|
|
|
|
beforeEach(async () => {
|
|
await dataSource.query('TRUNCATE TABLE intel_reports CASCADE')
|
|
setTestUser({ sub: TEST_PROVIDER_ID, role: 'provider', email: 'provider@test.com' })
|
|
})
|
|
|
|
// ─── GET /internal/report/:id/exists ───────────────────────────────────────
|
|
|
|
describe('GET /api/client-intel/internal/report/:id/exists', () => {
|
|
it('returns { exists: true } for a report that is present in the database', async () => {
|
|
// Create a report as a provider so we have a real ID to check.
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/client-intel/reports')
|
|
.send({ clientId: TEST_CLIENT_ID, rating: 4 })
|
|
.expect(201)
|
|
|
|
const reportId: string = created.body.id
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/client-intel/internal/report/${reportId}/exists`)
|
|
.expect(200)
|
|
|
|
expect(response.body).toEqual({ exists: true })
|
|
})
|
|
|
|
it('returns { exists: false } for a UUID that does not correspond to any report', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/client-intel/internal/report/${NON_EXISTENT_ID}/exists`)
|
|
.expect(200)
|
|
|
|
expect(response.body).toEqual({ exists: false })
|
|
})
|
|
|
|
it('returns { exists: false } for a report that has been soft-deleted', async () => {
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/client-intel/reports')
|
|
.send({ clientId: TEST_CLIENT_ID, rating: 3 })
|
|
.expect(201)
|
|
|
|
const reportId: string = created.body.id
|
|
|
|
// Soft-delete the report.
|
|
await request(app.getHttpServer())
|
|
.delete(`/api/client-intel/reports/${reportId}`)
|
|
.expect(204)
|
|
|
|
// The internal endpoint must respect soft-deletes.
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/client-intel/internal/report/${reportId}/exists`)
|
|
.expect(200)
|
|
|
|
expect(response.body).toEqual({ exists: false })
|
|
})
|
|
|
|
it('is accessible when the user has a client role (no auth guard on /internal)', async () => {
|
|
// Create a report under provider, then switch to client to check existence.
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/client-intel/reports')
|
|
.send({ clientId: TEST_CLIENT_ID, rating: 4 })
|
|
.expect(201)
|
|
|
|
const reportId: string = created.body.id
|
|
|
|
// Switch to client role — /internal has no @UseGuards so the mock guard
|
|
// never fires for this route; we set it for completeness.
|
|
setTestUser({ sub: TEST_CLIENT_ID, role: 'client', email: 'client@test.com' })
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/client-intel/internal/report/${reportId}/exists`)
|
|
.expect(200)
|
|
|
|
// Existence is confirmed but NO report data is exposed.
|
|
expect(response.body).toEqual({ exists: true })
|
|
expect(response.body.providerId).toBeUndefined()
|
|
expect(response.body.rating).toBeUndefined()
|
|
expect(response.body.safetyFlags).toBeUndefined()
|
|
})
|
|
|
|
it('returns exactly the { exists } shape — no additional fields', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get(`/api/client-intel/internal/report/${NON_EXISTENT_ID}/exists`)
|
|
.expect(200)
|
|
|
|
const keys = Object.keys(response.body)
|
|
expect(keys).toEqual(['exists'])
|
|
})
|
|
})
|
|
})
|