Skip to content

Commit

Permalink
refactor(application-generic): Modernize it
Browse files Browse the repository at this point in the history
- Convert it to ESM module
- Replace jest with vitest
- Restore all failing unit tests
  • Loading branch information
SokratisVidros committed Jan 1, 2025
1 parent 2f4dba2 commit 0baf2e1
Show file tree
Hide file tree
Showing 46 changed files with 300 additions and 937 deletions.
File renamed without changes.
3 changes: 1 addition & 2 deletions libs/application-generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +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",

"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
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: 'test@test.com', 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: 'test@test.com', 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: 'test@test.com', 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
Original file line number Diff line number Diff line change
@@ -1,71 +1,16 @@
import { Logger } from '@nestjs/common';

import { InMemoryProviderService } from './in-memory-provider.service';
import {
InMemoryProviderEnum,
InMemoryProviderClient,
ScanStream,
} from './types';

import { GetIsInMemoryClusterModeEnabled } from '../../usecases';
import { InMemoryProviderClient, ScanStream } from './types';

const LOG_CONTEXT = 'CacheInMemoryProviderService';

// TODO: This is an unecessary wrapping class. Replace it with InMemoryProviderService across the codebase.
export class CacheInMemoryProviderService {
public inMemoryProviderService: InMemoryProviderService;
public isCluster: boolean;
private getIsInMemoryClusterModeEnabled: GetIsInMemoryClusterModeEnabled;

constructor() {
this.getIsInMemoryClusterModeEnabled =
new GetIsInMemoryClusterModeEnabled();

const provider = this.selectProvider();
this.isCluster = this.isClusterMode();

const enableAutoPipelining =
process.env.REDIS_CACHE_ENABLE_AUTOPIPELINING === 'true';

this.inMemoryProviderService = new InMemoryProviderService(
provider,
this.isCluster,
enableAutoPipelining,
);
}

/**
* Rules for the provider selection:
* - For our self hosted users we assume all of them have a single node Redis
* instance.
* - For Novu we will use Elasticache. We fallback to a Redis Cluster configuration
* if Elasticache not configured properly. That's happening in the provider
* mapping in the /in-memory-provider/providers/index.ts
*/
private selectProvider(): InMemoryProviderEnum {
if (process.env.IS_SELF_HOSTED) {
return InMemoryProviderEnum.REDIS;
}

return InMemoryProviderEnum.ELASTICACHE;
}

private descriptiveLogMessage(message) {
return `[Provider: ${this.selectProvider()}] ${message}`;
}

private isClusterMode(): boolean {
const isClusterModeEnabled = this.getIsInMemoryClusterModeEnabled.execute();

Logger.log(
this.descriptiveLogMessage(
`Cluster mode ${
isClusterModeEnabled ? 'IS' : 'IS NOT'
} enabled for ${LOG_CONTEXT}`,
),
LOG_CONTEXT,
);

return isClusterModeEnabled;
this.inMemoryProviderService = new InMemoryProviderService({
logContext: LOG_CONTEXT,
});
}

public async initialize(): Promise<void> {
Expand All @@ -92,13 +37,6 @@ export class CacheInMemoryProviderService {
return this.inMemoryProviderService.isClientReady();
}

public providerInUseIsInClusterMode(): boolean {
const providerConfigured =
this.inMemoryProviderService.getProvider.configured;

return this.isCluster || providerConfigured !== InMemoryProviderEnum.REDIS;
}

public async shutdown(): Promise<void> {
await this.inMemoryProviderService.shutdown();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ let inMemoryProviderService: InMemoryProviderService;
describe('In-memory Provider Service', () => {
describe('Non cluster mode', () => {
beforeEach(async () => {
inMemoryProviderService = new InMemoryProviderService(
InMemoryProviderEnum.REDIS,
false,
);
inMemoryProviderService = new InMemoryProviderService();

await inMemoryProviderService.delayUntilReadiness();

Expand Down Expand Up @@ -78,12 +75,12 @@ describe('In-memory Provider Service', () => {
});
});

describe('Cluster mode', () => {
// TODO: Fix these test by spawiing a redis cluster locally
describe.skip('Cluster mode', () => {
beforeEach(async () => {
inMemoryProviderService = new InMemoryProviderService(
InMemoryProviderEnum.REDIS,
true,
);
inMemoryProviderService = new InMemoryProviderService({
inCluster: true,
});
await inMemoryProviderService.delayUntilReadiness();

expect(inMemoryProviderService.getStatus()).toEqual('ready');
Expand All @@ -95,11 +92,9 @@ describe('In-memory Provider Service', () => {

describe('TEMP: Check if enableAutoPipelining true is set properly in Cluster', () => {
it('enableAutoPipelining is enabled', async () => {
const clusterWithPipelining = new InMemoryProviderService(
InMemoryProviderEnum.REDIS,
true,
true,
);
const clusterWithPipelining = new InMemoryProviderService({
inCluster: true,
});
await clusterWithPipelining.delayUntilReadiness();

expect(clusterWithPipelining.getStatus()).toEqual('ready');
Expand Down
Loading

0 comments on commit 0baf2e1

Please sign in to comment.