Skip to content

Commit

Permalink
test(notifier): create tests for notifier and its redis helper (#338)
Browse files Browse the repository at this point in the history
* update lock file

* added tests for worker and redisHelper

* typo

* imp(notifier): improved testcase of the redis helper

* fix(notifier-tests): afterall quit from redis

* test(notifier-validator): remove check what to receive validation

* feat(notifier-types): what to reveive All changed to SeenMore

* test(notifier): restore RedisHelper original implementation before each test

Added new testcases
- should send event to channels at most once in one threshold period for one fitted rule, no matter how much events have passed
- should compute event count for period for each fitted rule

* revert: remove check what to receive validator

* test(notifier): add testcase for new events

* feat(notifier): add check for new events

* lint fix

* fix tests

* test(notifier-worker): improve readability

* test(notifier-worker): improve testcase

* test(notifier-worker): improve testacase description

* test(notifier-worker): improve testcase readability

* test(notifier-worker): improve test description

* but(): all bugs fixed

* feat(): all features added

* chore(): restore all bugs

* bug(notifier): fix bug with channel enabled validation

* update yarn.lock

* fix tests

* imp(notifier): updater redis structure key format

* Update extensions.ts

* improve docs

* remove empty lines

* fix(notifier): fix notifier redisHelper tests

* fix(notifer): notifier redisHelpers tests fix

* fix(notifier): fix notifier redisHelper test

---------

Co-authored-by: Peter Savchenko <specc.dev@gmail.com>
  • Loading branch information
e11sy and neSpecc authored Feb 2, 2025
1 parent bb8938b commit 0ef30b2
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 55 deletions.
5 changes: 2 additions & 3 deletions workers/email/src/templates/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,12 @@ Twig.extendFilter('prettyPath', (value = ''): string => {
* @param {number} maxLen - max length of string
* @returns {string}
*/
Twig.extendFilter('leftTrim', (value: string, maxLen: number): string => {
Twig.extendFilter('leftTrim', ((value: string, maxLen: number): string => {
if (value.length > maxLen) {
return '…' + value.slice(value.length - maxLen);
}

return value;
});
}) as unknown as (value: any, params: false | any[]) => string); // tmp case. We need to check if TS says correct types or our implementation is correct

/**
* Prettify time to show in 'DD days HH hours MM minutes"
Expand Down
5 changes: 4 additions & 1 deletion workers/limiter/tests/plans.mock.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { PlanDBScheme } from '@hawk.so/types';
import { ObjectId } from 'mongodb';

/**
* Mocked plans with different events limits
*/
export const mockedPlans = {
export const mockedPlans: { [key: string]: PlanDBScheme } = {
/**
* Plan #1 with small limit
*/
eventsLimit10: {
_id: new ObjectId('5e4ff528628a6c714515f4dc'),
name: 'Test plan #1',
monthlyCharge: 10,
monthlyChargeCurrency: 'RUB',
eventsLimit: 10,
isDefault: true,
},
Expand All @@ -22,6 +24,7 @@ export const mockedPlans = {
_id: new ObjectId('5e4ff528738a6c714515f4dc'),
name: 'Test plan #2',
monthlyCharge: 10,
monthlyChargeCurrency: 'RUB',
eventsLimit: 10000,
isDefault: false,
},
Expand Down
17 changes: 13 additions & 4 deletions workers/notifier/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Worker } from '../../../lib/worker';
import * as pkg from '../package.json';
import { Channel, ChannelKey, SenderData } from '../types/channel';
import { NotifierEvent, NotifierWorkerTask } from '../types/notifier-task';
import { Rule } from '../types/rule';
import { Rule, WhatToReceive } from '../types/rule';
import { SenderWorkerTask } from 'hawk-worker-sender/types/sender-task';
import RuleValidator from './validator';
import Time from '../../../lib/utils/time';
Expand Down Expand Up @@ -73,7 +73,16 @@ export default class NotifierWorker extends Worker {
return;
}

const currentEventCount = await this.redis.computeEventCountForPeriod(projectId, rule._id.toString(), event.groupHash, rule.eventThresholdPeriod);
/**
* If validation for rule with whatToReceive.New passed, then event is new and we can send it to channels
*/
if (rule.whatToReceive === WhatToReceive.New) {
await this.sendEventsToChannels(projectId, rule, event);

return;
}

const currentEventCount = await this.redis.computeEventCountForPeriod(projectId, rule._id.toString(), event.groupHash, rule.thresholdPeriod);

/**
* If threshold reached, then send event to channels
Expand Down Expand Up @@ -136,11 +145,11 @@ export default class NotifierWorker extends Worker {
* If channel is disabled by user, do not add event to it
*/
if (!options.isEnabled) {
return;
continue;
}

const channelKey: ChannelKey = [projectId, rule._id.toString(), name];

await this.sendToSenderWorker(channelKey, [ {
key: event.groupHash,
count: 1,
Expand Down
16 changes: 8 additions & 8 deletions workers/notifier/src/redisHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class RedisHelper {
private readonly redisClient: RedisClientType;

/**
*
* Create redis client and add error handler to it
*/
constructor() {
this.redisClient = createClient({ url: process.env.REDIS_URL });
Expand Down Expand Up @@ -48,19 +48,19 @@ export default class RedisHelper {
* @returns {number} current event count
*/
public async computeEventCountForPeriod(
projectId: string,
ruleId: string,
groupHash: NotifierEvent['groupHash'],
thresholdPeriod: Rule['eventThresholdPeriod']
projectId: string,
ruleId: string,
groupHash: NotifierEvent['groupHash'],
thresholdPeriod: Rule['thresholdPeriod']
): Promise<number> {
const script = `
local key = KEYS[1]
local currentTimestamp = tonumber(ARGV[1])
local thresholdExpirationPeriod = tonumber(ARGV[2])
local thresholdPeriod = tonumber(ARGV[2])
local startPeriodTimestamp = tonumber(redis.call("HGET", key, "timestamp"))
if ((startPeriodTimestamp == nil) or (currentTimestamp >= startPeriodTimestamp + thresholdExpirationPeriod)) then
if ((startPeriodTimestamp == nil) or (currentTimestamp >= startPeriodTimestamp + thresholdPeriod)) then
redis.call("HSET", key, "timestamp", currentTimestamp)
redis.call("HSET", key, "eventsCount", 0)
end
Expand All @@ -75,7 +75,7 @@ export default class RedisHelper {

const currentEventCount = await this.redisClient.eval(script, {
keys: [ key ],
arguments: [currentTimestamp.toString(), (thresholdPeriod).toString()],
arguments: [currentTimestamp.toString(), thresholdPeriod.toString()],
}) as number;

return (currentEventCount !== null) ? currentEventCount : 0;
Expand Down
4 changes: 2 additions & 2 deletions workers/notifier/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Rule } from '../types/rule';
* WhatToReceive property values
*/
export enum WhatToReceive {
All = 'ALL',
SeenMore = 'SEEN_MORE',
New = 'ONLY_NEW',
}

Expand Down Expand Up @@ -62,7 +62,7 @@ export default class RuleValidator {
*/
public checkWhatToReceive(): RuleValidator {
const { rule, event } = this;
const result = rule.whatToReceive === WhatToReceive.All ||
const result = rule.whatToReceive === WhatToReceive.SeenMore ||
(event.isNew && rule.whatToReceive === WhatToReceive.New);

if (!result) {
Expand Down
98 changes: 98 additions & 0 deletions workers/notifier/tests/redisHelper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import RedisHelper from '../src/redisHelper';
import { createClient, RedisClientType } from 'redis';

describe('RedisHelper', () => {
let redisHelper: RedisHelper;
let redisClientMock: jest.Mocked<ReturnType<typeof createClient>>;
let redisClient: RedisClientType;

beforeAll(async () => {
redisHelper = new RedisHelper();
redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
redisClientMock = Reflect.get(redisHelper, 'redisClient') as jest.Mocked<ReturnType<typeof createClient>>;
});

afterAll(async () => {
await redisClient.quit();
});

describe('initialize', () => {
it('should connect to redis client', async () => {
const connect = jest.spyOn(redisClientMock, 'connect');

await redisHelper.initialize();

expect(connect).toHaveBeenCalled();
});
});

describe('close', () => {
it('should close redis client', async () => {
const quit = jest.spyOn(redisClientMock, 'quit');

await redisHelper.close();

expect(quit).toHaveBeenCalled();
});

it('should not throw error on close if client is already closed', async () => {
const quit = jest.spyOn(redisClientMock, 'quit');

quit.mockClear();

await redisHelper.close();
await redisHelper.close();
await redisHelper.close();

expect(quit).toHaveBeenCalledTimes(0);
});
});

describe('computeEventCountForPeriod', () => {
beforeEach(async () => {
await redisHelper.initialize();
});

afterEach(async () => {
await redisHelper.close();
});

it('should return event count', async () => {
const ruleId = 'ruleId';
const groupHash = 'groupHash';
const projectId = 'projectId';
const thresholdPeriod = 1000;

const currentEventCount = await redisHelper.computeEventCountForPeriod(projectId, ruleId, groupHash, thresholdPeriod);

expect(currentEventCount).toBe(1);
});

it('should reset counter and timestamp if threshold period is expired', async () => {
const ruleId = 'ruleId';
const currentDate = Date.now();
const groupHash = 'groupHash';
const projectId = 'projectId';
const thresholdPeriod = 1000;

/**
* Send several events to increment counter
*/
await redisHelper.computeEventCountForPeriod(projectId, ruleId, groupHash, thresholdPeriod);
await redisHelper.computeEventCountForPeriod(projectId, ruleId, groupHash, thresholdPeriod);
await redisHelper.computeEventCountForPeriod(projectId, ruleId, groupHash, thresholdPeriod);

/**
* Update current date for threshold period expiration
*/
jest.spyOn(global.Date, 'now').mockImplementation(() => currentDate + 2 * thresholdPeriod + 1);

const currentEventCount = await redisHelper.computeEventCountForPeriod(projectId, ruleId, groupHash, thresholdPeriod);
const currentlyStoredTimestamp = await redisClient.hGet(`${projectId}:${ruleId}:${groupHash}:${thresholdPeriod}:times`, 'timestamp');

expect(currentEventCount).toBe(1);
expect(currentlyStoredTimestamp).toBe(Date.now().toString());
});
});
});
8 changes: 4 additions & 4 deletions workers/notifier/tests/validator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import '../../../env-test';
describe('RuleValidator', () => {
const ruleMock = {
isEnabled: true,
whatToReceive: WhatToReceive.All,
whatToReceive: WhatToReceive.SeenMore,
including: [],
excluding: [],
};
Expand Down Expand Up @@ -40,18 +40,18 @@ describe('RuleValidator', () => {
});

describe('checkWhatToReceive', () => {
it('should pass if what to receive is \'all\'', () => {
it('should pass if what to receive is \'SEEN_MORE\'', () => {
const rule = { ...ruleMock } as any;

rule.whatToReceive = WhatToReceive.All;
rule.whatToReceive = WhatToReceive.SeenMore;

const validator = new RuleValidator(rule, eventMock);

expect(() => validator.checkWhatToReceive()).not.toThrowError();
expect(validator.checkWhatToReceive()).toBeInstanceOf(RuleValidator);
});

it('should pass if what to receive is \'new\' and event is new', () => {
it('should pass if what to receive is \'ONLY_NEW\' and event is new', () => {
const rule = { ...ruleMock } as any;
const event = { ...eventMock };

Expand Down
Loading

0 comments on commit 0ef30b2

Please sign in to comment.