Skip to content

Commit

Permalink
Test/unit (#54)
Browse files Browse the repository at this point in the history
* test(unit): add exceptions

* test(unit): add template.util

* test(unit): add queue.util

* test(unit): add middleware

* test(unit): add tests by date.util

* refactor(service-core): remove logger from constructor

* test(unit): add unit test for the apple service

* test(unit): add unit test for the facebook service

* test(unit): add unit test for the github service

* test(unit): add unit test for the google service

* test(unit): add unit test for email service
  • Loading branch information
neverovski authored Dec 8, 2023
1 parent 7eaee37 commit 63fd5e4
Show file tree
Hide file tree
Showing 58 changed files with 2,234 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"rest-client.rememberCookiesForSubsequentRequests": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"eslint.workingDirectories": ["/src", "/tests"],
"npm.exclude": ["**/dist"],
Expand Down
7 changes: 4 additions & 3 deletions jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@
"testMatch": ["<rootDir>/tests/**/*.test.ts"],
"coverageDirectory": "coverage",
"moduleNameMapper": {
"__mocks__/(.*)": "<rootDir>/tests/__mocks__/$1",
"@common/(.*)": "<rootDir>/src/common/$1",
"@config": "<rootDir>/src/config",
"@config/(.*)": "<rootDir>/src/config/$1",
"@core": "<rootDir>/src/core",
"@config": "<rootDir>/src/config",
"@core/(.*)": "<rootDir>/src/core/$1",
"@database": "<rootDir>/src/database",
"@core": "<rootDir>/src/core",
"@database/(.*)": "<rootDir>/src/database/$1",
"@database": "<rootDir>/src/database",
"@i18n": "<rootDir>/src/i18n",
"@middleware/(.*)": "<rootDir>/src/middleware/$1",
"@modules/(.*)": "<rootDir>/src/modules/$1",
Expand Down
2 changes: 1 addition & 1 deletion src/@types/kernel.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type PagePayload = {
page: number;
};

type DateCtx = string | number | Date;
type FlexibleDate = string | number | Date;

type DeepPartial<T> = T extends object
? {
Expand Down
2 changes: 2 additions & 0 deletions src/common/constants/common.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const HASH_ENCODING = 'hex';
export const BIG_INT = Number.MAX_SAFE_INTEGER;

export const AUTH_REFRESH_LINK = '/api/v1/auth/refresh';

export const TIMEZONE_UTC = 'UTC';
128 changes: 77 additions & 51 deletions src/common/utils/date.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
addMonths as fnsAddMonths,
endOfDay as fnsEndOfDay,
format as fnsFormat,
formatISO as fnsFormatISO,
getUnixTime as fnsGetUnixTime,
isAfter as fnsIsAfter,
isBefore as fnsIsBefore,
Expand All @@ -13,98 +12,125 @@ import {
parseISO as fnsParseISO,
startOfDay as fnsStartOfDay,
} from 'date-fns';
import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz';
import ms from 'ms';

import { DEFAULT_FORMAT_DATE } from '@common/constants';
import { DEFAULT_FORMAT_DATE, TIMEZONE_UTC } from '@common/constants';

//TODO: should be work only with UTC time
export class DateUtil {
static addMillisecondToDate(date?: DateCtx, amount?: number): Date {
return fnsAddMilliseconds(DateUtil.transformDateToISO(date), amount || 0);
}
static addMillisecondToDate(date: FlexibleDate, amount?: number): Date {
const dateISO = this.parseISO(date);

static addMonths(date?: DateCtx, amount?: number): Date {
return fnsAddMonths(DateUtil.transformDateToISO(date), amount || 0);
return fnsAddMilliseconds(dateISO, amount || 0);
}

static endOfDay(date?: DateCtx | null) {
date = DateUtil.transformDateToISO(date || new Date());
static addMonths(date: FlexibleDate, amount?: number): Date {
const dateISO = this.parseISO(date);

return fnsEndOfDay(date);
return fnsAddMonths(dateISO, amount || 0);
}

static formatISO(date?: DateCtx) {
date = (DateUtil.isValid(date) ? date : new Date()) as DateCtx;
static endOfDay(date: FlexibleDate) {
const dateISO = this.parseISO(date);

return fnsFormatISO(DateUtil.parseISO(date));
return fnsEndOfDay(dateISO);
}

static isBetweenDay(
from: DateCtx,
to?: DateCtx | null,
date?: DateCtx | null,
dateFrom: FlexibleDate,
dateTo: FlexibleDate,
date: FlexibleDate,
) {
from = DateUtil.startOfDay(from);
to = DateUtil.endOfDay(to);
date = DateUtil.transformDateToISO(date || new Date());
const dateStartUtc = this.timeZoneToUTC(this.startOfDay(dateFrom));
const dateEndUtc = this.timeZoneToUTC(this.endOfDay(dateTo));
const dateISOUtc = this.parseISO(date);

return (
(fnsIsEqual(from, date) || fnsIsBefore(from, date)) &&
(fnsIsEqual(to, date) || fnsIsAfter(to, date))
(fnsIsEqual(dateStartUtc, dateISOUtc) ||
fnsIsBefore(dateStartUtc, dateISOUtc)) &&
(fnsIsEqual(dateEndUtc, dateISOUtc) || fnsIsAfter(dateEndUtc, dateISOUtc))
);
}

static isSameDay(dateLeft?: DateCtx, dateRight?: DateCtx): boolean {
if (DateUtil.isValid(dateLeft) && DateUtil.isValid(dateRight)) {
return fnsIsSameDay(
DateUtil.parseISO(dateLeft as DateCtx),
DateUtil.parseISO(dateRight as DateCtx),
);
static isSameDay(dateLeft: FlexibleDate, dateRight: FlexibleDate): boolean {
if (!this.isValid(dateLeft) || !this.isValid(dateRight)) {
return false;
}

return false;
return fnsIsSameDay(this.parseISO(dateLeft), this.parseISO(dateRight));
}

static isSameOrBeforeDay(from?: DateCtx | null, to?: DateCtx | null) {
from = DateUtil.startOfDay(from || new Date());
to = DateUtil.startOfDay(to || new Date());
static isSameOrBeforeDay(dateFrom: FlexibleDate, dateTo: FlexibleDate) {
const dateStartFrom = this.startOfDay(dateFrom);
const dateStartTo = this.startOfDay(dateTo);

return fnsIsEqual(from, to) || fnsIsBefore(from, to);
return (
fnsIsEqual(dateStartFrom, dateStartTo) ||
fnsIsBefore(dateStartFrom, dateStartTo)
);
}

static isValid(date?: DateCtx) {
return fnsIsValid(typeof date === 'string' ? Date.parse(date) : date);
}
static parseISO(date: FlexibleDate) {
if (!date || !this.isValid(date)) {
throw new Error('Invalid Date');
}

static parseISO(date: DateCtx) {
return typeof date === 'string' ? fnsParseISO(date) : date;
}

static startOfDay(date?: DateCtx | null) {
date = DateUtil.transformDateToISO(date || new Date());
static parseStringToMs(str: string): number {
if (!str) {
return 0;
}

return fnsStartOfDay(date);
return ms(str) || 0;
}

static toDate(date: DateCtx) {
return DateUtil.transformDateToISO(date);
static startOfDay(date: FlexibleDate) {
const dateISO = this.parseISO(date);

return fnsStartOfDay(dateISO);
}

// FIXME: https://github.com/date-fns/date-fns/issues/2151
static toFormat(date?: DateCtx, format = DEFAULT_FORMAT_DATE) {
return fnsFormat(DateUtil.transformDateToISO(date), format);
static timeZoneToUTC(date: FlexibleDate, tz = TIMEZONE_UTC) {
try {
const dateUtc = zonedTimeToUtc(date, tz);

if (!this.isValid(dateUtc)) {
throw new Error();
}

return dateUtc;
} catch {
throw new Error('Invalid Date');
}
}

static toMs(input: string): number {
return ms(input);
static toFormat(date: FlexibleDate, format = DEFAULT_FORMAT_DATE) {
const dateISO = this.parseISO(date);

return fnsFormat(dateISO, format);
}

static toUnix(date?: DateCtx): number {
return fnsGetUnixTime(DateUtil.transformDateToISO(date));
static toFormatUTC(localDate: FlexibleDate, format = DEFAULT_FORMAT_DATE) {
const dateISO = this.parseISO(localDate);
const date = utcToZonedTime(dateISO, TIMEZONE_UTC);

return fnsFormat(date, format);
}

static transformDateToISO(date?: DateCtx) {
date = (DateUtil.isValid(date) ? date : new Date()) as DateCtx;
static toUnix(date: FlexibleDate): number {
const dateISO = this.parseISO(date);

return DateUtil.parseISO(date);
return fnsGetUnixTime(dateISO);
}

private static isValid(date?: FlexibleDate) {
try {
return fnsIsValid(typeof date === 'string' ? Date.parse(date) : date);
} catch {
throw new Error('Invalid Date');
}
}
}
6 changes: 5 additions & 1 deletion src/common/utils/queue.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { IRedisConfig } from '@config';

export class QueueUtil {
static connect<T>(name: string, redisConfig: IRedisConfig) {
if (!name) throw new Error('name is required');
if (!redisConfig.host) throw new Error('redisConfig.host is required');
if (!redisConfig.port) throw new Error('redisConfig.port is required');

return new Bull<T>(name, {
redis: this.getRedisOptions(redisConfig),
prefix: redisConfig.queuePrefix,
Expand All @@ -20,7 +24,7 @@ export class QueueUtil {
host: redisConfig.host,
port: redisConfig.port,
...(redisConfig.username && { username: redisConfig.username }),
...(redisConfig.username && { password: redisConfig.password }),
...(redisConfig.password && { password: redisConfig.password }),
...(redisConfig.tls && {
tls: {},
connectTimeout: 30000,
Expand Down
2 changes: 1 addition & 1 deletion src/config/interface/redis.config.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface IRedisConfig {
clusterModeEnabled: boolean;
clusterModeEnabled?: boolean;
host: string;
password?: string;
port: number;
Expand Down
2 changes: 1 addition & 1 deletion src/core/controller.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class ControllerCore {
}

private getCookieMaxAge(options: Partial<CookieParam>) {
const maxAge = DateUtil.toMs(options?.expiresIn || '');
const maxAge = DateUtil.parseStringToMs(options?.expiresIn || '');

if (options?.maxAge || options?.rememberMe) {
return { maxAge: options.maxAge ?? maxAge };
Expand Down
27 changes: 17 additions & 10 deletions src/core/service/provider.service.core.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
import { container as Container } from 'tsyringe';

import { LoggerCtx } from '@common/enums';
import { InternalServerErrorException } from '@common/exceptions';
import { ILoggerService, LoggerInject } from '@providers/logger';
import type { ILoggerService } from '@providers/logger';

export class ProviderServiceCore {
protected readonly logger: ILoggerService;
private readonly loggerCtx: LoggerCtx;

constructor(loggerCtx?: LoggerCtx) {
this.loggerCtx = loggerCtx || LoggerCtx.SERVICE;
this.logger = Container.resolve<ILoggerService>(LoggerInject.SERVICE);
protected readonly logger?: ILoggerService;

constructor() {
this.init();
}

protected get loggerCtx(): LoggerCtx {
return LoggerCtx.SERVICE;
}

protected handleError(err: unknown) {
this.logger.error(this.constructor.name, { err, context: this.loggerCtx });
if (this.logger) {
this.logger.error(this.constructor.name, {
err,
context: this.loggerCtx,
});
}

return new InternalServerErrorException();
}

protected init() {
if (!this.logger) {
return;
}

this.logger.debug(`${this.constructor.name} initialized...`, {
context: this.loggerCtx,
});
Expand Down
24 changes: 15 additions & 9 deletions src/core/service/service.core.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { container as Container } from 'tsyringe';

import { LoggerCtx } from '@common/enums';
import { ILoggerService, LoggerInject } from '@providers/logger';
import { ILoggerService } from '@providers/logger';

export class ServiceCore {
protected readonly logger: ILoggerService;
private readonly loggerCtx: LoggerCtx;

constructor(loggerCtx?: LoggerCtx) {
this.loggerCtx = loggerCtx || LoggerCtx.SERVICE;
this.logger = Container.resolve<ILoggerService>(LoggerInject.SERVICE);
protected readonly logger?: ILoggerService;

constructor() {
this.init();
}

protected get loggerCtx(): LoggerCtx {
return LoggerCtx.SERVICE;
}

protected handleError(err: unknown) {
if (!this.logger) {
return;
}

this.logger.error(err, {
name: this.constructor.name,
context: this.loggerCtx,
});
}

protected init() {
if (!this.logger) {
return;
}

this.logger.debug(`${this.constructor.name} initialized...`, {
context: this.loggerCtx,
});
Expand Down
10 changes: 8 additions & 2 deletions src/modules/auth/service/auth-token.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
IRefreshTokenService,
RefreshTokenInject,
} from '@modules/refresh-token';
import { ILoggerService, LoggerInject } from '@providers/logger';
import { ITokenService, TokenInject } from '@providers/token';

//TODO: transfer refreshToken to redis
@Injectable()
export class AuthTokenService extends ServiceCore {
constructor(
@Inject(ConfigKey.JWT) private jwtConfig: IJwtConfig,
@Inject(LoggerInject.SERVICE) protected readonly logger: ILoggerService,
@Inject(RefreshTokenInject.SERVICE)
private refreshTokenService: IRefreshTokenService,
@Inject(TokenInject.SERVICE) private tokenService: ITokenService,
Expand All @@ -25,7 +27,9 @@ export class AuthTokenService extends ServiceCore {

getAccessToken(userId: Id, payload: UserPayload): Promise<string> {
const jti = HashUtil.generateUuid();
const expiresIn = DateUtil.toMs(this.jwtConfig.accessToken.expiresIn);
const expiresIn = DateUtil.parseStringToMs(
this.jwtConfig.accessToken.expiresIn,
);

return this.tokenService.signJwt(
{ ...payload, jti, sub: String(userId), typ: TokenType.BEARER },
Expand All @@ -36,7 +40,9 @@ export class AuthTokenService extends ServiceCore {

async getRefreshToken(userId: Id): Promise<string> {
const jti = HashUtil.generateUuid();
const expiresIn = DateUtil.toMs(this.jwtConfig.refreshToken.expiresIn);
const expiresIn = DateUtil.parseStringToMs(
this.jwtConfig.refreshToken.expiresIn,
);
const expiredAt = DateUtil.addMillisecondToDate(new Date(), expiresIn);

const [refreshToken] = await Promise.all([
Expand Down
Loading

0 comments on commit 63fd5e4

Please sign in to comment.