seed data
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, type NestModule } from '@nestjs/common';
|
||||
import { AnalyticsModule } from './domain/analytics/AnalyticsModule';
|
||||
import { AuthModule } from './domain/auth/AuthModule';
|
||||
import { BootstrapModule } from './domain/bootstrap/BootstrapModule';
|
||||
@@ -19,6 +19,7 @@ import { SponsorModule } from './domain/sponsor/SponsorModule';
|
||||
import { TeamModule } from './domain/team/TeamModule';
|
||||
|
||||
import { getApiPersistence, getEnableBootstrap } from './env';
|
||||
import { RequestContextMiddleware } from './shared/http/RequestContext';
|
||||
|
||||
const API_PERSISTENCE = getApiPersistence();
|
||||
const USE_DATABASE = API_PERSISTENCE === 'postgres';
|
||||
@@ -47,4 +48,8 @@ const ENABLE_BOOTSTRAP = getEnableBootstrap();
|
||||
PolicyModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
consumer.apply(RequestContextMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('AuthController', () => {
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should call service.getCurrentSession and return session DTO', async () => {
|
||||
it('should call service.getCurrentSession and write JSON response', async () => {
|
||||
const session: AuthSessionDTO = {
|
||||
token: 'token123',
|
||||
user: {
|
||||
@@ -91,18 +91,23 @@ describe('AuthController', () => {
|
||||
};
|
||||
(service.getCurrentSession as Mock).mockResolvedValue(session);
|
||||
|
||||
const result = await controller.getSession();
|
||||
const res = { json: vi.fn() } as any;
|
||||
|
||||
await controller.getSession(res);
|
||||
|
||||
expect(service.getCurrentSession).toHaveBeenCalled();
|
||||
expect(result).toEqual(session);
|
||||
expect(res.json).toHaveBeenCalledWith(session);
|
||||
});
|
||||
|
||||
it('should return null if no session', async () => {
|
||||
it('should write JSON null when no session', async () => {
|
||||
(service.getCurrentSession as Mock).mockResolvedValue(null);
|
||||
|
||||
const result = await controller.getSession();
|
||||
const res = { json: vi.fn() } as any;
|
||||
|
||||
expect(result).toBeNull();
|
||||
await controller.getSession(res);
|
||||
|
||||
expect(service.getCurrentSession).toHaveBeenCalled();
|
||||
expect(res.json).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Controller, Get, Post, Body, Query, Inject } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Query, Inject, Res } from '@nestjs/common';
|
||||
import { Public } from './Public';
|
||||
import { AuthService } from './AuthService';
|
||||
import { LoginParamsDTO, SignupParamsDTO, AuthSessionDTO } from './dtos/AuthDto';
|
||||
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
|
||||
import type { Response } from 'express';
|
||||
|
||||
@Public()
|
||||
@Controller('auth')
|
||||
@@ -20,8 +21,11 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Get('session')
|
||||
async getSession(): Promise<AuthSessionDTO | null> {
|
||||
return this.authService.getCurrentSession();
|
||||
async getSession(@Res() res: Response): Promise<void> {
|
||||
const session = await this.authService.getCurrentSession();
|
||||
// Nest treats `null`/`undefined` as "no body" for Express responses; we need JSON `null`
|
||||
// so browser clients can safely call `response.json()`.
|
||||
res.json(session);
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
|
||||
@@ -37,8 +37,13 @@ export const COMMAND_RESULT_OUTPUT_PORT_TOKEN = 'CommandResultOutputPort';
|
||||
export const AuthProviders: Provider[] = [
|
||||
{
|
||||
provide: AUTH_REPOSITORY_TOKEN,
|
||||
useFactory: (passwordHashingService: IPasswordHashingService, logger: Logger) => {
|
||||
// Seed initial users for InMemoryUserRepository
|
||||
useFactory: (userRepository: InMemoryUserRepository, passwordHashingService: IPasswordHashingService, logger: Logger) =>
|
||||
new InMemoryAuthRepository(userRepository, passwordHashingService, logger),
|
||||
inject: [USER_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => {
|
||||
const initialUsers: StoredUser[] = [
|
||||
{
|
||||
// Match seeded racing driver id so dashboard works in inmemory mode.
|
||||
@@ -50,14 +55,8 @@ export const AuthProviders: Provider[] = [
|
||||
createdAt: new Date(),
|
||||
},
|
||||
];
|
||||
const inMemoryUserRepository = new InMemoryUserRepository(logger, initialUsers);
|
||||
return new InMemoryAuthRepository(inMemoryUserRepository, passwordHashingService, logger);
|
||||
return new InMemoryUserRepository(logger, initialUsers);
|
||||
},
|
||||
inject: [PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: USER_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryUserRepository(logger), // Factory for InMemoryUserRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
|
||||
92
apps/api/src/domain/auth/AuthSession.http.test.ts
Normal file
92
apps/api/src/domain/auth/AuthSession.http.test.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import 'reflect-metadata';
|
||||
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import request from 'supertest';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { requestContextMiddleware } from '@adapters/http/RequestContext';
|
||||
import { AuthModule } from './AuthModule';
|
||||
|
||||
describe('Auth session (HTTP, inmemory)', () => {
|
||||
let app: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [AuthModule],
|
||||
}).compile();
|
||||
|
||||
app = module.createNestApplication();
|
||||
|
||||
app.use(requestContextMiddleware);
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
transform: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app?.close();
|
||||
});
|
||||
|
||||
it('signup sets gp_session cookie and session persists across requests', async () => {
|
||||
const agent = request.agent(app.getHttpServer());
|
||||
|
||||
const signupRes = await agent
|
||||
.post('/auth/signup')
|
||||
.send({ email: 'u1@gridpilot.local', password: 'pw1', displayName: 'User 1' })
|
||||
.expect(201);
|
||||
|
||||
const setCookie = signupRes.headers['set-cookie'] as string[] | undefined;
|
||||
expect(setCookie?.some((v) => v.startsWith('gp_session='))).toBe(true);
|
||||
|
||||
const sessionRes = await agent.get('/auth/session').expect(200);
|
||||
|
||||
expect(sessionRes.body).toMatchObject({
|
||||
token: expect.stringMatching(/^gp_/),
|
||||
user: {
|
||||
email: 'u1@gridpilot.local',
|
||||
displayName: 'User 1',
|
||||
userId: expect.any(String),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('login sets gp_session cookie for seeded admin and logout clears it', async () => {
|
||||
const agent = request.agent(app.getHttpServer());
|
||||
|
||||
const loginRes = await agent
|
||||
.post('/auth/login')
|
||||
.send({ email: 'admin@gridpilot.local', password: 'admin123' })
|
||||
.expect(201);
|
||||
|
||||
const setCookie = loginRes.headers['set-cookie'] as string[] | undefined;
|
||||
expect(setCookie?.some((v) => v.startsWith('gp_session='))).toBe(true);
|
||||
|
||||
const sessionRes = await agent.get('/auth/session').expect(200);
|
||||
expect(sessionRes.body).toMatchObject({
|
||||
token: expect.any(String),
|
||||
user: {
|
||||
userId: 'driver-1',
|
||||
email: 'admin@gridpilot.local',
|
||||
displayName: 'Admin',
|
||||
},
|
||||
});
|
||||
|
||||
const logoutRes = await agent.post('/auth/logout').expect(201);
|
||||
expect(logoutRes.body).toEqual({ success: true });
|
||||
|
||||
const logoutCookies = logoutRes.headers['set-cookie'] as string[] | undefined;
|
||||
expect(logoutCookies?.some((v) => v.includes('gp_session=') && v.includes('Max-Age=0'))).toBe(true);
|
||||
|
||||
await agent.get('/auth/session').expect(200).expect((res) => {
|
||||
expect(res.body).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
11
apps/api/src/shared/http/RequestContext.ts
Normal file
11
apps/api/src/shared/http/RequestContext.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import { Injectable, type NestMiddleware } from '@nestjs/common';
|
||||
|
||||
import { requestContextMiddleware } from '@adapters/http/RequestContext';
|
||||
|
||||
@Injectable()
|
||||
export class RequestContextMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction): void {
|
||||
requestContextMiddleware(req, res, next as NextFunction);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user