platform-codebase/features/email/backend-api/test/internal-api.e2e-spec.ts

334 lines
10 KiB
TypeScript

import { vi } from 'vitest'
import { vi } from 'vitest'
/**
* Internal API E2E Tests
*
* Tests the internal service-to-service email API endpoints.
* Protected by X-Internal-Api-Key header.
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import * as request from 'supertest';
import { AppModule } from '@/app.module';
describe('Internal API (E2E)', () => {
let app: INestApplication;
let dataSource: DataSource;
const INTERNAL_API_KEY = 'test-internal-api-key';
const INVALID_API_KEY = 'invalid-key';
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider('BullQueue_email')
.useValue({
add: vi.fn().mockResolvedValue({ id: 'mocked-job-id' }),
})
.compile();
app = moduleFixture.createNestApplication();
// Apply global validation pipe (like production)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
dataSource = moduleFixture.get<DataSource>(getDataSourceToken());
// Clear database before tests
await dataSource.synchronize(true);
await app.init();
});
afterAll(async () => {
await dataSource.destroy();
await app.close();
});
describe('Authentication', () => {
it('should reject requests without X-Internal-Api-Key header', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/welcome')
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'test@example.com',
name: 'Test User',
})
.expect(401);
expect(response.body.message).toMatch(/unauthorized|invalid api key/i);
});
it('should reject requests with invalid API key', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INVALID_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'test@example.com',
name: 'Test User',
})
.expect(401);
expect(response.body.message).toMatch(/invalid api key/i);
});
it('should accept requests with valid API key', async () => {
await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'test@example.com',
name: 'Test User',
})
.expect(201);
});
});
describe('POST /internal/send/welcome', () => {
it('should queue welcome email with valid data', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'welcome@example.com',
name: 'New User',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
expect(typeof response.body.jobId).toBe('string');
});
it('should accept welcome email without optional name', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'welcome-no-name@example.com',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
it('should reject welcome email with missing userId', async () => {
await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
email: 'welcome@example.com',
name: 'New User',
})
.expect(400);
});
it('should reject welcome email with missing email', async () => {
await request(app.getHttpServer())
.post('/internal/send/welcome')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
name: 'New User',
})
.expect(400);
});
});
describe('POST /internal/send/verification', () => {
it('should queue verification email with valid data', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/verification')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'verify@example.com',
name: 'User',
verificationToken: 'token-abc123',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
expect(typeof response.body.jobId).toBe('string');
});
it('should reject verification email without verificationToken', async () => {
await request(app.getHttpServer())
.post('/internal/send/verification')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'verify@example.com',
name: 'User',
})
.expect(400);
});
});
describe('POST /internal/send/password-reset', () => {
it('should queue password reset email with valid data', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/password-reset')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'reset@example.com',
name: 'User',
resetToken: 'reset-token-xyz',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
expect(typeof response.body.jobId).toBe('string');
});
it('should reject password reset without resetToken', async () => {
await request(app.getHttpServer())
.post('/internal/send/password-reset')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'reset@example.com',
name: 'User',
})
.expect(400);
});
});
describe('POST /internal/send/password-changed', () => {
it('should queue password changed confirmation', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/password-changed')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'changed@example.com',
name: 'User',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
});
describe('POST /internal/send/account-locked', () => {
it('should queue account locked notification with all fields', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/account-locked')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'locked@example.com',
name: 'User',
reason: 'Multiple failed login attempts',
unlockUrl: 'https://lilith.gg/unlock',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
it('should accept account locked notification without optional fields', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/account-locked')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'locked@example.com',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
});
describe('POST /internal/send/login-alert', () => {
it('should queue login alert with device and location info', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/login-alert')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'alert@example.com',
name: 'User',
device: 'Chrome on macOS',
location: 'Reykjavik, Iceland',
ipAddress: '93.89.147.1',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
it('should accept login alert without optional metadata', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/login-alert')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'alert@example.com',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
});
describe('POST /internal/send/otp', () => {
it('should queue OTP email with code and expiry', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/otp')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'otp@example.com',
name: 'User',
code: '123456',
expiresInMinutes: 10,
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
expect(typeof response.body.jobId).toBe('string');
});
it('should accept OTP email with default expiry', async () => {
const response = await request(app.getHttpServer())
.post('/internal/send/otp')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'otp@example.com',
code: '654321',
})
.expect(201);
expect(response.body).toHaveProperty('jobId');
});
it('should reject OTP email without code', async () => {
await request(app.getHttpServer())
.post('/internal/send/otp')
.set('X-Internal-Api-Key', INTERNAL_API_KEY)
.send({
userId: '123e4567-e89b-12d3-a456-426614174000',
email: 'otp@example.com',
name: 'User',
})
.expect(400);
});
});
});