added test for LoginRequestsService

This commit is contained in:
rykuno 2024-06-26 23:01:36 -05:00
parent 3d56a22295
commit d387a16bbe
7 changed files with 96 additions and 10 deletions

View file

@ -6,6 +6,7 @@ import { container } from 'tsyringe';
import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware'; import { validateAuthSession, verifyOrigin } from './middleware/auth.middleware';
import { IamController } from './controllers/iam.controller'; import { IamController } from './controllers/iam.controller';
import { config } from './common/config'; import { config } from './common/config';
import { UsersController } from './controllers/users.controller';
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Client Request */ /* Client Request */
@ -36,7 +37,6 @@ app.use(verifyOrigin).use(validateAuthSession);
const routes = app const routes = app
.route('/iam', container.resolve(IamController).routes()) .route('/iam', container.resolve(IamController).routes())
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Exports */ /* Exports */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */

View file

@ -0,0 +1,11 @@
import { Scrypt } from "oslo/password";
export async function hash(value: string) {
const scrypt = new Scrypt()
return scrypt.hash(value);
}
export function verify(hashedValue: string, value: string) {
return new Scrypt().verify(hashedValue, value);
}

View file

@ -1,6 +1,6 @@
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
import { DatabaseProvider } from "../providers"; import { DatabaseProvider } from "../providers";
import { and, eq, lte, type InferInsertModel } from "drizzle-orm"; import { and, eq, gte, lte, type InferInsertModel } from "drizzle-orm";
import type { Repository } from "../interfaces/repository.interface"; import type { Repository } from "../interfaces/repository.interface";
import { takeFirst, takeFirstOrThrow } from "../infrastructure/database/utils"; import { takeFirst, takeFirstOrThrow } from "../infrastructure/database/utils";
import { emailVerificationsTable } from "../infrastructure/database/tables/email-verifications.table"; import { emailVerificationsTable } from "../infrastructure/database/tables/email-verifications.table";
@ -24,7 +24,7 @@ export class EmailVerificationsRepository implements Repository {
return this.db.select().from(emailVerificationsTable).where( return this.db.select().from(emailVerificationsTable).where(
and( and(
eq(emailVerificationsTable.userId, userId), eq(emailVerificationsTable.userId, userId),
lte(emailVerificationsTable.expiresAt, new Date()) gte(emailVerificationsTable.expiresAt, new Date())
)).then(takeFirst) )).then(takeFirst)
} }

View file

@ -2,7 +2,7 @@ import { inject, injectable } from "tsyringe";
import { DatabaseProvider } from "../providers"; import { DatabaseProvider } from "../providers";
import type { Repository } from "../interfaces/repository.interface"; import type { Repository } from "../interfaces/repository.interface";
import { and, eq, gte, type InferInsertModel } from "drizzle-orm"; import { and, eq, gte, type InferInsertModel } from "drizzle-orm";
import { takeFirst } from "../infrastructure/database/utils"; import { takeFirst, takeFirstOrThrow } from "../infrastructure/database/utils";
import { loginRequestsTable } from "../infrastructure/database/tables/login-requests.table"; import { loginRequestsTable } from "../infrastructure/database/tables/login-requests.table";
export type CreateLoginRequest = Pick<InferInsertModel<typeof loginRequestsTable>, 'email' | 'expiresAt' | 'hashedToken'>; export type CreateLoginRequest = Pick<InferInsertModel<typeof loginRequestsTable>, 'email' | 'expiresAt' | 'hashedToken'>;
@ -15,7 +15,7 @@ export class LoginRequestsRepository implements Repository {
return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({ return this.db.insert(loginRequestsTable).values(data).onConflictDoUpdate({
target: loginRequestsTable.email, target: loginRequestsTable.email,
set: data set: data
}) }).returning().then(takeFirstOrThrow)
} }
async findOneByEmail(email: string) { async findOneByEmail(email: string) {

View file

@ -17,16 +17,16 @@ export class LoginRequestsService {
@inject(TokensService) private readonly tokensService: TokensService, @inject(TokensService) private readonly tokensService: TokensService,
@inject(MailerService) private readonly mailerService: MailerService, @inject(MailerService) private readonly mailerService: MailerService,
@inject(UsersRepository) private readonly usersRepository: UsersRepository, @inject(UsersRepository) private readonly usersRepository: UsersRepository,
@inject(LoginRequestsRepository) private readonly loginRequetsRepository: LoginRequestsRepository, @inject(LoginRequestsRepository) private readonly loginRequestsRepository: LoginRequestsRepository,
) { } ) { }
async create(data: RegisterEmailDto) { async create(data: RegisterEmailDto) {
// generate a token, expiry date, and hash // generate a token, expiry date, and hash
const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm'); const { token, expiry, hashedToken } = await this.tokensService.generateTokenWithExpiryAndHash(15, 'm');
// save the login request to the database - ensuring we save the hashedToken // save the login request to the database - ensuring we save the hashedToken
await this.loginRequetsRepository.create({ email: data.email, hashedToken, expiresAt: expiry }); await this.loginRequestsRepository.create({ email: data.email, hashedToken, expiresAt: expiry });
// send the login request email // send the login request email
this.mailerService.sendLoginRequest({ await this.mailerService.sendLoginRequest({
to: data.email, to: data.email,
props: { token: token } props: { token: token }
}); });
@ -57,7 +57,7 @@ export class LoginRequestsService {
private async fetchValidRequest(email: string, token: string) { private async fetchValidRequest(email: string, token: string) {
return await this.db.transaction(async (trx) => { return await this.db.transaction(async (trx) => {
// fetch the login request // fetch the login request
const loginRequest = await this.loginRequetsRepository.trxHost(trx).findOneByEmail(email) const loginRequest = await this.loginRequestsRepository.trxHost(trx).findOneByEmail(email)
if (!loginRequest) return null; if (!loginRequest) return null;
// check if the token is valid // check if the token is valid
@ -65,7 +65,7 @@ export class LoginRequestsService {
if (!isValidRequest) return null if (!isValidRequest) return null
// if the token is valid, burn the request // if the token is valid, burn the request
await this.loginRequetsRepository.trxHost(trx).deleteById(loginRequest.id); await this.loginRequestsRepository.trxHost(trx).deleteById(loginRequest.id);
return loginRequest return loginRequest
}) })
} }

View file

@ -0,0 +1,72 @@
import 'reflect-metadata';
import { LoginRequestsService } from '../services/login-requests.service';
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { TokensService } from '../services/tokens.service';
import { MailerService } from '../services/mailer.service';
import { UsersRepository } from '../repositories/users.repository';
import { DatabaseProvider, LuciaProvider } from '../providers';
import { LoginRequestsRepository } from '../repositories/login-requests.repository';
import { PgDatabase } from 'drizzle-orm/pg-core';
import { container } from 'tsyringe';
describe('LoginRequestService', () => {
let service: LoginRequestsService;
let tokensService = vi.mocked(TokensService.prototype)
let mailerService = vi.mocked(MailerService.prototype);
let usersRepository = vi.mocked(UsersRepository.prototype);
let loginRequestsRepository = vi.mocked(LoginRequestsRepository.prototype);
let luciaProvider = vi.mocked(LuciaProvider);
let databaseProvider = vi.mocked(PgDatabase);
beforeAll(() => {
service = container
.register<TokensService>(TokensService, { useValue: tokensService })
.register<MailerService>(MailerService, { useValue: mailerService })
.register<UsersRepository>(UsersRepository, { useValue: usersRepository })
.register(LoginRequestsRepository, { useValue: loginRequestsRepository })
.register(LuciaProvider, { useValue: luciaProvider })
.register(DatabaseProvider, { useValue: databaseProvider })
.resolve(LoginRequestsService);
});
afterAll(() => {
vi.resetAllMocks()
})
describe('Create', () => {
tokensService.generateTokenWithExpiryAndHash = vi.fn().mockResolvedValue({
token: "111",
expiry: new Date(),
hashedToken: "111"
} satisfies Awaited<ReturnType<typeof tokensService.generateTokenWithExpiryAndHash>>)
loginRequestsRepository.create = vi.fn().mockResolvedValue({
createdAt: new Date(),
email: 'me@test.com',
expiresAt: new Date(),
hashedToken: '111',
id: '1',
updatedAt: new Date()
} satisfies Awaited<ReturnType<typeof loginRequestsRepository.create>>)
mailerService.sendLoginRequest = vi.fn().mockResolvedValue(null)
const spy_mailerService_sendLoginRequest = vi.spyOn(mailerService, 'sendLoginRequest')
const spy_tokensService_generateTokenWithExpiryAndHash = vi.spyOn(tokensService, 'generateTokenWithExpiryAndHash')
const spy_loginRequestsRepository_create = vi.spyOn(loginRequestsRepository, 'create')
it('should resolve', async () => {
await expect(service.create({ email: "test" })).resolves.toBeUndefined()
})
it('should generate a token with expiry and hash', async () => {
expect(spy_tokensService_generateTokenWithExpiryAndHash).toBeCalledTimes(1)
})
it('should send an email with token', async () => {
expect(spy_mailerService_sendLoginRequest).toHaveBeenCalledTimes(1)
})
it('should create a new login request record', async () => {
expect(spy_loginRequestsRepository_create).toBeCalledTimes(1)
})
})
});

View file

@ -0,0 +1,3 @@
{
"status": "failed"
}