Skip to content

Commit

Permalink
feat(auth && user): implementing get profile endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
leonardodimarchi committed Oct 6, 2023
1 parent dd8946e commit ac6ace4
Show file tree
Hide file tree
Showing 21 changed files with 439 additions and 65 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ coverage/
.env
postgres-data

.vscode
.idea
.eslintcache
62 changes: 62 additions & 0 deletions .vscode/typescript.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
// Place your backend-template workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }

"region": {
"prefix": "region",
"body": ["//#region $1", "", "$0", "", "//#endregion"]
},
"entity": {
"prefix": "entity",
"body": [
"export interface $1EntityProps {",
"}",
"",
"export type $1EntityCreateProps = Replace<",
" $1EntityProps,",
" {",
" }",
">;",
"",
"export class $1Entity extends BaseEntity<$1EntityProps> {",
" private constructor(",
" props: $1EntityProps,",
" baseEntityProps?: BaseEntityProps,",
" ) {",
" super(props, baseEntityProps);",
" Object.freeze(this);",
" }",
"",
" static create(",
" {}: $1EntityCreateProps,",
" baseEntityProps?: BaseEntityProps,",
" ): Either<InvalidEmailError | InvalidNameError, $1Entity> {",
"",
"",
" return new Right(",
" new $1Entity(",
" {",
" },",
" baseEntityProps,",
" ),",
" );",
" }",
"",
"}"
]
}
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This is where i'm going to place my ideas and things that i want to do or use in
- [ ] Serverless adapter example
- [ ] Add user reset password module
- [x] Add some module to interact with user (such as course or quizzes)
- [ ] Auth module with passport
- [x] Auth module with passport
- [ ] Validate request user permissions when calling a method (for example, an admin user cannot enroll at a course)
- [x] Add base user roles such as Admin, Student and Instructor
- [ ] Environment service
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"test": "jest --config ./test/jest.config.ts --maxWorkers=1",
"test:watch": "jest --watch --config ./test/jest.config.ts --maxWorkers=1",
"test": "jest --config ./test/jest.config.ts",
"test:watch": "jest --watch --config ./test/jest.config.ts",
"test:cov": "jest --coverage --config ./test/jest.cov.config.ts",
"test:e2e": "jest --config test/jest-e2e.ts",
"test:clear-cache": "jest --clearCache",
Expand Down
15 changes: 4 additions & 11 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
import { NestFactory } from '@nestjs/core';
import { I18nValidationExceptionFilter, I18nValidationPipe } from 'nestjs-i18n';
import { AppModule } from './app.module';
import { setupSwagger } from './setup-swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new I18nValidationPipe());
app.useGlobalFilters(new I18nValidationExceptionFilter());

const config = new DocumentBuilder()
.setTitle('Backend Template')
.setDescription('This is a backend template for REST APIs')
.addBearerAuth()
.build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
setupSwagger(app);

await app.listen(3000);
}
Expand Down
6 changes: 2 additions & 4 deletions src/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { UserRepository } from '@modules/user/domain/repositories/user.repository';
import { PasswordEncryptionService } from '@modules/user/domain/services/password-encryption.service';
import { UserDatabaseModule } from '@modules/user/infra/database/user-database.module';
import { UserServiceModule } from '@modules/user/infra/services/user-services.module';
import { UserModule } from '@modules/user/user.module';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { LoginUseCase } from './domain/usecases/login.usecase';
Expand All @@ -11,8 +10,7 @@ import { AuthController } from './presenter/controllers/auth.controller';

@Module({
imports: [
UserDatabaseModule,
UserServiceModule,
UserModule,
JwtModule.register({
secret: 'SECRET',
signOptions: { expiresIn: '60s' },
Expand Down
20 changes: 20 additions & 0 deletions src/modules/auth/domain/entities/request-user.entity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { faker } from '@faker-js/faker';
import { Right } from '@shared/helpers/either';
import { RequestUserEntity } from './request-user.entity';

describe('RequestUserEntity', () => {
it('should be able to instantiate', () => {
const id = faker.string.uuid();
const email = faker.internet.email();

const entity = RequestUserEntity.create({
id,
email,
});

expect(entity).toBeInstanceOf(Right);

expect((entity.value as RequestUserEntity).id).toBe(id);
expect((entity.value as RequestUserEntity).email).toBe(email);
});
});
33 changes: 33 additions & 0 deletions src/modules/auth/domain/entities/request-user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Either, Right } from '@shared/helpers/either';
import { UUID } from 'crypto';

export interface RequestUserEntityProps {
id: UUID;
email: string;
}

export type RequestUserEntityCreateProps = RequestUserEntityProps;

export class RequestUserEntity {
private constructor(props: RequestUserEntityProps) {
this.props = props;
Object.freeze(this);
}

static create({
id,
email,
}: RequestUserEntityCreateProps): Either<Error, RequestUserEntity> {
return new Right(new RequestUserEntity({ id, email }));
}

private props: RequestUserEntityProps;

get id(): UUID {
return this.props.id;
}

get email(): string {
return this.props.email;
}
}
Empty file.
23 changes: 23 additions & 0 deletions src/modules/auth/infra/strategies/auth-jwt.strategy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { faker } from '@faker-js/faker';
import { JwtStrategy } from './auth-jwt.strategy';

describe('AuthJwtStrategy', () => {
let strategy: JwtStrategy;

beforeEach(() => {
strategy = new JwtStrategy();
});

it('should return a request user entity', () => {
const id = faker.number.int();
const email = faker.internet.email();

const result = strategy.validate({
sub: id,
email,
});

expect(result.id).toBe(id);
expect(result.email).toBe(email);
});
});
14 changes: 9 additions & 5 deletions src/modules/auth/infra/strategies/auth-jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { RequestUserEntity } from '@modules/auth/domain/entities/request-user.entity';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { UUID } from 'crypto';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
Expand All @@ -12,10 +14,12 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
});
}

public validate(payload: { sub: number; email: string }): {
id: number;
email: string;
} {
return { id: payload.sub, email: payload.email };
public validate(payload: { sub: UUID; email: string }): RequestUserEntity {
const entityResult = RequestUserEntity.create({
id: payload.sub,
email: payload.email,
});

return entityResult.value as RequestUserEntity;
}
}
62 changes: 56 additions & 6 deletions src/modules/auth/presenter/controllers/auth.controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { UserNotFoundError } from '@modules/auth/domain/errors/user-not-found.error';
import { GetUserByIdUseCase } from '@modules/user/domain/usecases/get-user-by-id.usecase';
import { UserViewModel } from '@modules/user/presenter/models/view-models/user.view-model';
import {
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Left, Right } from '@shared/helpers/either';
import { MockRequestUser } from 'test/factories/mock-request-user';
import { MockUser } from 'test/factories/mock-user';
import { createI18nMock } from 'test/utils/create-i18n-mock';
import { DeepMocked, createMock } from 'test/utils/create-mock';
import { AuthController } from './auth.controller';

describe('AuthController', () => {
let controller: AuthController;
let jwtService: DeepMocked<JwtService>;
let getUserByIdUseCase: DeepMocked<GetUserByIdUseCase>;

beforeEach(() => {
jwtService = createMock<JwtService>();
controller = new AuthController(jwtService);
getUserByIdUseCase = createMock<GetUserByIdUseCase>();
controller = new AuthController(jwtService, getUserByIdUseCase);
});

describe('login', () => {
Expand All @@ -36,13 +47,52 @@ describe('AuthController', () => {
});

describe('getProfile', () => {
it('should return the request user view-model', () => {
const requestUser = MockUser.createEntity();
const requestUserViewModel = new UserViewModel(requestUser);
it('should return the request user', async () => {
const requestUserEntity = MockRequestUser.createEntity();

const userEntity = MockUser.createEntity({
override: {
email: requestUserEntity.email,
},
basePropsOverride: { id: requestUserEntity.id },
});

const userViewModel = new UserViewModel(userEntity);

getUserByIdUseCase.exec.mockResolvedValueOnce(
new Right({ user: userEntity }),
);

const result = await controller.getProfile(
requestUserEntity,
createI18nMock(),
);

expect(result).toEqual(userViewModel);
});

it('should throw a not found exception if the user was not found', async () => {
const requestUserEntity = MockRequestUser.createEntity();

getUserByIdUseCase.exec.mockResolvedValueOnce(
new Left(new UserNotFoundError(requestUserEntity.id)),
);

const call = async () =>
await controller.getProfile(requestUserEntity, createI18nMock());

expect(call).rejects.toThrow(NotFoundException);
});

it('should throw a internal server exception if there is some unknown error', async () => {
const requestUserEntity = MockRequestUser.createEntity();

getUserByIdUseCase.exec.mockResolvedValueOnce(new Left(new Error()));

const result = controller.getProfile(requestUser);
const call = async () =>
await controller.getProfile(requestUserEntity, createI18nMock());

expect(result).toEqual(requestUserViewModel);
expect(call).rejects.toThrow(InternalServerErrorException);
});
});
});
Loading

0 comments on commit ac6ace4

Please sign in to comment.