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

chore(application-generic): Modernize app generic #7424

Draft
wants to merge 3 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
9 changes: 2 additions & 7 deletions libs/application-generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,12 @@
"keywords": [],
"scripts": {
"start": "npm run watch:build",
"test": "vitest",
"prebuild": "rimraf build",
"build": "run-p build:*",
"build:main": "tsc -p tsconfig.json",
"build:copy-template": "cpx \"src/**/*.handlebars\" build/main",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"lint:fix": "eslint src --fix",
"lint": "eslint src",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "jest src --watch",
"reset-hard": "git clean -dfx && git reset --hard && pnpm install"
},
"engines": {
Expand Down Expand Up @@ -75,7 +71,7 @@
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"date-fns": "^2.29.2",
"got": "^11.8.6",
"got": "^14.4.5",
"handlebars": "^4.7.7",
"i18next": "^23.7.6",
"ioredis": "^5.2.4",
Expand Down Expand Up @@ -106,7 +102,6 @@
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@types/got": "^9.6.12",
"@types/jest": "29.5.2",
"@types/newrelic": "^9.14.6",
"@types/sanitize-html": "^2.11.0",
Expand Down
64 changes: 33 additions & 31 deletions libs/application-generic/src/commands/base.command.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { IsDefined, IsEmail, IsNotEmpty } from 'class-validator';
import sinon from 'sinon';
// eslint-disable-next-line import/no-namespace
import * as Sentry from '@sentry/node';
import { BadRequestException } from '@nestjs/common';

import { BaseCommand } from './base.command';
import { BaseCommand, CommandValidationException } from './base.command';

export class TestCommand extends BaseCommand {
@IsDefined()
Expand All @@ -16,47 +12,53 @@ export class TestCommand extends BaseCommand {
password: string;
}

describe('BaseCommand', function () {
beforeAll(() => {
sinon.stub(Sentry, 'addBreadcrumb');
describe('BaseCommand', () => {
it('should return object of type that extends the base', async () => {
const obj = { email: '[email protected]', password: 'P@ssw0rd' };
expect(TestCommand.create(obj)).toEqual(obj);
});

it('should throw BadRequestException with error messages when field values are not valid', async function () {
it('should throw CommandValidationException with error messages when field values are not valid', async () => {
try {
TestCommand.create({ email: undefined, password: undefined });
expect(false).toBeTruthy();
} catch (e) {
expect((e as BadRequestException).getResponse()).toEqual({
statusCode: 400,
error: 'Bad Request',
message: [
'email should not be null or undefined',
'email must be an email',
'email should not be empty',
'password should not be null or undefined',
],
expect((e as CommandValidationException).getResponse()).toEqual({
className: 'TestCommand',
constraintsViolated: {
email: {
messages: [
'email should not be null or undefined',
'email must be an email',
'email should not be empty',
],
value: undefined,
},
password: {
messages: ['password should not be null or undefined'],
value: undefined,
},
},
message: 'Validation failed',
});
}
});

it('should throw BadRequestException with error message when only one field is not valid', async function () {
it('should throw CommandValidationException with error message when only one field is not valid', async () => {
try {
TestCommand.create({ email: '[email protected]', password: undefined });
expect(false).toBeTruthy();
} catch (e) {
expect((e as BadRequestException).getResponse()).toEqual({
statusCode: 400,
error: 'Bad Request',
message: ['password should not be null or undefined'],
expect((e as CommandValidationException).getResponse()).toEqual({
className: 'TestCommand',
constraintsViolated: {
password: {
messages: ['password should not be null or undefined'],
value: undefined,
},
},
message: 'Validation failed',
});
}
});

it('should return object of type that extends the base', async function () {
const obj = { email: '[email protected]', password: 'P@ssw0rd' };
const res = TestCommand.create(obj);

expect(res instanceof TestCommand).toBeTruthy();
expect(res).toEqual(obj);
});
});
5 changes: 1 addition & 4 deletions libs/application-generic/src/commands/base.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ export abstract class BaseCommand {
this: new (...args: unknown[]) => T,
data: T,
): T {
const convertedObject = plainToInstance<T, unknown>(this, {
...data,
});

const convertedObject = plainToInstance<T, unknown>(this, data);
const errors = validateSync(convertedObject);
const flattenedErrors = flattenErrors(errors);
if (Object.keys(flattenedErrors).length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import { MockCacheService } from './cache-service.mock';
* TODO: Maybe create a Test single Redis instance to be able to run it in the
* pipeline. Local wise they work
*/
describe.skip('Cache Service - Redis Instance - Non Cluster Mode', () => {
describe('Cache Service - Redis Instance - Non Cluster Mode', () => {
let cacheService: CacheService;
let cacheInMemoryProviderService: CacheInMemoryProviderService;

beforeAll(async () => {
process.env.IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'false';

cacheInMemoryProviderService = new CacheInMemoryProviderService();
expect(cacheInMemoryProviderService.isCluster).toBe(false);
expect(cacheInMemoryProviderService.inMemoryProviderService.inCluster).toBe(
false,
);

cacheService = new CacheService(cacheInMemoryProviderService);
await cacheService.initialize();
Expand Down Expand Up @@ -70,15 +72,17 @@ describe.skip('Cache Service - Redis Instance - Non Cluster Mode', () => {
});
});

describe('Cache Service - Cluster Mode', () => {
describe.skip('Cache Service - Cluster Mode', () => {
let cacheService: CacheService;
let cacheInMemoryProviderService: CacheInMemoryProviderService;

beforeAll(async () => {
process.env.IS_IN_MEMORY_CLUSTER_MODE_ENABLED = 'true';

cacheInMemoryProviderService = new CacheInMemoryProviderService();
expect(cacheInMemoryProviderService.isCluster).toBe(true);
expect(cacheInMemoryProviderService.inMemoryProviderService.inCluster).toBe(
true,
);

cacheService = new CacheService(cacheInMemoryProviderService);
await cacheService.initialize();
Expand Down Expand Up @@ -144,9 +148,8 @@ describe('cache-service', function () {
cacheService = MockCacheService.createClient();
});

afterEach(function (done) {
afterEach(function () {
cacheService.delByPattern('*');
done();
});

it('should store data in cache', async function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import {
OrdinalEnum,
OrdinalValueEnum,
} from '@novu/shared';
import { vi } from 'vitest';

import { TimedDigestDelayService } from './timed-digest-delay.service';

describe('TimedDigestDelayService', () => {
describe('calculate', () => {
let clock: typeof jest;
let clock;

beforeEach(() => {
const date = new Date('2023-05-04T12:00:00Z');
clock = jest.useFakeTimers('modern' as FakeTimersConfig);
clock = vi.useFakeTimers('modern' as FakeTimersConfig);
clock.setSystemTime(date.getTime());
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import Redlock from 'redlock';
import { setTimeout } from 'timers/promises';

import { DistributedLockService } from './distributed-lock.service';
import { FeatureFlagsService } from '../feature-flags.service';
import {
InMemoryProviderClient,
InMemoryProviderEnum,
CacheInMemoryProviderService,
} from '../in-memory-provider';
import { CacheInMemoryProviderService } from '../in-memory-provider';
import { vi } from 'vitest';

// eslint-disable-next-line no-multi-assign
const originalRedisCacheServiceHost = (process.env.REDIS_CACHE_SERVICE_HOST =
Expand All @@ -19,16 +15,16 @@ const originalRedisClusterServiceHost = process.env.REDIS_CLUSTER_SERVICE_HOST;
const originalRedisClusterServicePorts =
process.env.REDIS_CLUSTER_SERVICE_PORTS;

const spyDecreaseLockCounter = jest.spyOn(
const spyDecreaseLockCounter = vi.spyOn(
DistributedLockService.prototype,
<any>'decreaseLockCounter',
);
const spyIncreaseLockCounter = jest.spyOn(
const spyIncreaseLockCounter = vi.spyOn(
DistributedLockService.prototype,
<any>'increaseLockCounter',
);
const spyLock = jest.spyOn(Redlock.prototype, 'acquire');
const spyUnlock = jest.spyOn(Redlock.prototype, 'release');
const spyLock = vi.spyOn(Redlock.prototype, 'acquire');
const spyUnlock = vi.spyOn(Redlock.prototype, 'release');

describe('Distributed Lock Service', () => {
afterEach(() => {
Expand Down Expand Up @@ -135,6 +131,7 @@ describe('Distributed Lock Service', () => {
});
});

// TODO: Revise the following tests as they are too slow to run at the moment without fake timers
describe('Functionalities', () => {
it('should create lock and it should expire after the TTL set', async () => {
const resource = 'lock-created';
Expand All @@ -153,7 +150,13 @@ describe('Distributed Lock Service', () => {
resourceExists = await client!.exists(resource);
expect(resourceExists).toEqual(0);

expect(spyLock).toHaveBeenNthCalledWith(1, [resource], TTL);
expect(spyLock).toHaveBeenNthCalledWith(1, [resource], TTL, {
automaticExtensionThreshold: 500,
driftFactor: 0.01,
retryCount: 50,
retryDelay: 100,
retryJitter: 200,
});
expect(spyIncreaseLockCounter).toHaveBeenCalledTimes(1);
// Unlock is still called even when the lock expires by going over TTL but errors
expect(spyUnlock).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -182,7 +185,13 @@ describe('Distributed Lock Service', () => {
expect(resourceExists).toEqual(0);
}

expect(spyLock).toHaveBeenNthCalledWith(1, [resource], TTL);
expect(spyLock).toHaveBeenNthCalledWith(1, [resource], TTL, {
automaticExtensionThreshold: 500,
driftFactor: 0.01,
retryCount: 50,
retryDelay: 100,
retryJitter: 200,
});
expect(spyIncreaseLockCounter).toHaveBeenCalledTimes(1);
expect(spyDecreaseLockCounter).toHaveBeenCalledTimes(1);
expect(spyUnlock).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -217,7 +226,13 @@ describe('Distributed Lock Service', () => {
secondCall: true,
});
expect(spyLock).toHaveBeenCalledTimes(2);
expect(spyLock).toHaveBeenCalledWith([resource], TTL);
expect(spyLock).toHaveBeenCalledWith([resource], TTL, {
automaticExtensionThreshold: 500,
driftFactor: 0.01,
retryCount: 50,
retryDelay: 100,
retryJitter: 200,
});
expect(spyIncreaseLockCounter).toHaveBeenCalledTimes(2);
expect(spyIncreaseLockCounter).toHaveBeenCalledWith(resource);
expect(spyDecreaseLockCounter).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -251,7 +266,13 @@ describe('Distributed Lock Service', () => {

expect(executed).toEqual(1);
expect(spyLock).toHaveBeenCalledTimes(5);
expect(spyLock).toHaveBeenCalledWith([resource], TTL);
expect(spyLock).toHaveBeenCalledWith([resource], TTL, {
automaticExtensionThreshold: 500,
driftFactor: 0.01,
retryCount: 50,
retryDelay: 100,
retryJitter: 200,
});
expect(spyIncreaseLockCounter).toHaveBeenCalledTimes(5);
expect(spyIncreaseLockCounter).toHaveBeenCalledWith(resource);
expect(spyUnlock.mock.calls.length).toBeGreaterThanOrEqual(5);
Expand Down Expand Up @@ -286,11 +307,17 @@ describe('Distributed Lock Service', () => {

expect(executed).toEqual(5);
expect(spyLock).toHaveBeenCalledTimes(5);
expect(spyLock).toHaveBeenCalledWith([resource], TTL);
expect(spyLock).toHaveBeenCalledWith([resource], TTL, {
automaticExtensionThreshold: 500,
driftFactor: 0.01,
retryCount: 50,
retryDelay: 100,
retryJitter: 200,
});
expect(spyIncreaseLockCounter).toHaveBeenCalledTimes(5);
expect(spyIncreaseLockCounter).toHaveBeenCalledWith(resource);
expect(spyUnlock.mock.calls.length).toBeGreaterThanOrEqual(5);
});
}, 10000);
});
});

Expand Down
Loading
Loading