feat: initialize the server e2e tests

This commit is contained in:
Ahmed Bouhuolia
2024-12-15 00:49:10 +02:00
parent 05f4b49b58
commit 2ba31148ca
10 changed files with 43 additions and 278 deletions

View File

@@ -1,29 +1,4 @@
import { AuthForgetPasswordService } from './AuthForgetPassword.service';
import { AuthSendResetPasswordService } from './AuthResetPassword.service';
import { AuthSigninService } from './AuthSignin.service';
import { AuthSignupService } from './AuthSignup.service';
export class AuthApplication {
constructor(
private readonly authSigninService: AuthSigninService,
private readonly authSignupService: AuthSignupService,
private readonly authResetPasswordService: AuthSendResetPasswordService,
private readonly authForgetPasswordService: AuthForgetPasswordService,
) {}
async signin(email: string, password: string) {
return this.authSigninService.signIn(email, password);
}
async signup(data: any) {
return this.authSignupService.signup(data);
}
async resetPassword(data: any) {
return this.authResetPasswordService.resetPassword(data);
}
async forgetPassword(data: any) {
return this.authForgetPasswordService.execute(data);
}
}

View File

@@ -1 +0,0 @@
export class AuthForgetPasswordService {}

View File

@@ -1,132 +0,0 @@
import { Injectable, Inject } from '@nestjs/common';
import uniqid from 'uniqid';
import moment from 'moment';
import config from '@/config';
import {
IAuthResetedPasswordEventPayload,
IAuthSendedResetPassword,
IAuthSendingResetPassword,
IPasswordReset,
ISystemUser,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { PasswordReset } from '@/system/models';
import { ERRORS } from './_constants';
import { ServiceError } from '@/exceptions';
import { hashPassword } from '@/utils';
@Injectable()
export class AuthSendResetPasswordService {
constructor(
private readonly eventPublisher: EventPublisher,
@Inject('SystemUserRepository') private readonly systemUserRepository: any,
@Inject('PasswordResetModel')
private readonly passwordResetModel: typeof PasswordReset,
) {}
/**
* Generates and retrieve password reset token for the given user email.
* @param {string} email
* @return {<Promise<IPasswordReset>}
*/
public async sendResetPassword(email: string): Promise<PasswordReset> {
const user = await this.validateEmailExistance(email);
const token: string = uniqid();
// Triggers sending reset password event.
await this.eventPublisher.emitAsync(events.auth.sendingResetPassword, {
user,
token,
} as IAuthSendingResetPassword);
// Delete all stored tokens of reset password that associate to the give email.
this.deletePasswordResetToken(email);
// Creates a new password reset row with unique token.
const passwordReset = await this.passwordResetModel
.query()
.insert({ email, token });
// Triggers sent reset password event.
await this.eventPublisher.emitAsync(events.auth.sendResetPassword, {
user,
token,
} as IAuthSendedResetPassword);
return passwordReset;
}
/**
* Resets a user password from given token.
* @param {string} token - Password reset token.
* @param {string} password - New Password.
* @return {Promise<void>}
*/
public async resetPassword(token: string, password: string): Promise<void> {
// Finds the password reset token.
const tokenModel: IPasswordReset = await this.passwordResetModel
.query()
.findOne('token', token);
// In case the password reset token not found throw token invalid error..
if (!tokenModel) {
throw new ServiceError(ERRORS.TOKEN_INVALID);
}
// Different between token creation datetime and current time.
if (
moment().diff(tokenModel.createdAt, 'seconds') >
config.resetPasswordSeconds
) {
// Deletes the expired token by expired token email.
await this.deletePasswordResetToken(tokenModel.email);
throw new ServiceError(ERRORS.TOKEN_EXPIRED);
}
const user = await this.systemUserRepository.findOneByEmail(
tokenModel.email,
);
if (!user) {
throw new ServiceError(ERRORS.USER_NOT_FOUND);
}
const hashedPassword = await hashPassword(password);
await this.systemUserRepository.update(
{ password: hashedPassword },
{ id: user.id },
);
// Deletes the used token.
await this.deletePasswordResetToken(tokenModel.email);
// Triggers `onResetPassword` event.
await this.eventPublisher.emitAsync(events.auth.resetPassword, {
user,
token,
password,
} as IAuthResetedPasswordEventPayload);
}
/**
* Deletes the password reset token by the given email.
* @param {string} email
* @returns {Promise}
*/
private async deletePasswordResetToken(email: string) {
return this.passwordResetModel.query().where('email', email).delete();
}
/**
* Validates the given email existance on the storage.
* @throws {ServiceError}
* @param {string} email - email address.
*/
private async validateEmailExistance(email: string): Promise<ISystemUser> {
const userByEmail = await this.systemUserRepository.findOneByEmail(email);
if (!userByEmail) {
throw new ServiceError(ERRORS.EMAIL_NOT_FOUND);
}
return userByEmail;
}
}

View File

@@ -1,88 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { cloneDeep } from 'lodash';
import { SystemUser } from '../System/models/SystemUser';
import { TenantModel } from '../System/models/TenantModel';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { ServiceError } from '../Items/ServiceError';
import { ERRORS } from '../Items/Items.constants';
import {
IAuthSignedInEventPayload,
IAuthSigningInEventPayload,
IAuthSignInPOJO,
} from './Auth.interfaces';
@Injectable()
export class AuthSigninService {
constructor(
private readonly eventEmitter: EventEmitter2,
@Inject(SystemUser.name)
private readonly systemUserModel: typeof SystemUser,
@Inject(TenantModel.name)
private readonly tenantModel: typeof TenantModel,
) {}
/**
* Validates the given email and password.
* @param {ISystemUser} user
*/
public async validateSignIn(user: SystemUser) {
// Validate if the given user is inactive.
if (!user.active) {
throw new ServiceError(ERRORS.USER_INACTIVE);
}
}
/**
* sign-in and generates JWT token.
* @param {string} email - Email address.
* @param {string} password - Password.
* @return {Promise<{user: IUser, token: string}>}
*/
public async signIn(
email: string,
password: string,
): Promise<IAuthSignInPOJO> {
// Finds the user of the given email address.
const user = await SystemUser.query()
.findOne('email', email)
.modify('inviteAccepted');
// Validate the given email and password.
await this.validateSignIn(user);
// Triggers on signing-in event.
await this.eventEmitter.emitAsync(events.auth.signingIn, {
email,
password,
user,
} as IAuthSigningInEventPayload);
const token = generateToken(user);
// Update the last login at of the user.
// await systemUserRepository.patchLastLoginAt(user.id);
// Triggers `onSignIn` event.
await this.eventEmitter.emitAsync(events.auth.signIn, {
email,
password,
user,
} as IAuthSignedInEventPayload);
const tenant = await this.tenantModel
.query()
.findById(user.tenantId)
.withGraphFetched('metadata');
// Keep the user object immutable.
const outputUser = cloneDeep(user);
// Remove password property from user object.
Reflect.deleteProperty(outputUser, 'password');
return { user: outputUser, token, tenant };
}
}

View File

@@ -1 +0,0 @@
export class AuthSignupService {}

View File

@@ -1,24 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/modules/App/App.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

View File

@@ -0,0 +1,20 @@
import { INestApplication } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../src/modules/App/App.module';
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
export { app };

View File

@@ -0,0 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { faker } from '@faker-js/faker';
import { AppModule } from '../src/modules/App/App.module';
import { app } from './init-app-test';
describe('Items (e2e)', () => {
it('/items (POST)', () => {
return request(app.getHttpServer())
.post('/items')
.set('organization-id', '4064541lv40nhca')
.send({
name: faker.commerce.productName(),
type: 'service',
})
.expect(201);
});
});

View File

@@ -5,5 +5,8 @@
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/../src/$1"
}
}

View File

@@ -23,7 +23,6 @@
"@reduxjs/toolkit": "^1.2.5",
"@stripe/connect-js": "^3.3.12",
"@stripe/react-connect-js": "^3.3.13",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1",
"@tiptap/core": "2.1.13",
@@ -33,7 +32,6 @@
"@tiptap/pm": "2.1.13",
"@tiptap/react": "2.1.13",
"@tiptap/starter-kit": "2.1.13",
"@types/jest": "^26.0.15",
"@types/js-money": "^0.6.1",
"@types/lodash": "^4.14.172",
"@types/node": "^14.14.9",
@@ -68,10 +66,6 @@
"helmet": "^3.21.0",
"history": "4.10.1",
"http-proxy-middleware": "^1.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"js-cookie": "2.2.1",
"js-money": "^0.6.3",
"lodash": "^4.17.15",