334 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|