Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add remainder auth tests #2233

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
});
5 changes: 1 addition & 4 deletions packages/altair-api/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ module.exports = {
transform: {
'^.+\\.(t|j)s$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.(t|j)s',
'!src/**/mocks/**',
],
collectCoverageFrom: ['src/**/*.(t|j)s', '!src/**/mocks/**'],
coverageDirectory: '../coverage',
testEnvironment: 'node',
setupFilesAfterEnv: ['./custom-matchers.ts'],
Expand Down
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function mockRequest(props?: object): Request {
export function mockRequest(props?: Partial<Request>): Request {

the props should be valid fields in request right? Same treatment for response below.

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')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of mocking bcrypt, let's generate a valid hash and hardcode it in the test. You can create a map of password-to-hash that you can use in the tests, if you need multiple.

.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
Loading