Skip to content

Commit

Permalink
test: session service tests (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
nathan2slime committed Jul 13, 2024
1 parent 4c132e5 commit 7959c33
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 5 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@automock/jest": "^2.1.0",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/swagger": "^7.4.0",
Expand Down
43 changes: 43 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions src/app/auth/tests/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Response, Request } from 'express';
import { Document, Model, Types } from 'mongoose';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';

Expand All @@ -10,6 +10,7 @@ import { SignInDto, SignUpDto } from '~/app/auth/auth.dto';
import { User } from '~/schemas/user.schema';
import { Session } from '~/schemas/session.schema';
import { SessionService } from '~/app/session/session.service';
import { Entity } from '~/types';

describe('AuthController', () => {
let authController: AuthController;
Expand Down Expand Up @@ -60,8 +61,7 @@ describe('AuthController', () => {
accessToken: expect.anything(),
user: expect.anything(),
refreshToken: expect.anything(),
} as Document<unknown, object, Session> &
Session & { _id: Types.ObjectId };
} as Entity<Session>;

jest.spyOn(authService, 'signIn').mockImplementation(async () => data);
const response = await authController.signIn(payload, res);
Expand All @@ -86,8 +86,7 @@ describe('AuthController', () => {
accessToken: expect.anything(),
user: expect.anything(),
refreshToken: expect.anything(),
} as Document<unknown, object, Session> &
Session & { _id: Types.ObjectId };
} as Entity<Session>;

jest.spyOn(authService, 'signUp').mockResolvedValue(data);
const response = await authController.signUp(payload, res);
Expand Down
140 changes: 140 additions & 0 deletions src/app/auth/tests/auth.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as bcrypt from 'bcrypt';

import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from '~/app/auth/auth.service';
import { Model } from 'mongoose';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';
import { HttpException, HttpStatus } from '@nestjs/common';

import { User } from '~/schemas/user.schema';
import { Session } from '~/schemas/session.schema';
import { SessionService } from '~/app/session/session.service';
import { SignInDto, SignUpDto } from '~/app/auth/auth.dto';
import { Entity } from '~/types';
import {
EMAIL_IS_ALREDY_IN_USE,
INVALID_CREDENTIALS,
USER_NOT_FOUND,
} from '~/errors';

describe('AuthService', () => {
let authService: AuthService;
let userModel: Model<User>;
let sessionService: SessionService;

afterEach(() => jest.clearAllMocks());

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [],
providers: [
AuthService,
JwtService,
SessionService,
{
provide: getModelToken(User.name),
useValue: Model,
},
{
provide: getModelToken(Session.name),
useValue: Model,
},
],
}).compile();

authService = app.get<AuthService>(AuthService);
userModel = app.get<Model<User>>(getModelToken(User.name));
sessionService = app.get<SessionService>(SessionService);
});

describe('signUp', () => {
const payload: SignUpDto = {
email: expect.anything(),
password: expect.anything(),
name: expect.anything(),
};
const user = {} as unknown as Entity<User>[];
const session = {} as unknown as Entity<Session>;

it('must create user and return his created session', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(null);
jest.spyOn(userModel, 'create').mockResolvedValue(user);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);

const data = await authService.signUp(payload);

expect(data).toBe(session);
expect(sessionService.create).toHaveBeenCalledTimes(1);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});

it('should return error if email is already in use', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);

await expect(authService.signUp(payload)).rejects.toThrow(
new HttpException(EMAIL_IS_ALREDY_IN_USE, HttpStatus.CONFLICT),
);
expect(sessionService.create).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});
});

describe('signIn', () => {
const payload: SignInDto = {
email: expect.anything(),
password: expect.anything(),
};
const user = {} as unknown as Entity<User>;
const session = {} as unknown as Entity<Session>;

it('should return the logged in user is session', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(sessionService, 'findByUser').mockResolvedValue(session);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => true);

const data = await authService.signIn(payload);

expect(data).toBe(session);
expect(sessionService.findByUser).toHaveBeenCalledTimes(1);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});

it('must log in user and create a new session', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(sessionService, 'findByUser').mockResolvedValue(null);
jest.spyOn(sessionService, 'create').mockResolvedValue(session);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => true);

const data = await authService.signIn(payload);

expect(data).toBe(session);
expect(sessionService.findByUser).toHaveBeenCalledTimes(1);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});

it('should return error when password is invalid', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(user);
jest.spyOn(sessionService, 'findByUser').mockResolvedValue(session);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => false);

await expect(authService.signIn(payload)).rejects.toThrow(
new HttpException(INVALID_CREDENTIALS, HttpStatus.UNAUTHORIZED),
);
expect(sessionService.findByUser).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});

it('should return error when user is not found', async () => {
jest.spyOn(userModel, 'findOne').mockResolvedValue(null);
jest.spyOn(bcrypt, 'compare').mockImplementation(async () => false);

await expect(authService.signIn(payload)).rejects.toThrow(
new HttpException(USER_NOT_FOUND, HttpStatus.NOT_FOUND),
);
expect(bcrypt.compare).toHaveBeenCalledTimes(0);
expect(userModel.findOne).toHaveBeenCalledTimes(1);
});
});
});
115 changes: 115 additions & 0 deletions src/app/session/tests/session.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { getModelToken } from '@nestjs/mongoose';
import { Model } from 'mongoose';

import { SessionService } from '~/app/session/session.service';
import { Session } from '~/schemas/session.schema';
import { Entity, EntityQuery } from '~/types';

describe('AuthService', () => {
let sessionService: SessionService;
let jwtService: JwtService;
let sessionModel: Model<Session>;

const user = expect.anything();

afterEach(() => jest.clearAllMocks());

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [],
providers: [
JwtService,
SessionService,
{
provide: getModelToken(Session.name),
useValue: Model,
},
],
}).compile();

jwtService = app.get<JwtService>(JwtService);
sessionModel = app.get<Model<Session>>(getModelToken(Session.name));
sessionService = app.get<SessionService>(SessionService);
});

describe('refresh', () => {
it('must generate a new access token and refresh its session', async () => {
const accessToken = expect.anything();
const session = {
populate: jest.fn(() => session),
} as unknown as EntityQuery<Session, 'findOneAndUpdate'>;

jest.spyOn(jwtService, 'signAsync').mockResolvedValue(accessToken);
jest.spyOn(sessionModel, 'findOneAndUpdate').mockResolvedValue(session);

const data = await sessionService.refresh(user);

expect(data).toBe(session);
expect(session.populate).toHaveBeenCalledTimes(1);
expect(session.populate).toHaveBeenCalledWith('user');
expect(sessionModel.findOneAndUpdate).toHaveBeenCalledTimes(1);
expect(sessionModel.findOneAndUpdate).toHaveBeenCalledWith(
{ user, isExpired: false },
{ accessToken },
{ new: true },
);
});
});

describe('create', () => {
const session = {
populate: jest.fn(),
};

it('should create and return a new session', async () => {
jest.spyOn(jwtService, 'signAsync').mockResolvedValue(expect.anything());
jest
.spyOn(sessionModel, 'create')
.mockResolvedValue(session as unknown as Entity<Session>[]);

const data = await sessionService.create(user);

expect(session.populate).toHaveBeenCalledTimes(1);
expect(session.populate).toHaveBeenCalledWith('user');
expect(sessionModel.create).toHaveBeenCalledTimes(1);
expect(data).toBe(session);
});
});

describe('expireSession', () => {
it('should expire a session', async () => {
jest
.spyOn(sessionModel, 'updateOne')
.mockResolvedValue(expect.anything());

await sessionService.expireSession(user);

expect(sessionModel.updateOne).toHaveBeenCalledTimes(1);
expect(sessionModel.updateOne).toHaveBeenCalledWith(
{ user },
{ isExpired: true },
);
});
});

describe('findByUser', () => {
it('must return session with user data', async () => {
const session = {
populate: jest.fn(() => session),
} as unknown as EntityQuery<Session, 'findOne'>;
jest.spyOn(sessionModel, 'findOne').mockReturnValue(session);

const data = await sessionService.findByUser(user);

expect(session.populate).toHaveBeenCalledWith('user');
expect(session.populate).toHaveBeenCalledTimes(1);
expect(sessionModel.findOne).toHaveBeenCalledWith({
user,
isExpired: false,
});
expect(data).toBe(session);
});
});
});
5 changes: 5 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Types, Document, Query } from 'mongoose';

export type Entity<T> = Document<unknown, object, T> &
T & { _id: Types.ObjectId };
export type EntityQuery<T, F> = Query<unknown, unknown, object, T, F, object>;

0 comments on commit 7959c33

Please sign in to comment.