tests
This commit is contained in:
106
apps/api/src/domain/admin/Admin.http.test.ts
Normal file
106
apps/api/src/domain/admin/Admin.http.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { requestContextMiddleware } from '@adapters/http/RequestContext';
|
||||
import { AuthenticationGuard } from '../auth/AuthenticationGuard';
|
||||
import { AuthorizationGuard } from '../auth/AuthorizationGuard';
|
||||
import { IDENTITY_SESSION_PORT_TOKEN } from '../auth/AuthProviders';
|
||||
import { FeatureAvailabilityGuard } from '../policy/FeatureAvailabilityGuard';
|
||||
|
||||
describe('Admin domain (HTTP, module-wiring)', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
let app: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.resetModules();
|
||||
|
||||
process.env.GRIDPILOT_API_PERSISTENCE = 'inmemory';
|
||||
process.env.GRIDPILOT_API_BOOTSTRAP = 'true';
|
||||
delete process.env.DATABASE_URL;
|
||||
|
||||
const { AppModule } = await import('../../app.module');
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
|
||||
// Ensure AsyncLocalStorage request context is present for getActorFromRequestContext()
|
||||
app.use(requestContextMiddleware);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const reflector = new Reflector();
|
||||
const sessionPort = module.get(IDENTITY_SESSION_PORT_TOKEN);
|
||||
|
||||
const authorizationService = {
|
||||
getRolesForUser: () => [],
|
||||
};
|
||||
|
||||
const policyService = {
|
||||
getSnapshot: async () => ({
|
||||
policyVersion: 1,
|
||||
operationalMode: 'normal',
|
||||
maintenanceAllowlist: { view: [], mutate: [] },
|
||||
capabilities: {},
|
||||
loadedFrom: 'defaults',
|
||||
loadedAtIso: new Date(0).toISOString(),
|
||||
}),
|
||||
};
|
||||
|
||||
app.useGlobalGuards(
|
||||
new AuthenticationGuard(sessionPort as any),
|
||||
new AuthorizationGuard(reflector, authorizationService as any),
|
||||
new FeatureAvailabilityGuard(reflector, policyService as any),
|
||||
);
|
||||
|
||||
await app.init();
|
||||
}, 20_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await app?.close();
|
||||
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('module compiles and app is initialized', () => {
|
||||
expect(app).toBeDefined();
|
||||
expect(app.getHttpServer()).toBeDefined();
|
||||
});
|
||||
|
||||
it('rejects unauthenticated actor on admin endpoints (401)', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get('/admin/users')
|
||||
.expect(401);
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/admin/dashboard/stats')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('rejects authenticated non-admin actor (403)', async () => {
|
||||
const agent = request.agent(app.getHttpServer());
|
||||
|
||||
await agent
|
||||
.post('/auth/signup')
|
||||
.send({ email: 'user-admin-test@gridpilot.local', password: 'Password123!', displayName: 'Regular User' })
|
||||
.expect(201);
|
||||
|
||||
await agent.get('/admin/users').expect(403);
|
||||
await agent.get('/admin/dashboard/stats').expect(403);
|
||||
});
|
||||
});
|
||||
220
apps/api/src/domain/admin/AdminController.test.ts
Normal file
220
apps/api/src/domain/admin/AdminController.test.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { AdminController } from './AdminController';
|
||||
import { ListUsersRequestDto } from './dtos/ListUsersRequestDto';
|
||||
import { UserListResponseDto, UserResponseDto } from './dtos/UserResponseDto';
|
||||
import { DashboardStatsResponseDto } from './dto/DashboardStatsResponseDto';
|
||||
|
||||
describe('AdminController', () => {
|
||||
let controller: AdminController;
|
||||
let mockService: {
|
||||
listUsers: ReturnType<typeof vi.fn>;
|
||||
getDashboardStats: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockService = {
|
||||
listUsers: vi.fn(),
|
||||
getDashboardStats: vi.fn(),
|
||||
};
|
||||
|
||||
controller = new AdminController(mockService as never);
|
||||
});
|
||||
|
||||
describe('listUsers', () => {
|
||||
it('should list users with basic query params', async () => {
|
||||
const mockUser: UserResponseDto = {
|
||||
id: 'user-1',
|
||||
email: 'test@example.com',
|
||||
displayName: 'Test User',
|
||||
roles: ['admin'],
|
||||
status: 'active',
|
||||
isSystemAdmin: false,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
const mockResponse: UserListResponseDto = {
|
||||
users: [mockUser],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
mockService.listUsers.mockResolvedValue(mockResponse);
|
||||
|
||||
const query: ListUsersRequestDto = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const req = { user: { userId: 'admin-1' } } as any;
|
||||
const result = await controller.listUsers(query, req);
|
||||
|
||||
expect(mockService.listUsers).toHaveBeenCalledWith({
|
||||
actorId: 'admin-1',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should list users with all query params', async () => {
|
||||
const mockResponse: UserListResponseDto = {
|
||||
users: [],
|
||||
total: 0,
|
||||
page: 2,
|
||||
limit: 20,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
mockService.listUsers.mockResolvedValue(mockResponse);
|
||||
|
||||
const query: ListUsersRequestDto = {
|
||||
page: 2,
|
||||
limit: 20,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
email: 'admin',
|
||||
search: 'test',
|
||||
sortBy: 'email',
|
||||
sortDirection: 'desc',
|
||||
};
|
||||
|
||||
const req = { user: { userId: 'owner-1' } } as any;
|
||||
const result = await controller.listUsers(query, req);
|
||||
|
||||
expect(mockService.listUsers).toHaveBeenCalledWith({
|
||||
actorId: 'owner-1',
|
||||
page: 2,
|
||||
limit: 20,
|
||||
role: 'owner',
|
||||
status: 'active',
|
||||
email: 'admin',
|
||||
search: 'test',
|
||||
sortBy: 'email',
|
||||
sortDirection: 'desc',
|
||||
});
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
|
||||
it('should handle missing user ID from request', async () => {
|
||||
const mockResponse: UserListResponseDto = {
|
||||
users: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
mockService.listUsers.mockResolvedValue(mockResponse);
|
||||
|
||||
const query: ListUsersRequestDto = { page: 1, limit: 10 };
|
||||
const req = {} as any;
|
||||
|
||||
await controller.listUsers(query, req);
|
||||
|
||||
expect(mockService.listUsers).toHaveBeenCalledWith({
|
||||
actorId: 'current-user',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle optional query params being undefined', async () => {
|
||||
const mockResponse: UserListResponseDto = {
|
||||
users: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalPages: 0,
|
||||
};
|
||||
|
||||
mockService.listUsers.mockResolvedValue(mockResponse);
|
||||
|
||||
const query: ListUsersRequestDto = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
const req = { user: { userId: 'admin-1' } } as any;
|
||||
await controller.listUsers(query, req);
|
||||
|
||||
expect(mockService.listUsers).toHaveBeenCalledWith({
|
||||
actorId: 'admin-1',
|
||||
page: 1,
|
||||
limit: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDashboardStats', () => {
|
||||
it('should return dashboard stats', async () => {
|
||||
const mockStats: DashboardStatsResponseDto = {
|
||||
totalUsers: 150,
|
||||
activeUsers: 120,
|
||||
suspendedUsers: 20,
|
||||
deletedUsers: 10,
|
||||
systemAdmins: 5,
|
||||
recentLogins: 25,
|
||||
newUsersToday: 8,
|
||||
userGrowth: [],
|
||||
roleDistribution: [],
|
||||
statusDistribution: {
|
||||
active: 120,
|
||||
suspended: 20,
|
||||
deleted: 10,
|
||||
},
|
||||
activityTimeline: [],
|
||||
};
|
||||
|
||||
mockService.getDashboardStats.mockResolvedValue(mockStats);
|
||||
|
||||
const req = { user: { userId: 'admin-1' } } as any;
|
||||
const result = await controller.getDashboardStats(req);
|
||||
|
||||
expect(mockService.getDashboardStats).toHaveBeenCalledWith({
|
||||
actorId: 'admin-1',
|
||||
});
|
||||
expect(result).toEqual(mockStats);
|
||||
});
|
||||
|
||||
it('should handle missing user ID from request', async () => {
|
||||
const mockStats: DashboardStatsResponseDto = {
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
suspendedUsers: 0,
|
||||
deletedUsers: 0,
|
||||
systemAdmins: 0,
|
||||
recentLogins: 0,
|
||||
newUsersToday: 0,
|
||||
userGrowth: [],
|
||||
roleDistribution: [],
|
||||
statusDistribution: {
|
||||
active: 0,
|
||||
suspended: 0,
|
||||
deleted: 0,
|
||||
},
|
||||
activityTimeline: [],
|
||||
};
|
||||
|
||||
mockService.getDashboardStats.mockResolvedValue(mockStats);
|
||||
|
||||
const req = {} as any;
|
||||
const result = await controller.getDashboardStats(req);
|
||||
|
||||
expect(mockService.getDashboardStats).toHaveBeenCalledWith({
|
||||
actorId: 'current-user',
|
||||
});
|
||||
expect(result).toEqual(mockStats);
|
||||
});
|
||||
|
||||
it('should handle service errors gracefully', async () => {
|
||||
mockService.getDashboardStats.mockRejectedValue(new Error('Database connection failed'));
|
||||
|
||||
const req = { user: { userId: 'admin-1' } } as any;
|
||||
|
||||
await expect(controller.getDashboardStats(req)).rejects.toThrow('Database connection failed');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user