Skip to content

Commit

Permalink
Add unit tests for all auth controllers and services
Browse files Browse the repository at this point in the history
  • Loading branch information
horvathmarton committed Jul 20, 2023
1 parent 5a26690 commit 1f56633
Show file tree
Hide file tree
Showing 9 changed files with 455 additions and 2 deletions.
68 changes: 68 additions & 0 deletions packages/altair-api/custom-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,76 @@ function toBeSubscriptionItem(subItem: any) {
};
}

function toBePlan(plan: any) {
let check = typeCheck('Plan.id', plan?.id, 'string');
if (!check.pass) return check;

check = typeCheck('Plan.maxQueriesCount', plan?.maxQueriesCount, 'number');
if (!check.pass) return check;

check = typeCheck('Plan.maxTeamsCount', plan?.maxTeamsCount, 'number');
if (!check.pass) return check;

check = typeCheck('Plan.maxTeamsCount', plan?.maxTeamsCount, 'number');
if (!check.pass) return check;

check = typeCheck(
'Plan.maxTeamMembersCount',
plan?.maxTeamMembersCount,
'number'
);
if (!check.pass) return check;

check = typeCheck('Plan.canUpgradePro', plan?.canUpgradePro, 'boolean');
if (!check.pass) return check;

return {
pass: true,
message: () => `expected ${plan} not to match the shape of a Plan object`,
};
}

function toBeUserStats(stats: any) {
let check = typeCheck('UserStats.queries.own', stats?.queries?.own, 'number');
if (!check.pass) return check;

check = typeCheck(
'UserStats.queries.access',
stats?.queries?.access,
'number'
);
if (!check.pass) return check;

check = typeCheck(
'UserStats.collections.own',
stats?.collections?.own,
'number'
);
if (!check.pass) return check;

check = typeCheck(
'UserStats.collections.access',
stats?.collections?.access,
'number'
);
if (!check.pass) return check;

check = typeCheck('UserStats.teams.own', stats?.teams?.own, 'number');
if (!check.pass) return check;

check = typeCheck('UserStats.teams.access', stats?.teams?.access, 'number');
if (!check.pass) return check;

return {
pass: true,
message: () => `expected ${stats} not to match the shape of a Plan object`,
};
}

expect.extend({
toBeUser,
toBePlanConfig,
toBeSubscriptionItem,
toBePlan,
toBeUserStats,
});
2 changes: 2 additions & 0 deletions packages/altair-api/jest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ declare namespace jest {
toBeUser(): R;
toBePlanConfig(): R;
toBeSubscriptionItem(): R;
toBePlan(): R;
toBeUserStats(): R;
}
}
128 changes: 128 additions & 0 deletions packages/altair-api/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,19 @@ import { PrismaService } from 'nestjs-prisma';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PasswordService } from './password/password.service';
import { mockRequest, mockResponse } from './mocks/express.mock';
import { mockUser } from './mocks/prisma-service.mock';
import { User } from '@altairgraphql/db';
import { IToken } from '@altairgraphql/api-utils';
import { BadRequestException } from '@nestjs/common';

describe('AuthController', () => {
let controller: AuthController;
let authService: AuthService;

const tokenMock =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
let authServiceReturnMock: User & { tokens: IToken };

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
Expand All @@ -22,9 +32,127 @@ describe('AuthController', () => {
}).compile();

controller = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);

authServiceReturnMock = {
...mockUser(),
tokens: {
accessToken: tokenMock,
refreshToken: tokenMock,
},
};
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('googleSigninCallback', () => {
it(`should redirect to the URL encoded in the state`, () => {
// GIVEN
const requestMock = mockRequest({
user: mockUser(),
query: {
state: 'https://google.com',
},
});
const responseMock = mockResponse({
redirect: jest.fn(),
});
jest
.spyOn(authService, 'googleLogin')
.mockReturnValueOnce(authServiceReturnMock);

// WHEN
controller.googleSigninCallback(requestMock, responseMock);

// THEN
expect(responseMock.redirect).toBeCalledWith(
'https://google.com/?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
);
});

it(`should throw if the state can't be parsed to URL`, () => {
// GIVEN
const requestMock = mockRequest({
user: mockUser(),
query: {
state: 'hi',
},
});
const responseMock = mockResponse({
redirect: jest.fn(),
});
jest
.spyOn(authService, 'googleLogin')
.mockReturnValueOnce(authServiceReturnMock);

// THEN
expect(() =>
controller.googleSigninCallback(requestMock, responseMock)
).toThrow(BadRequestException);
});

it(`should redirect to the product website if the state query param is not provided`, () => {
// GIVEN
const requestMock = mockRequest({
user: mockUser(),
query: {},
});
const responseMock = mockResponse({
redirect: jest.fn(),
});
jest
.spyOn(authService, 'googleLogin')
.mockReturnValueOnce(authServiceReturnMock);

// WHEN
controller.googleSigninCallback(requestMock, responseMock);

// THEN
expect(responseMock.redirect).toBeCalledWith('https://altairgraphql.dev');
});
});

describe('getUserProfile', () => {
it(`should return the user object from the service`, () => {
// GIVEN
const requestMock = mockRequest({ user: mockUser() });
jest
.spyOn(authService, 'googleLogin')
.mockReturnValueOnce(authServiceReturnMock);

// WHEN
const user = controller.getUserProfile(requestMock);

// THEN
expect(user).toBeUser();
});
});

describe('getShortlivedEventsToken', () => {
it(`should return a short lived token for the current user`, () => {
// GIVEN
const requestMock = mockRequest({ user: mockUser() });
jest
.spyOn(authService, 'getShortLivedEventsToken')
.mockReturnValueOnce(tokenMock);

// WHEN
const token = controller.getShortlivedEventsToken(requestMock);

// THEN
expect(token.slt).toEqual(tokenMock);
});

it(`should throw an error if the user ID is missing from the request`, () => {
// GIVEN
const requestMock = mockRequest();

// THEN
expect(() => controller.getShortlivedEventsToken(requestMock)).toThrow(
BadRequestException
);
});
});
});
9 changes: 9 additions & 0 deletions packages/altair-api/src/auth/mocks/express.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Request, Response } from 'express';

export function mockRequest(props?: object): Request {
return { ...props } as Request;
}

export function mockResponse(props?: object): Response {
return { ...props } as Response;
}
4 changes: 2 additions & 2 deletions packages/altair-api/src/auth/mocks/prisma-service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export function mockUser(): User {
} as User;
}

export function mockUserPlan(): UserPlan {
export function mockUserPlan(): UserPlan & { planConfig: PlanConfig } {
return {
userId: 'f7102dc9-4c0c-42b4-9a17-e2bd4af94d5a',
planRole: 'my role',
quantity: 1,
planConfig: mockPlanConfig(),
} as UserPlan;
} as UserPlan & { planConfig: PlanConfig };
}

export function mockPlanConfig(): PlanConfig {
Expand Down
7 changes: 7 additions & 0 deletions packages/altair-api/src/auth/mocks/stripe-service.mock.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IPlanInfo } from '@altairgraphql/api-utils';
import Stripe from 'stripe';

export function mockStripeCustomer(): Stripe.Customer {
Expand All @@ -20,3 +21,9 @@ export function mockSubscriptionItem(): Stripe.Response<Stripe.SubscriptionItem>
lastResponse: {},
} as Stripe.Response<Stripe.SubscriptionItem>;
}

export function mockPlanInfo(): IPlanInfo {
return {
priceId: 'c444e512-4a6d-4b68-bb80-43c32edde415',
} as IPlanInfo;
}
69 changes: 69 additions & 0 deletions packages/altair-api/src/auth/password/password.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,88 @@
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { PasswordService } from './password.service';
import * as bcrypt from 'bcrypt';

describe('PasswordService', () => {
let service: PasswordService;
let configService: ConfigService;

const passwordMock = '123456';
const hashMock =
'8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92';

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PasswordService, ConfigService],
}).compile();

service = module.get<PasswordService>(PasswordService);
configService = module.get<ConfigService>(ConfigService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('bcryptSaltRounds', () => {
it(`should return the salt rounds`, () => {
// GIVEN
jest
.spyOn(configService, 'get')
.mockReturnValueOnce({ bcryptSaltOrRound: 16 });

// WHEN
const rounds = service.bcryptSaltRounds;

// THEN
expect(rounds).toEqual(16);
});
});

describe('validatePassword', () => {
it(`should return true if the password matches the provided hash`, async () => {
// GIVEN
jest
.spyOn(bcrypt, 'compare')
.mockImplementationOnce(() => Promise.resolve(true));

// WHEN
const validationResult = await service.validatePassword(
passwordMock,
hashMock
);

// THEN
expect(validationResult).toBe(true);
});

it(`should return false if the password doesn't match the provided hash`, async () => {
// GIVEN
jest
.spyOn(bcrypt, 'compare')
.mockImplementationOnce(() => Promise.resolve(false));

// WHEN
const validationResult = await service.validatePassword(
passwordMock,
`${hashMock}-test`
);

// THEN
expect(validationResult).toBe(false);
});
});

describe('hashPassword', () => {
it(`should returned the hash of the password`, async () => {
// GIVEN
jest.spyOn(bcrypt, 'hash').mockImplementationOnce(() => hashMock);

// WHEN
const hash = await service.hashPassword(passwordMock);

// THEN
expect(hash).toEqual(hashMock);
});
});
});
Loading

0 comments on commit 1f56633

Please sign in to comment.