176 lines
6.0 KiB
TypeScript
176 lines
6.0 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { INestApplication, ValidationPipe, ExecutionContext } from '@nestjs/common';
|
|
import * as request from 'supertest';
|
|
import { AppModule } from '../../src/app.module';
|
|
import { GlobalExceptionFilter } from '../../src/shared/filters/global-exception.filter';
|
|
import { PrismaService } from '../../src/infrastructure/persistence/prisma/prisma.service';
|
|
import { JwtAuthGuard } from '../../src/shared/guards/jwt-auth.guard';
|
|
|
|
describe('Presence API (E2E)', () => {
|
|
let app: INestApplication;
|
|
let prisma: PrismaService;
|
|
|
|
// Mock JWT token for testing (in real scenario, generate from auth service)
|
|
const mockJwtToken = 'test-jwt-token';
|
|
const mockUserId = BigInt(12345);
|
|
|
|
beforeAll(async () => {
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
})
|
|
// Override JWT guard for testing
|
|
.overrideGuard(JwtAuthGuard)
|
|
.useValue({
|
|
canActivate: (context: ExecutionContext) => {
|
|
const req = context.switchToHttp().getRequest();
|
|
req.user = { userId: mockUserId.toString() };
|
|
return true;
|
|
},
|
|
})
|
|
.compile();
|
|
|
|
app = moduleFixture.createNestApplication();
|
|
|
|
// Apply same configuration as main.ts
|
|
app.useGlobalFilters(new GlobalExceptionFilter());
|
|
app.useGlobalPipes(
|
|
new ValidationPipe({
|
|
whitelist: true,
|
|
transform: true,
|
|
forbidNonWhitelisted: true,
|
|
}),
|
|
);
|
|
app.setGlobalPrefix('api/v1');
|
|
|
|
await app.init();
|
|
|
|
prisma = moduleFixture.get<PrismaService>(PrismaService);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
describe('POST /api/v1/presence/heartbeat', () => {
|
|
it('should record heartbeat successfully', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post('/api/v1/presence/heartbeat')
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.send({
|
|
installId: 'test-install-id-12345',
|
|
appVersion: '1.0.0',
|
|
clientTs: Date.now(),
|
|
})
|
|
.expect(201);
|
|
|
|
expect(response.body).toHaveProperty('ok', true);
|
|
expect(response.body).toHaveProperty('serverTs');
|
|
expect(typeof response.body.serverTs).toBe('number');
|
|
});
|
|
|
|
it('should reject heartbeat without authentication', async () => {
|
|
// This test depends on whether we mock the guard or not
|
|
// With mocked guard always returning true, this will pass
|
|
// In real scenario, this should return 401
|
|
});
|
|
|
|
it('should validate installId format', async () => {
|
|
// Test with non-string installId to trigger validation error
|
|
const response = await request(app.getHttpServer())
|
|
.post('/api/v1/presence/heartbeat')
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.send({
|
|
installId: 12345, // Invalid: not a string
|
|
appVersion: '1.0.0',
|
|
clientTs: Date.now(),
|
|
})
|
|
.expect(400);
|
|
|
|
expect(response.body.statusCode).toBe(400);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/presence/online-count', () => {
|
|
it('should return online count', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/presence/online-count')
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body).toHaveProperty('count');
|
|
expect(typeof response.body.count).toBe('number');
|
|
expect(response.body).toHaveProperty('windowSeconds');
|
|
expect(response.body.windowSeconds).toBe(180);
|
|
expect(response.body).toHaveProperty('queriedAt');
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/presence/online-history', () => {
|
|
it('should return online history', async () => {
|
|
const startTime = new Date(Date.now() - 3600000).toISOString(); // 1 hour ago
|
|
const endTime = new Date().toISOString();
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/presence/online-history')
|
|
.query({
|
|
startTime,
|
|
endTime,
|
|
interval: '5m',
|
|
})
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body).toHaveProperty('data');
|
|
expect(Array.isArray(response.body.data)).toBe(true);
|
|
expect(response.body).toHaveProperty('interval', '5m');
|
|
expect(response.body).toHaveProperty('startTime');
|
|
expect(response.body).toHaveProperty('endTime');
|
|
expect(response.body).toHaveProperty('total');
|
|
expect(response.body).toHaveProperty('summary');
|
|
});
|
|
|
|
it('should validate startTime format', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/presence/online-history')
|
|
.query({
|
|
startTime: 'invalid-date',
|
|
endTime: new Date().toISOString(),
|
|
})
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.expect(400);
|
|
|
|
expect(response.body.statusCode).toBe(400);
|
|
});
|
|
|
|
it('should validate interval enum', async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/presence/online-history')
|
|
.query({
|
|
startTime: new Date(Date.now() - 3600000).toISOString(),
|
|
endTime: new Date().toISOString(),
|
|
interval: '10m', // Invalid interval
|
|
})
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.expect(400);
|
|
|
|
expect(response.body.statusCode).toBe(400);
|
|
});
|
|
|
|
it('should use default interval when not provided', async () => {
|
|
const startTime = new Date(Date.now() - 3600000).toISOString();
|
|
const endTime = new Date().toISOString();
|
|
|
|
const response = await request(app.getHttpServer())
|
|
.get('/api/v1/presence/online-history')
|
|
.query({
|
|
startTime,
|
|
endTime,
|
|
})
|
|
.set('Authorization', `Bearer ${mockJwtToken}`)
|
|
.expect(200);
|
|
|
|
expect(response.body.interval).toBe('5m');
|
|
});
|
|
});
|
|
});
|