This commit is contained in:
2025-12-21 22:35:38 +01:00
parent 3c64f328e2
commit 9bd2e630e6
38 changed files with 736 additions and 684 deletions

View File

@@ -1,7 +1,8 @@
import { vi } from 'vitest';
import { Mock, vi } from 'vitest';
import { AuthController } from './AuthController';
import { AuthService } from './AuthService';
import { SignupParams, LoginParams, AuthSessionDTO } from './dtos/AuthDto';
import { AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
describe('AuthController', () => {
let controller: AuthController;
@@ -36,7 +37,7 @@ describe('AuthController', () => {
displayName: 'Test User',
},
};
(service.signupWithEmail as jest.Mock).mockResolvedValue(session);
(service.signupWithEmail as Mock).mockResolvedValue(session);
const result = await controller.signup(params);
@@ -59,7 +60,7 @@ describe('AuthController', () => {
displayName: 'Test User',
},
};
(service.loginWithEmail as jest.Mock).mockResolvedValue(session);
(service.loginWithEmail as Mock).mockResolvedValue(session);
const result = await controller.login(params);
@@ -78,7 +79,7 @@ describe('AuthController', () => {
displayName: 'Test User',
},
};
(service.getCurrentSession as jest.Mock).mockResolvedValue(session);
(service.getCurrentSession as Mock).mockResolvedValue(session);
const result = await controller.getSession();
@@ -87,7 +88,7 @@ describe('AuthController', () => {
});
it('should return null if no session', async () => {
(service.getCurrentSession as jest.Mock).mockResolvedValue(null);
(service.getCurrentSession as Mock).mockResolvedValue(null);
const result = await controller.getSession();
@@ -97,8 +98,8 @@ describe('AuthController', () => {
describe('logout', () => {
it('should call service.logout and return DTO', async () => {
const dto = { success: true };
(service.logout as jest.Mock).mockResolvedValue(dto);
const dto: CommandResultDTO = { success: true };
(service.logout as Mock).mockResolvedValue(dto);
const result = await controller.logout();

View File

@@ -1,6 +1,7 @@
import { Controller, Get, Post, Body } from '@nestjs/common';
import { AuthService } from './AuthService';
import { LoginParams, SignupParams, AuthSessionDTO } from './dtos/AuthDto';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
@Controller('auth')
export class AuthController {
@@ -22,7 +23,7 @@ export class AuthController {
}
@Post('logout')
async logout(): Promise<{ success: boolean }> {
async logout(): Promise<CommandResultDTO> {
return this.authService.logout();
}
}

View File

@@ -31,6 +31,8 @@ export const IDENTITY_SESSION_PORT_TOKEN = 'IdentitySessionPort';
export const LOGIN_USE_CASE_TOKEN = 'LoginUseCase';
export const SIGNUP_USE_CASE_TOKEN = 'SignupUseCase';
export const LOGOUT_USE_CASE_TOKEN = 'LogoutUseCase';
export const AUTH_SESSION_PRESENTER_TOKEN = 'AuthSessionPresenter';
export const COMMAND_RESULT_PRESENTER_TOKEN = 'CommandResultPresenter';
export const AuthProviders: Provider[] = [
{
@@ -73,20 +75,28 @@ export const AuthProviders: Provider[] = [
},
{
provide: LOGIN_USE_CASE_TOKEN,
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) =>
new LoginUseCase(authRepo, passwordHashing, logger),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) =>
new LoginUseCase(authRepo, passwordHashing, logger, presenter),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN],
},
{
provide: SIGNUP_USE_CASE_TOKEN,
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger) =>
new SignupUseCase(authRepo, passwordHashing, logger),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN],
useFactory: (authRepo: IAuthRepository, passwordHashing: IPasswordHashingService, logger: Logger, presenter: AuthSessionPresenter) =>
new SignupUseCase(authRepo, passwordHashing, logger, presenter),
inject: [AUTH_REPOSITORY_TOKEN, PASSWORD_HASHING_SERVICE_TOKEN, LOGGER_TOKEN, AUTH_SESSION_PRESENTER_TOKEN],
},
{
provide: LOGOUT_USE_CASE_TOKEN,
useFactory: (sessionPort: IdentitySessionPort, logger: Logger) =>
new LogoutUseCase(sessionPort, logger),
inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN],
useFactory: (sessionPort: IdentitySessionPort, logger: Logger, presenter: CommandResultPresenter) =>
new LogoutUseCase(sessionPort, logger, presenter),
inject: [IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, COMMAND_RESULT_PRESENTER_TOKEN],
},
{
provide: AUTH_SESSION_PRESENTER_TOKEN,
useClass: AuthSessionPresenter,
},
{
provide: COMMAND_RESULT_PRESENTER_TOKEN,
useClass: CommandResultPresenter,
},
];

View File

@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
// Core Use Cases
import { LoginUseCase, type LoginInput } from '@core/identity/application/use-cases/LoginUseCase';
@@ -11,12 +11,10 @@ import { User } from '@core/identity/domain/entities/User';
import type { IUserRepository } from '@core/identity/domain/repositories/IUserRepository';
import type { Logger } from '@core/shared/application';
import { IDENTITY_SESSION_PORT_TOKEN, LOGGER_TOKEN, LOGIN_USE_CASE_TOKEN, LOGOUT_USE_CASE_TOKEN, SIGNUP_USE_CASE_TOKEN, USER_REPOSITORY_TOKEN } from './AuthProviders';
import { AuthenticatedUserDTO, AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto';
import { AuthSessionDTO, LoginParams, SignupParams } from './dtos/AuthDto';
import { AuthSessionPresenter } from './presenters/AuthSessionPresenter';
import type { CommandResultDTO } from './presenters/CommandResultPresenter';
import { CommandResultPresenter } from './presenters/CommandResultPresenter';
import { CommandResultPresenter, type CommandResultDTO } from './presenters/CommandResultPresenter';
@Injectable()
export class AuthService {
constructor(
@Inject(LOGGER_TOKEN) private logger: Logger,
@@ -25,31 +23,10 @@ export class AuthService {
@Inject(LOGIN_USE_CASE_TOKEN) private readonly loginUseCase: LoginUseCase,
@Inject(SIGNUP_USE_CASE_TOKEN) private readonly signupUseCase: SignupUseCase,
@Inject(LOGOUT_USE_CASE_TOKEN) private readonly logoutUseCase: LogoutUseCase,
private readonly authSessionPresenter: AuthSessionPresenter,
private readonly commandResultPresenter: CommandResultPresenter,
) {}
private mapUserToAuthenticatedUserDTO(user: User): AuthenticatedUserDTO {
return {
userId: user.getId().value,
email: user.getEmail() ?? '',
displayName: user.getDisplayName() ?? '',
};
}
private buildAuthSessionDTO(token: string, user: AuthenticatedUserDTO): AuthSessionDTO {
return {
token,
user: {
userId: user.userId,
email: user.email,
displayName: user.displayName,
},
};
}
async getCurrentSession(): Promise<AuthSessionDTO | null> {
// TODO this must call a use case
this.logger.debug('[AuthService] Attempting to get current session.');
const coreSession = await this.identitySessionPort.getCurrentSession();
if (!coreSession) {
@@ -87,7 +64,8 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Signup failed');
}
const userDTO = this.authSessionPresenter.getResponseModel();
const authSessionPresenter = new AuthSessionPresenter();
const userDTO = authSessionPresenter.getResponseModel();
const coreUserDTO = {
id: userDTO.userId,
displayName: userDTO.displayName,
@@ -116,7 +94,8 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Login failed');
}
const userDTO = this.authSessionPresenter.getResponseModel();
const authSessionPresenter = new AuthSessionPresenter();
const userDTO = authSessionPresenter.getResponseModel();
const coreUserDTO = {
id: userDTO.userId,
displayName: userDTO.displayName,
@@ -133,6 +112,7 @@ export class AuthService {
async logout(): Promise<CommandResultDTO> {
this.logger.debug('[AuthService] Attempting logout.');
const commandResultPresenter = new CommandResultPresenter();
const result = await this.logoutUseCase.execute();
if (result.isErr()) {
@@ -140,6 +120,6 @@ export class AuthService {
throw new Error(error.details?.message ?? 'Logout failed');
}
return this.commandResultPresenter.getResponseModel();
return commandResultPresenter.getResponseModel();
}
}

View File

@@ -5,16 +5,12 @@ import { UserId } from '@core/identity/domain/value-objects/UserId';
describe('AuthSessionPresenter', () => {
let presenter: AuthSessionPresenter;
let mockIdentitySessionPort: any;
beforeEach(() => {
mockIdentitySessionPort = {
createSession: vi.fn(),
};
presenter = new AuthSessionPresenter(mockIdentitySessionPort);
presenter = new AuthSessionPresenter();
});
it('maps successful result into response model', async () => {
it('maps successful result into response model', () => {
const user = User.create({
id: UserId.fromString('user-1'),
displayName: 'Test User',
@@ -22,20 +18,15 @@ describe('AuthSessionPresenter', () => {
passwordHash: { value: 'hash' } as any,
});
const expectedSession = {
token: 'token-123',
user: {
userId: 'user-1',
email: 'user@example.com',
displayName: 'Test User',
},
const expectedUser = {
userId: 'user-1',
email: 'user@example.com',
displayName: 'Test User',
};
mockIdentitySessionPort.createSession.mockResolvedValue(expectedSession);
presenter.present({ user });
await presenter.present({ user });
expect(presenter.getResponseModel()).toEqual(expectedSession);
expect(presenter.getResponseModel()).toEqual(expectedUser);
});
it('getResponseModel throws when not presented', () => {