Skip to content

Commit

Permalink
Full test coverage for user service
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle1morel committed Feb 7, 2025
1 parent b55301d commit be9550b
Show file tree
Hide file tree
Showing 2 changed files with 383 additions and 1 deletion.
2 changes: 1 addition & 1 deletion app/src/services/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const service = {
/**
* @function listIdps
* Lists all known identity providers
* @param {boolean} [active] Optional boolean on user active status
* @param {boolean} [active] Boolean on identity_provider active status
* @returns {Promise<object>} The result of running the find operation
*/
listIdps: (active: boolean) => {
Expand Down
382 changes: 382 additions & 0 deletions app/tests/unit/services/user.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
import { NIL, v4 as uuidv4 } from 'uuid';

import { Prisma } from '@prisma/client';
import { contactService, userService } from '../../../src/services';
import { prismaMock } from '../../__mocks__/prismaMock';
import { IdentityProvider } from '../../../src/utils/enums/application';

import type { IdentityProvider as IDPType, User } from '../../../src/types';

const uuidv4Pattern = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/;

const _identity_provider = Prisma.validator<Prisma.identity_providerDefaultArgs>()({});
type PrismaIdentityProvider = Prisma.identity_providerGetPayload<typeof _identity_provider>;

const _user = Prisma.validator<Prisma.userDefaultArgs>()({});
type PrismaUser = Prisma.userGetPayload<typeof _user>;

const prismaIdirIdentityProvider: PrismaIdentityProvider = {
idp: IdentityProvider.IDIR,
active: true,
created_at: new Date(),
created_by: NIL,
updated_at: null,
updated_by: null
};

const idirIdentityProvider: IDPType = {
idp: IdentityProvider.IDIR,
active: true
};

const prismaIdirUser: PrismaUser = {
user_id: uuidv4(),
idp: IdentityProvider.IDIR,
sub: 'sub',
email: 'test@email.com',
first_name: 'Test',
full_name: 'Test User',
last_name: 'User',
active: true,
created_at: new Date(),
created_by: NIL,
updated_at: null,
updated_by: null
};

const idirUser: User = {
userId: prismaIdirUser.user_id,
idp: prismaIdirUser.idp,
sub: prismaIdirUser.sub,
email: prismaIdirUser.email,
firstName: prismaIdirUser.first_name,
fullName: prismaIdirUser.full_name,
lastName: prismaIdirUser.last_name,
active: prismaIdirUser.active
};

const idirToken = {
sub: prismaIdirUser.sub,
given_name: prismaIdirUser.first_name,
name: prismaIdirUser.full_name,
family_name: prismaIdirUser.last_name,
email: prismaIdirUser.email,
identity_provider: prismaIdirUser.idp
};

describe('user service', () => {
beforeEach(() => {
prismaMock.$transaction.mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(callback: any) => callback(prismaMock)
);
});

afterEach(() => {
jest.resetAllMocks();
jest.restoreAllMocks();
});

describe('createIdp', () => {
it('creates the idp', async () => {
prismaMock.identity_provider.create.mockResolvedValueOnce(prismaIdirIdentityProvider);
const response = await userService.createIdp(IdentityProvider.IDIR);

expect(prismaMock.identity_provider.create).toHaveBeenCalledTimes(1);
expect(response).toEqual(prismaIdirIdentityProvider);
});
});

describe('createUser', () => {
it('searches for and returns an existing user', async () => {
prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
const response = await userService.createUser(idirUser);

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(response).toEqual(idirUser);
});

it('creates new idp if not existing', async () => {
const readIdpSpy = jest.spyOn(userService, 'readIdp');
const createIdpSpy = jest.spyOn(userService, 'createIdp');

prismaMock.user.findFirst.mockResolvedValueOnce(null);
readIdpSpy.mockResolvedValueOnce(null);
prismaMock.user.create.mockResolvedValueOnce(prismaIdirUser);

await userService.createUser({ ...idirUser, userId: undefined });

expect(readIdpSpy).toHaveBeenCalledTimes(1);
expect(createIdpSpy).toHaveBeenCalledTimes(1);
});

it('creates new user if not existing', async () => {
const readIdpSpy = jest.spyOn(userService, 'readIdp');
const createIdpSpy = jest.spyOn(userService, 'createIdp');

prismaMock.user.findFirst.mockResolvedValueOnce(null);
readIdpSpy.mockResolvedValueOnce(idirIdentityProvider);
prismaMock.user.create.mockResolvedValueOnce(prismaIdirUser);

const response = await userService.createUser({ ...idirUser, userId: undefined });

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(readIdpSpy).toHaveBeenCalledTimes(1);
expect(createIdpSpy).toHaveBeenCalledTimes(0);
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1);
expect(prismaMock.user.create).toHaveBeenCalledTimes(1);
expect(response).toEqual(
expect.objectContaining({
...idirUser,
userId: expect.stringMatching(uuidv4Pattern)
})
);
});

it('uses existing transaction if provided', async () => {
const readIdpSpy = jest.spyOn(userService, 'readIdp');

prismaMock.user.findFirst.mockResolvedValueOnce(null);
readIdpSpy.mockResolvedValueOnce(idirIdentityProvider);
prismaMock.user.create.mockResolvedValueOnce(prismaIdirUser);

await prismaMock.$transaction(async (trx) => {
await userService.createUser({ ...idirUser, userId: undefined }, trx);
});

// This is a bit jank - have to add 1 for the prismaMock.$transaction in this test itself
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1);
});
});

describe('getCurrentUserId', () => {
it('should return user id if found', async () => {
prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
const response = await userService.getCurrentUserId('sub');

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(response).toEqual(prismaIdirUser.user_id);
});

it('should return defaultValue if user not found', async () => {
prismaMock.user.findFirst.mockResolvedValueOnce(null);
const response = await userService.getCurrentUserId('test');

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(response).toEqual(undefined);
});
});

describe('listIdps', () => {
it('calls identity_provider.findMany', async () => {
prismaMock.identity_provider.findMany.mockResolvedValueOnce([prismaIdirIdentityProvider]);
const response = await userService.listIdps(true);

expect(prismaMock.identity_provider.findMany).toHaveBeenCalledTimes(1);
expect(response).toStrictEqual([prismaIdirIdentityProvider]);
});
});

describe('login', () => {
it('searches for and returns an existing user', async () => {
prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
await userService.login(idirToken);

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
});

it('calls createUser if existing user not found', async () => {
const createUserSpy = jest.spyOn(userService, 'createUser');
const updateUserSpy = jest.spyOn(userService, 'updateUser');

prismaMock.user.findFirst.mockResolvedValueOnce(null);
createUserSpy.mockResolvedValueOnce(idirUser);
await userService.login(idirToken);

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(createUserSpy).toHaveBeenCalledTimes(1);
expect(updateUserSpy).toHaveBeenCalledTimes(0);
});

it('calls updateUser if existing user found', async () => {
const createUserSpy = jest.spyOn(userService, 'createUser');
const updateUserSpy = jest.spyOn(userService, 'updateUser');

prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
updateUserSpy.mockResolvedValueOnce(idirUser);
await userService.login(idirToken);

expect(prismaMock.user.findFirst).toHaveBeenCalledTimes(1);
expect(createUserSpy).toHaveBeenCalledTimes(0);
expect(updateUserSpy).toHaveBeenCalledTimes(1);
});

it('creates contact entry if not existing', async () => {
const updateUserSpy = jest.spyOn(userService, 'updateUser');
const searchContactsSpy = jest.spyOn(contactService, 'searchContacts');
const upsertContactsSpy = jest.spyOn(contactService, 'upsertContacts');

prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
updateUserSpy.mockResolvedValueOnce(idirUser);
await userService.login(idirToken);

expect(updateUserSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledTimes(1);
expect(upsertContactsSpy).toHaveBeenCalledTimes(1);
});

it('does not create contact if user returns nothing', async () => {
const updateUserSpy = jest.spyOn(userService, 'updateUser');
const searchContactsSpy = jest.spyOn(contactService, 'searchContacts');
const upsertContactsSpy = jest.spyOn(contactService, 'upsertContacts');

prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
updateUserSpy.mockResolvedValueOnce(null);
await userService.login(idirToken);

expect(updateUserSpy).toHaveBeenCalledTimes(1);
expect(searchContactsSpy).toHaveBeenCalledTimes(0);
expect(upsertContactsSpy).toHaveBeenCalledTimes(0);
});

it('returns the user', async () => {
const updateUserSpy = jest.spyOn(userService, 'updateUser');

prismaMock.user.findFirst.mockResolvedValueOnce(prismaIdirUser);
updateUserSpy.mockResolvedValueOnce(idirUser);
const response = await userService.login(idirToken);

expect(updateUserSpy).toHaveBeenCalledTimes(1);
expect(response).toEqual(idirUser);
});
});

describe('readIdp', () => {
it('calls identity_provider.findUnique', async () => {
prismaMock.identity_provider.findUnique.mockResolvedValueOnce(prismaIdirIdentityProvider);
await userService.readIdp(IdentityProvider.IDIR);

expect(prismaMock.identity_provider.findUnique).toHaveBeenCalledTimes(1);
});

it('converts prisma model to application model', async () => {
prismaMock.identity_provider.findUnique.mockResolvedValueOnce(prismaIdirIdentityProvider);
const response = await userService.readIdp(IdentityProvider.IDIR);

expect(response).toStrictEqual(idirIdentityProvider);
});
});

describe('readUser', () => {
it('calls user.findUnique', async () => {
prismaMock.user.findUnique.mockResolvedValueOnce(prismaIdirUser);
await userService.readUser(idirUser.userId as string);

expect(prismaMock.user.findUnique).toHaveBeenCalledTimes(1);
});

it('converts prisma model to application model', async () => {
prismaMock.user.findUnique.mockResolvedValueOnce(prismaIdirUser);
const response = await userService.readUser(idirUser.userId as string);

expect(response).toStrictEqual(idirUser);
});

it('returns null if user not found', async () => {
prismaMock.user.findUnique.mockResolvedValueOnce(null);
const response = await userService.readUser('badId');

expect(response).toEqual(null);
});
});

describe('searchUsers', () => {
it('calls user.findMany', async () => {
prismaMock.user.findMany.mockResolvedValueOnce([prismaIdirUser]);
await userService.searchUsers({ userId: [idirUser.userId as string] });

expect(prismaMock.user.findMany).toHaveBeenCalledTimes(1);
});

it('converts prisma model to application model', async () => {
prismaMock.user.findMany.mockResolvedValueOnce([prismaIdirUser]);
const response = await userService.searchUsers({ userId: [idirUser.userId as string] });

expect(response).toStrictEqual([idirUser]);
});

it('filters NIL userIds', async () => {
const nilUser: PrismaUser = { ...prismaIdirUser, user_id: NIL };
prismaMock.user.findMany.mockResolvedValueOnce([nilUser]);
const response = await userService.searchUsers({});

expect(response).toEqual([]);
});
});

describe('updateUser', () => {
it('returns same user if no data changes', async () => {
const readUserSpy = jest.spyOn(userService, 'readUser');

readUserSpy.mockResolvedValueOnce(idirUser);

const response = await userService.updateUser(idirUser.userId as string, idirUser);

expect(prismaMock.user.update).toHaveBeenCalledTimes(0);
expect(response).toEqual(idirUser);
});

it('creates new idp if not existing', async () => {
const readUserSpy = jest.spyOn(userService, 'readUser');
const readIdpSpy = jest.spyOn(userService, 'readIdp');
const createIdpSpy = jest.spyOn(userService, 'createIdp');

readUserSpy.mockResolvedValueOnce(idirUser);
readIdpSpy.mockResolvedValueOnce(null);

const changedUser = { ...idirUser, firstName: 'Changed' };
await userService.updateUser(changedUser.userId as string, changedUser);

expect(readIdpSpy).toHaveBeenCalledTimes(1);
expect(createIdpSpy).toHaveBeenCalledTimes(1);
});

it('updates the user', async () => {
const readUserSpy = jest.spyOn(userService, 'readUser');
const readIdpSpy = jest.spyOn(userService, 'readIdp');

readUserSpy.mockResolvedValueOnce(idirUser);
readIdpSpy.mockResolvedValueOnce(idirIdentityProvider);
prismaMock.user.update.mockResolvedValueOnce({ ...prismaIdirUser, first_name: 'Changed' });

const changedUser = { ...idirUser, firstName: 'Changed' };
const response = await userService.updateUser(changedUser.userId as string, changedUser);

expect(prismaMock.$transaction).toHaveBeenCalledTimes(1);
expect(prismaMock.user.update).toHaveBeenCalledTimes(1);
expect(response).toEqual(
expect.objectContaining({
...changedUser
})
);
});

it('uses existing transaction if provided', async () => {
const readUserSpy = jest.spyOn(userService, 'readUser');
const readIdpSpy = jest.spyOn(userService, 'readIdp');

readUserSpy.mockResolvedValueOnce(idirUser);
readIdpSpy.mockResolvedValueOnce(idirIdentityProvider);
prismaMock.user.update.mockResolvedValueOnce({ ...prismaIdirUser, first_name: 'Changed' });

const changedUser = { ...idirUser, firstName: 'Changed' };

await prismaMock.$transaction(async (trx) => {
await userService.updateUser(changedUser.userId as string, changedUser, trx);
});

// This is a bit jank - have to add 1 for the prismaMock.$transaction in this test itself
expect(prismaMock.$transaction).toHaveBeenCalledTimes(1);
});
});
});

0 comments on commit be9550b

Please sign in to comment.