diff --git a/README.md b/README.md index 8e1633d..3476862 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ $ yarn add @myunisoft/redis - Abstraction - [KVPeer](./docs/KVPeer.md) - [TimedKVPeer](./docs/TimedKVPeer.md) - - [RestrictedKV](./docs/RestrictedKV.md) - [StoreContext](./docs/StoreContext.md) - [PubSub](./docs/pubsub/Channel.md) - [Stream](./docs/stream/Stream.md) diff --git a/docs/RestrictedKV.md b/docs/RestrictedKV.md deleted file mode 100644 index b0359ad..0000000 --- a/docs/RestrictedKV.md +++ /dev/null @@ -1,107 +0,0 @@ -

- RestrictedKV -

- -

- This class is used to prevent from brut force attack. It allow to lock key usage on a number of failed attempt. -

- - -## Interface - -```ts -type RestrictedKVOptions = { - autoClearExpired?: number; - allowedAttempt?: number; - banTimeInSecond?: number; -} - -interface Attempt { - failure: number; - lastTry: number; - locked: boolean; -} - -type KeyType = string | Buffer; -``` - -## Constants - -- kDefaultAllowedAttempt = 6; -- kDefaultBanTime = 60 * 5; - -## 📚 Usage - -```ts -import { RestrictedKV, MemoryAdapter } from "@myunisoft/redis"; - -const allowedAttempt = 2; -const banTime = 60; - -const memoryAdapter = new MemoryAdapter(); - -const restrictedKV = new RestrictedKV({ - adapter: memoryAdapter, - allowedAttempt, - banTimeInSecond: banTime -}); -``` - -## 📜 API - -### getAttempt(key: KeyType): Promise< Attempt > - -Returns the number of attempts (failure, last tentative timestamp ...) for a given key. - -```ts -const key: string = "foo" - -const attempt = await restrictedKV.getAttempt(key); -const { failure, lastTry, locked } = attempt; - -strictEqual(failure, 0); -strictEqual(lastTry, Date.now()) -strictEqual(locked, false); -``` - -### fail(key: KeyType): Promise< Attempt > - -Increment an attempt failure for a given key. -When the number of failures exceeds the defined limitation, the key is locked. - -```ts -const key: string = "foo"; - -const attempt = await restrictedKV.fail(key); -const { failure, lastTry, locked } = attempt; - -strictEqual(failure, 1); -strictEqual(lastTry, Date.now()); -strictEqual(locked, false); -``` -### success( ) - -Notify a successful attempt for a given key. This will remove all traces of previous failed attempt. - -```ts -const key: string = "foo"; - -await restrictedKV.success(email); - -const attempt = await restrictedKV.getAttempt(key); -const { failure, lastTry, locked } = attempt; - -strictEqual(failure, 0); -strictEqual(lastTry, Date.now()); -strictEqual(locked, false); -``` - -### clearExpired() - -Clear all keys where the last attempt exceeds an allocated lifetime. - -```ts -await restrictedKV.clearExpired() -``` - -Cast the event `expiredKeys` olding the removed keys. diff --git a/docs/adapter/memory.adapter.md b/docs/adapter/memory.adapter.md index 9675055..677a843 100644 --- a/docs/adapter/memory.adapter.md +++ b/docs/adapter/memory.adapter.md @@ -56,16 +56,6 @@ const result = await memoryAdapter.deleteValue(key); console.log(result); // 0 for Failure, 1 for Success ``` -### clearExpired(options: { banTimeInSecond: number; }): (string | Buffer)[] - -this method is used to clear expired key-value pairs in memory - -```ts -const result = await memoryAdapter.clearExpired({ banTimeInSecond: 10 }); - -console.log(result); // [] -``` - ### getValue(key: string): null | unknown this method is used to get a value from memory diff --git a/docs/adapter/redis.adapter.md b/docs/adapter/redis.adapter.md index 5718493..5ef1d6e 100644 --- a/docs/adapter/redis.adapter.md +++ b/docs/adapter/redis.adapter.md @@ -93,28 +93,6 @@ const result = await redisAdapter.deleteValue(key); console.log(result); // 0 for Failure, 1 for Success ``` -### clearExpired(options: ClearExpiredOptions): (string | Buffer)[] - -this method is used to clear expired key-value pairs in redis - -```ts -const result = await redisAdapter.clearExpired({ banTimeInSecond: 10 }); - -console.log(result); // [] -``` - -### isKeyExpired(options: RedisIsKeyExpiredOptions): boolean - -this method is used to check if a key is expired in redis - -```ts -const key = "foo"; - -const result = await redisAdapter.isKeyExpired({ key, banTimeInSecond: 10 }); - -console.log(result); // false -``` - ### deepParseInput(input: Record | any[]): Generator | any[]> this method is used to deep parse input diff --git a/src/class/RestrictedKV.class.ts b/src/class/RestrictedKV.class.ts deleted file mode 100644 index f8fdeb1..0000000 --- a/src/class/RestrictedKV.class.ts +++ /dev/null @@ -1,155 +0,0 @@ -// Import Internal dependencies -import { KVPeer, type KVOptions } from "./KVPeer.class.js"; -import type { KeyType } from "../types/index.js"; -import { ClearExpiredOptions } from "./adapter/redis.adapter.js"; - -// CONSTANTS -const kDefaultAllowedAttempt = 6; -const kDefaultBanTime = 60 * 5; - -export type RestrictedKVOptions = Pick, "adapter"> & { - autoClearExpired?: number; - allowedAttempt?: number; - banTimeInSecond?: number; -}; - -// type Definition -export interface Attempt { - failure: number; - lastTry: number; - locked: boolean; -} - -export type RawAttempt = Record; - -/** -* @class RestrictedKV -* @classdesc Implementation to prevent brute force attacks. -*/ -export class RestrictedKV extends KVPeer> { - private autoClearInterval: NodeJS.Timeout | null; - - protected allowedAttempt: number; - protected banTimeInSecond: number; - - static getDefaultAttempt() { - return { failure: 0, lastTry: Date.now(), locked: false }; - } - - constructor(options: RestrictedKVOptions) { - const { autoClearExpired, allowedAttempt, banTimeInSecond, adapter } = options; - - super({ - adapter, - type: "object" - }); - - this.allowedAttempt = allowedAttempt ?? kDefaultAllowedAttempt; - this.banTimeInSecond = banTimeInSecond ?? kDefaultBanTime; - - if (autoClearExpired) { - this.autoClearInterval = setInterval(async() => { - try { - const connectionPerf = this.adapter.getPerformance ? (await this.adapter.getPerformance()) : { isAlive: true }; - - if (connectionPerf.isAlive) { - await this.adapter.clearExpired({ - banTimeInSecond: this.banTimeInSecond - }); - } - } - catch (error) { - console.error(error); - } - }, autoClearExpired).unref(); - } - } - - private parseRawAttempt(data: RawAttempt): Attempt { - return { - failure: Number(data.failure ?? 0), - lastTry: Number(data.lastTry ?? Date.now()), - locked: (data.locked ?? "false") === "true" - }; - } - - async clearExpired( - options: ClearExpiredOptions = { banTimeInSecond: this.banTimeInSecond } - ): Promise { - const expiredKeys = await this.adapter.clearExpired(options); - - if (expiredKeys.length > 0) { - this.emit("expiredKeys", expiredKeys); - } - } - - clearAutoClearInterval() { - if (this.autoClearInterval) { - clearInterval(this.autoClearInterval); - } - this.autoClearInterval = null; - } - - /** - * @description Returns the number of attempts (failure, last tentative timestamp ...) for a given key - * - * @param key - key - * - * @example - * ```ts - * handler.getAttempt("myKey") - * ``` - */ - async getAttempt(key: KeyType): Promise { - const data = await this.getValue(key) as RawAttempt | null; - - return Object.assign({}, RestrictedKV.getDefaultAttempt(), data === null ? {} : this.parseRawAttempt(data)); - } - - /** - * @description Increment an access failure for a given key. - * The method also allows to define whether a key is locked or not (when the number of failures exceeds the defined limitation). - * - * @param key - key - * - * @example - * ```ts - * handler.fail("myKey") - * ``` - */ - async fail(key: KeyType): Promise { - const stored = await this.getAttempt(key); - const attempt: Attempt = { failure: 1, lastTry: Date.now(), locked: false }; - - if (stored !== null) { - const diff = (Date.now() - stored.lastTry) / 1000; - if (diff < this.banTimeInSecond) { - attempt.failure = stored.failure + 1; - } - if (attempt.failure > this.allowedAttempt) { - attempt.locked = true; - } - } - - await this.adapter.setValue({ key, value: attempt }); - - return attempt; - } - - /** - * @description Notify a successful access for a given key. This will remove all traces of previous failed access. - * - * @param key - key - * - * @example - * ```ts - * handler.success("email@domain.com") - * ``` - */ - async success(key: KeyType) { - const rawStored = await this.getValue(key); - if (rawStored !== null) { - await this.adapter.deleteValue(key); - } - } -} diff --git a/src/class/adapter/memory.adapter.ts b/src/class/adapter/memory.adapter.ts index e8ef639..27b8388 100644 --- a/src/class/adapter/memory.adapter.ts +++ b/src/class/adapter/memory.adapter.ts @@ -47,24 +47,6 @@ export class MemoryAdapter implements DatabaseConnection { return isDelete ? 1 : 0; } - clearExpired(options: { banTimeInSecond: number; }): (string | Buffer)[] { - const { banTimeInSecond } = options; - - const expired: string[] = []; - - for (const [key, value] of this.#values) { - if (this.isKeyExpired({ - banTimeInSecond, - value: value as Record - })) { - expired.push(key); - this.#values.delete(key); - } - } - - return expired; - } - // Implement the no-argument version of getValue getValue(key: string): null | unknown { const valueExist = this.#values.has(key); @@ -75,12 +57,4 @@ export class MemoryAdapter implements DatabaseConnection { return this.#values.get(key); } - - private isKeyExpired(options: InMemIsKeyExpiredOptions) { - const { banTimeInSecond, value } = options; - - const lastTry = "lastTry" in value ? Number(value.lastTry) : null; - - return lastTry === null ? false : (Date.now() - lastTry) / 1000 >= banTimeInSecond; - } } diff --git a/src/class/adapter/redis.adapter.ts b/src/class/adapter/redis.adapter.ts index 918c1fc..c6abff0 100644 --- a/src/class/adapter/redis.adapter.ts +++ b/src/class/adapter/redis.adapter.ts @@ -8,7 +8,6 @@ import { Err, Ok, Result } from "@openally/result"; // Import Internal Dependencies import { AssertConnectionError, AssertDisconnectionError } from "../error/connection.error.js"; import type { DatabaseConnection, KeyType, Value } from "../../types/index.js"; -import { Attempt } from "../RestrictedKV.class.js"; // CONSTANTS const kDefaultAttempt = 4; @@ -151,50 +150,6 @@ export class RedisAdapter extends Redis implements DatabaseConnection { return this.del(finalKey); } - async clearExpired(options: ClearExpiredOptions): Promise<(string | Buffer)[]> { - const promises = [this.keysBuffer("*"), this.keys("*")]; - - const data = [...await Promise.all(promises)].flat(); - if (data.length === 0) { - return []; - } - - const results = await Promise.all(data.map(async(key) => { - const expired = await this.isKeyExpired({ - ...options, - key - }); - - return { key, expired }; - })); - - const expiredKeys = results - .filter((row) => row.expired) - .map((row) => row.key); - - if (expiredKeys.length > 0) { - const pipeline = this.pipeline(); - - expiredKeys.forEach((key) => pipeline.del(key)); - - await pipeline.exec(); - } - - return expiredKeys; - } - - private async isKeyExpired(options: RedisIsKeyExpiredOptions): Promise { - const { banTimeInSecond, key } = options; - - const attempt = await this.getValue( - key, - "object" - ) as Attempt; - const lastTry = "lastTry" in attempt ? Number(attempt.lastTry) : null; - - return lastTry === null ? false : (Date.now() - lastTry) / 1000 >= banTimeInSecond; - } - private* deepParseInput(input: Record | any[]) { if (Array.isArray(input)) { for (const value of input) { diff --git a/src/index.ts b/src/index.ts index 0ef554e..f1ee7e2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,6 @@ export * from "./class/stream/index.js"; export * from "./class/pubSub/Channel.class.js"; export * from "./class/KVPeer.class.js"; export * from "./class/TimedKVPeer.class.js"; -export * from "./class/RestrictedKV.class.js"; export * from "./class/StoreContext.class.js"; // Export Types diff --git a/src/types/index.ts b/src/types/index.ts index 7c22779..5c36d6b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -47,6 +47,5 @@ export interface DatabaseConnection { setValue(...unknown): Promise> | Result; deleteValue(...unknown): Promise | number; - clearExpired(...unknown): Promise<(string | Buffer)[]> | (string | Buffer)[]; getValue(...unknown): Promise | unknown; } diff --git a/test/class/RestrictedKV.spec.ts b/test/class/RestrictedKV.spec.ts deleted file mode 100644 index c6b889f..0000000 --- a/test/class/RestrictedKV.spec.ts +++ /dev/null @@ -1,612 +0,0 @@ -/* eslint-disable max-nested-callbacks */ -// Import Node.js Dependencies -import assert from "node:assert"; -import { describe, before, after, test, it, beforeEach, mock, Mock } from "node:test"; -import timers from "node:timers/promises"; -import { EventEmitter } from "node:events"; - -// Import Third-party Dependencies -import MockDate from "mockdate"; - -// Import Internal Dependencies -import { MemoryAdapter, RedisAdapter, RestrictedKV } from "../../src"; -import { randomValue } from "../fixtures/utils/randomValue"; - -// Internal Dependencies Mock -mock.method(RestrictedKV.prototype, "deleteValue", async() => "deleteValue"); - -MockDate.set(Date.now()); - -describe("RestrictedKV", () => { - describe("RedisAdapter", () => { - let redisAdapter: RedisAdapter; - - before(async() => { - redisAdapter = new RedisAdapter({ - port: Number(process.env.REDIS_PORT), - host: process.env.REDIS_HOST - }); - - await redisAdapter.initialize(); - await redisAdapter.flushdb(); - }); - - after(async() => { - await redisAdapter.close(true); - }); - - describe("Instantiated with default options", () => { - let restrictedKV: RestrictedKV; - - before(() => { - restrictedKV = new RestrictedKV({ - adapter: redisAdapter - }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test(`WHEN calling getDefaultAttempt - THEN it should return a default default attempt object`, () => { - const defaultAttempt = RestrictedKV.getDefaultAttempt(); - assert.deepStrictEqual(defaultAttempt, { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - - describe("getAttempt", () => { - const lastTry = Date.now(); - - test(`Given an Attempt object with all keys - WHEN calling getAttempt - THEN it should return the initial object`, - async() => { - const key = randomValue(); - const payload = { failure: 0, lastTry, locked: false }; - - await restrictedKV.setValue({ key, value: payload }); - assert.deepEqual(await restrictedKV.getAttempt(key), payload); - }); - - test(`Given a partial of Attempt object - WHEN calling getAttempt - THEN it should return a completed Attempt object`, - async() => { - const optionsWithLocked = { - key: randomValue(), - value: { locked: false } - }; - - await restrictedKV.setValue(optionsWithLocked); - const attemptWithLocked = await restrictedKV.getAttempt(optionsWithLocked.key); - assert.deepEqual(attemptWithLocked, Object.assign({}, - { lastTry: Date.now(), failure: 0 }, - optionsWithLocked.value - )); - - const optionsWithFailure = { - key: randomValue(), - value: { failure: 0 } - }; - - await restrictedKV.setValue(optionsWithFailure); - const attemptWithFailure = await restrictedKV.getAttempt(optionsWithFailure.key); - assert.deepEqual(attemptWithFailure, Object.assign({}, - { lastTry: Date.now(), locked: false }, - optionsWithFailure.value - )); - }); - }); - - describe("fail", () => { - test(`Given a fake key - WHEN calling fail - THEN it should return a new Attempt object with failure property init at 1`, - async() => { - const attempt = await restrictedKV.fail("my-fake-key"); - assert.equal(attempt.failure, 1); - }); - - test(`Given a valid key - WHEN calling fail - THEN it should return the associated Attempt object with failure property incremented`, - async() => { - const lastTry = Date.now(); - const payload = { failure: 1, lastTry, locked: false }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - const attempt = await restrictedKV.fail(key); - assert.equal(attempt.failure, payload.failure + 1); - }); - }); - - describe("success", () => { - let deleteValueMock: Mock; - - before(() => { - deleteValueMock = mock.method(redisAdapter, "deleteValue", async() => 1); - }); - - after(() => { - deleteValueMock.mock.restore(); - }); - - test(`GIVEN an unknown key - WHEN calling success - THEN it should return a clean Attempt object`, - async() => { - const key = randomValue(); - - await restrictedKV.success(key); - - const attempt = await restrictedKV.getAttempt(key); - assert.deepEqual(attempt, { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - - test(`GIVEN a known key - WHEN calling success - THEN it should deleted the KVPeer`, - async() => { - const lastTry = Date.now(); - const payload = { failure: 0, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - await restrictedKV.success(key); - - assert.equal(deleteValueMock.mock.calls.length, 1); - assert.deepEqual(deleteValueMock.mock.calls[0].arguments, [key]); - }); - }); - - describe("clearExpired", () => { - test("should not clean not expired key from database", async() => { - const payload = { failure: 0, lastTry: Date.now(), locked: false }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - await restrictedKV.clearExpired(); - - assert.deepEqual(await restrictedKV.getAttempt(key), payload); - }); - - test("should clear all keys from the database when invoked", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - await restrictedKV.clearExpired(); - - const attempt = await restrictedKV.getAttempt(key); - assert.equal(attempt.failure, 0); - assert.equal(attempt.locked, false); - }); - - describe("expiredKeys event", () => { - let emitMock: Mock; - before(() => { - emitMock = mock.method(RestrictedKV.prototype, "emit", () => void 0); - }); - - test("should not send an event when no cleared key", async() => { - await restrictedKV.clearExpired(); - - assert.equal(emitMock.mock.calls.length, 0); - }); - - test("should send an event with expiredKeys", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - await restrictedKV.clearExpired(); - assert.equal(emitMock.mock.calls.length, 1); - }); - }); - }); - }); - - describe("allowedAttempt", () => { - const allowedAttempt = 2; - - let restrictedKV: RestrictedKV; - - before(() => { - restrictedKV = new RestrictedKV({ - allowedAttempt, - adapter: redisAdapter - }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test("should lock the key after allowedAttempt fail instead of 3", async() => { - const lastTry = Date.now(); - const payload = { failure: 2, lastTry, locked: false }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - const attempt = await restrictedKV.fail(key); - assert.equal(attempt.failure, payload.failure + 1); - assert.equal(attempt.locked, true); - }); - }); - - describe("banTime", () => { - const allowedAttempt = 2; - const banTime = 60; - - const key = randomValue(); - const lastTry = Date.now(); - const payload = { failure: 1, lastTry, locked: false }; - - let restrictedKV: RestrictedKV; - - before(async() => { - restrictedKV = new RestrictedKV({ - allowedAttempt, - banTimeInSecond: banTime, - adapter: redisAdapter - }); - - await restrictedKV.setValue({ key, value: payload }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test("should unlock the key after the given banTime", async() => { - await timers.setTimeout(140); - - const attempt = await restrictedKV.getValue(key); - assert.equal(attempt?.failure, payload.failure); - }); - }); - - describe("autoClearInterval database suite", () => { - let restrictedKV: RestrictedKV; - - before(async() => { - restrictedKV = new RestrictedKV({ - autoClearExpired: 20, - adapter: redisAdapter - }); - }); - - beforeEach(async() => { - await redisAdapter.flushdb(); - }); - - after(async() => { - restrictedKV.clearAutoClearInterval(); - }); - - it("should clear all keys from the database when invoked", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - await timers.setTimeout(1000); - - assert.deepEqual(await restrictedKV.getAttempt(key), { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - }); - }); - - describe("MemoryAdapter", () => { - let memoryAdapter: MemoryAdapter; - - before(async() => { - memoryAdapter = new MemoryAdapter(); - }); - - describe("Instantiated with default options", () => { - let restrictedKV: RestrictedKV; - - before(() => { - restrictedKV = new RestrictedKV({ - adapter: memoryAdapter - }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test(`WHEN calling getDefaultAttempt - THEN it should return a default default attempt object`, () => { - const defaultAttempt = RestrictedKV.getDefaultAttempt(); - assert.deepStrictEqual(defaultAttempt, { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - - describe("getAttempt", () => { - const lastTry = Date.now(); - - test(`Given an Attempt object with all keys - WHEN calling getAttempt - THEN it should return the initial object`, - async() => { - const key = randomValue(); - const payload = { failure: 0, lastTry, locked: false }; - - await restrictedKV.setValue({ key, value: payload }); - assert.deepEqual(await restrictedKV.getAttempt(key), payload); - }); - - test(`Given a partial of Attempt object - WHEN calling getAttempt - THEN it should return a completed Attempt object`, - async() => { - const optionsWithLocked = { - key: randomValue(), - value: { locked: false } - }; - - await restrictedKV.setValue(optionsWithLocked); - const attemptWithLocked = await restrictedKV.getAttempt(optionsWithLocked.key); - assert.deepEqual(attemptWithLocked, Object.assign({}, - { lastTry: Date.now(), failure: 0 }, - optionsWithLocked.value - )); - - const optionsWithFailure = { - key: randomValue(), - value: { failure: 0 } - }; - - await restrictedKV.setValue(optionsWithFailure); - const attemptWithFailure = await restrictedKV.getAttempt(optionsWithFailure.key); - assert.deepEqual(attemptWithFailure, Object.assign({}, - { lastTry: Date.now(), locked: false }, - optionsWithFailure.value - )); - }); - }); - - describe("fail", () => { - test(`Given a fake key - WHEN calling fail - THEN it should return a new Attempt object with failure property init at 1`, - async() => { - const attempt = await restrictedKV.fail("my-fake-key"); - assert.equal(attempt.failure, 1); - }); - - test(`Given a valid key - WHEN calling fail - THEN it should return the associated Attempt object with failure property incremented`, - async() => { - const lastTry = Date.now(); - const payload = { failure: 1, lastTry, locked: false }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - const attempt = await restrictedKV.fail(key); - assert.equal(attempt.failure, payload.failure + 1); - }); - }); - - describe("success", () => { - let deleteValueMock: Mock; - - before(() => { - deleteValueMock = mock.method(memoryAdapter, "deleteValue", async() => 1); - }); - - after(() => { - deleteValueMock.mock.restore(); - }); - - test(`GIVEN an unknown key - WHEN calling success - THEN it should return a clean Attempt object`, - async() => { - const key = randomValue(); - - await restrictedKV.success(key); - - const attempt = await restrictedKV.getAttempt(key); - assert.deepEqual(attempt, { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - - test(`GIVEN a known key - WHEN calling success - THEN it should deleted the KVPeer`, - async() => { - const lastTry = Date.now(); - const payload = { failure: 0, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - await restrictedKV.success(key); - - assert.equal(deleteValueMock.mock.calls.length, 1); - assert.deepEqual(deleteValueMock.mock.calls[0].arguments, [key]); - }); - }); - - describe("clearExpired", () => { - // test("should not clean not expired key from database", async() => { - // const payload = { failure: 0, lastTry: Date.now(), locked: false }; - // const key = randomValue(); - - // await restrictedKV.setValue({ key, value: payload }); - // await restrictedKV.clearExpired(); - - // assert.deepEqual(await restrictedKV.getAttempt(key), payload); - // }); - - test("should clear all keys from the database when invoked", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - await restrictedKV.clearExpired(); - - const attempt = await restrictedKV.getAttempt(key); - assert.equal(attempt.failure, 0); - assert.equal(attempt.locked, false); - }); - - describe("expiredKeys event", () => { - let emitMock: Mock; - before(() => { - emitMock = mock.method(RestrictedKV.prototype, "emit", () => void 0); - }); - - test("should not send an event when no cleared key", async() => { - await restrictedKV.clearExpired(); - - assert.equal(emitMock.mock.calls.length, 0); - }); - - test("should send an event with expiredKeys", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - await restrictedKV.clearExpired(); - assert.equal(emitMock.mock.calls.length, 1); - }); - }); - }); - }); - - describe("allowedAttempt", () => { - const allowedAttempt = 2; - - let restrictedKV: RestrictedKV; - - before(() => { - restrictedKV = new RestrictedKV({ - allowedAttempt, - adapter: memoryAdapter - }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test("should lock the key after allowedAttempt fail instead of 3", async() => { - const lastTry = Date.now(); - const payload = { failure: 2, lastTry, locked: false }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - const attempt = await restrictedKV.fail(key); - assert.equal(attempt.failure, payload.failure + 1); - assert.equal(attempt.locked, true); - }); - }); - - describe("banTime", () => { - const allowedAttempt = 2; - const banTime = 60; - - const key = randomValue(); - const lastTry = Date.now(); - const payload = { failure: 1, lastTry, locked: false }; - - let restrictedKV: RestrictedKV; - - before(async() => { - restrictedKV = new RestrictedKV({ - allowedAttempt, - banTimeInSecond: banTime, - adapter: memoryAdapter - }); - - await restrictedKV.setValue({ key, value: payload }); - }); - - it("should be instantiated", () => { - assert.ok(restrictedKV instanceof RestrictedKV); - assert.ok(restrictedKV instanceof EventEmitter); - }); - - test("should unlock the key after the given banTime", async() => { - await timers.setTimeout(140); - - const attempt = await restrictedKV.getValue(key); - assert.equal(attempt?.failure, payload.failure); - }); - }); - - describe("autoClearInterval database suite", () => { - let restrictedKV: RestrictedKV; - - before(async() => { - restrictedKV = new RestrictedKV({ - autoClearExpired: 20, - adapter: memoryAdapter - }); - }); - - beforeEach(async() => { - memoryAdapter.flushall(); - }); - - after(async() => { - restrictedKV.clearAutoClearInterval(); - }); - - it("should clear all keys from the database when invoked", async() => { - const lastTry = Date.now() - (90 * 1_000 * 60); - const payload = { failure: 3, lastTry, locked: true }; - const key = randomValue(); - - await restrictedKV.setValue({ key, value: payload }); - - await timers.setTimeout(1000); - - assert.deepEqual(await restrictedKV.getAttempt(key), { - failure: 0, - locked: false, - lastTry: Date.now() - }); - }); - }); - }); -}); diff --git a/test/class/adapter/memory.adapter.spec.ts b/test/class/adapter/memory.adapter.spec.ts index 71285da..a7d2adb 100644 --- a/test/class/adapter/memory.adapter.spec.ts +++ b/test/class/adapter/memory.adapter.spec.ts @@ -59,23 +59,6 @@ describe("MemoryAdapter", () => { }); }); - describe("clearExpired", () => { - const key = "foo"; - const value = { value: "bar", lastTry: Date.now() }; - - const memoryAdapter = new MemoryAdapter(); - - it("Should clear expired key for the given banTimeInSecond", async() => { - memoryAdapter.setValue({ key, value }); - - await timers.setTimeout(100); - - const result = memoryAdapter.clearExpired({ banTimeInSecond: 0 }); - - assert.equal(result[0], key); - }); - }); - describe("getValue", () => { const key = "foo"; const fakeKey = "fake";