Skip to content
This repository was archived by the owner on Apr 15, 2025. It is now read-only.

Commit 67a66cb

Browse files
author
williamd5
authored
Merge pull request #1 from cloudnode-pro/feature/tests
Implement automated unit tests
2 parents b879926 + 45dd34f commit 67a66cb

File tree

5 files changed

+187
-2
lines changed

5 files changed

+187
-2
lines changed

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Unit tests
2+
'on':
3+
pull_request:
4+
types:
5+
- opened
6+
- synchronize
7+
- reopened
8+
schedule:
9+
- cron: '0 6 * * 0'
10+
11+
jobs:
12+
test:
13+
name: 'Node.js v${{ matrix.node }}'
14+
runs-on: ubuntu-latest
15+
strategy:
16+
matrix:
17+
node:
18+
- 18
19+
- 16
20+
steps:
21+
- uses: actions/setup-node@v1
22+
with:
23+
node-version: '${{ matrix.node }}'
24+
- uses: actions/checkout@v2
25+
- name: 'Cache node_modules'
26+
uses: actions/cache@v2
27+
with:
28+
path: ~/.npm
29+
key: ${{ runner.os }}-node-v${{ matrix.node }}-${{ hashFiles('**/package.json') }}
30+
restore-keys: |
31+
${{ runner.os }}-node-v${{ matrix.node }}-
32+
- name: Install Dependencies
33+
run: npm install
34+
- name: Run All Node.js Tests
35+
run: npm run test

.idea/jsLibraryMappings.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
"name": "cldn-ratelimit",
33
"version": "1.1.0",
44
"description": "Simple ratelimiter for Node.js",
5-
"main": "index.js",
5+
"main": "lib/RateLimit.js",
6+
"type": "module",
67
"scripts": {
7-
"test": "c8 mocha"
8+
"_test": "mocha",
9+
"test": "c8 npm run _test",
10+
"coverage": "c8 --reporter=html npm run _test",
11+
"build": "tsc"
812
},
913
"repository": {
1014
"type": "git",

src/RateLimit.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ export class RateLimit {
5353
RateLimit.#instances.set(name, this);
5454
}
5555

56+
/**
57+
* Get rate limit name
58+
* @readonly
59+
*/
60+
get name(): string {
61+
return this.#name;
62+
}
63+
5664
/**
5765
* Check the attempt state for a source ID without decrementing the remaining attempts
5866
* @param {string} source - Unique source identifier (e.g. username, IP, etc.)

test/test.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import assert from 'assert';
2+
import {RateLimit} from "../lib/RateLimit.js";
3+
4+
describe("RateLimit", () => {
5+
describe("instance methods", () => {
6+
it("should create a new RateLimit instance", () => {
7+
assert(new RateLimit("test", 5, 1) instanceof RateLimit);
8+
});
9+
it("should not allow creating a new instance with the same name", () => {
10+
assert.throws(() => new RateLimit("test", 5, 1));
11+
});
12+
it("should fetch the existing RateLimit instance", () => {
13+
assert(RateLimit.get("test") instanceof RateLimit);
14+
});
15+
it("should decrement the remaining attempts", () => {
16+
const rateLimit = RateLimit.get("test");
17+
const result = rateLimit.attempt("source1");
18+
assert(result.remaining === 4);
19+
});
20+
it("should check the remaining attempts", () => {
21+
const rateLimit = RateLimit.get("test");
22+
const result = rateLimit.check("source1");
23+
assert(result.remaining === 4);
24+
});
25+
it("should prevent attempts when the limit is reached", () => {
26+
const rateLimit = RateLimit.get("test");
27+
rateLimit.attempt("source1"); // 3 remaining
28+
rateLimit.attempt("source1"); // 2 remaining
29+
rateLimit.attempt("source1"); // 1 remaining
30+
rateLimit.attempt("source1"); // 0 remaining
31+
const result = rateLimit.attempt("source1"); // -1 remaining
32+
assert.strictEqual(result.allow, false);
33+
});
34+
it("should reset the remaining attempts after the timeout", async () => {
35+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
36+
const rateLimit = RateLimit.get("test");
37+
await sleep(1500);
38+
assert.strictEqual(rateLimit.attempt("source1").allow, true);
39+
});
40+
it("should reset the remaining attempts", () => {
41+
const rateLimit = RateLimit.get("test");
42+
rateLimit.reset("source1");
43+
assert.strictEqual(rateLimit.check("source1").remaining, 5);
44+
});
45+
it("should set the remaining attempts", () => {
46+
const rateLimit = RateLimit.get("test");
47+
rateLimit.setRemaining("source1", 3);
48+
assert.strictEqual(rateLimit.check("source1").remaining, 3);
49+
});
50+
it("should reset all attempts", () => {
51+
const rateLimit = RateLimit.get("test");
52+
rateLimit.attempt("source2");
53+
rateLimit.attempt("source3");
54+
rateLimit.clear();
55+
assert.strictEqual(rateLimit.check("source1").remaining, 5);
56+
assert.strictEqual(rateLimit.check("source2").remaining, 5);
57+
assert.strictEqual(rateLimit.check("source3").remaining, 5);
58+
});
59+
it("should delete the RateLimit instance", () => {
60+
const rateLimit = RateLimit.get("test");
61+
rateLimit.attempt("source1");
62+
rateLimit.delete();
63+
assert.strictEqual(RateLimit.get("test"), null);
64+
assert.throws(() => rateLimit.check("source1"), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
65+
assert.throws(() => rateLimit.attempt("source1"), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
66+
assert.throws(() => rateLimit.reset("source1"), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
67+
assert.throws(() => rateLimit.setRemaining("source1", 3), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
68+
assert.throws(() => rateLimit.clear(), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
69+
assert.throws(() => rateLimit.delete(), {message: "Rate limit \"test\" has been deleted. Construct a new instance"});
70+
});
71+
});
72+
describe("static methods", () => {
73+
it("should create a new RateLimit instance", () => {
74+
const rateLimit = RateLimit.create("test", 5, 1);
75+
assert(rateLimit instanceof RateLimit);
76+
});
77+
it("should return the same instance if created with the same name", () => {
78+
const rateLimit1 = RateLimit.create("test", 10, 1);
79+
assert(rateLimit1.limit === 5);
80+
});
81+
it("should make a rate limit attempt", () => {
82+
const result = RateLimit.attempt("test", "source1");
83+
assert(result.remaining === 4);
84+
});
85+
it("should make an attempt with custom weight", () => {
86+
const result = RateLimit.attempt("test", "source1", 2);
87+
assert(result.remaining === 2);
88+
});
89+
it("should check the remaining attempts", () => {
90+
const result = RateLimit.check("test", "source1");
91+
assert(result.remaining === 2);
92+
});
93+
it("should prevent attempts when the limit is reached", () => {
94+
RateLimit.attempt("test", "source1"); // 1 remaining
95+
RateLimit.attempt("test", "source1"); // 0 remaining
96+
const result = RateLimit.attempt("test", "source1"); // -1 remaining
97+
assert.strictEqual(result.allow, false);
98+
});
99+
it("should reset the remaining attempts after the timeout", async () => {
100+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
101+
await sleep(1500);
102+
assert.strictEqual(RateLimit.attempt("test", "source1").allow, true);
103+
});
104+
it("should reset the remaining attempts", () => {
105+
RateLimit.reset("test", "source1");
106+
assert.strictEqual(RateLimit.check("test", "source1").remaining, 5);
107+
});
108+
it("should set the remaining attempts", () => {
109+
RateLimit.setRemaining("test", "source1", 3);
110+
assert.strictEqual(RateLimit.check("test", "source1").remaining, 3);
111+
});
112+
it("should reset all attempts", () => {
113+
RateLimit.attempt("test", "source2");
114+
RateLimit.attempt("test", "source3");
115+
RateLimit.clear("test");
116+
assert.strictEqual(RateLimit.check("test", "source1").remaining, 5);
117+
assert.strictEqual(RateLimit.check("test", "source2").remaining, 5);
118+
assert.strictEqual(RateLimit.check("test", "source3").remaining, 5);
119+
});
120+
it("should delete the RateLimit instance", () => {
121+
RateLimit.attempt("test", "source1");
122+
RateLimit.delete("test");
123+
assert.strictEqual(RateLimit.get("test"), null);
124+
assert.throws(() => RateLimit.check("test", "source1"), {message: "Rate limit with name \"test\" does not exist"});
125+
assert.throws(() => RateLimit.attempt("test", "source1"), {message: "Rate limit with name \"test\" does not exist"});
126+
assert.throws(() => RateLimit.reset("test", "source1"), {message: "Rate limit with name \"test\" does not exist"});
127+
assert.throws(() => RateLimit.setRemaining("test", "source1", 3), {message: "Rate limit with name \"test\" does not exist"});
128+
assert.throws(() => RateLimit.clear("test"), {message: "Rate limit with name \"test\" does not exist"});
129+
assert.throws(() => RateLimit.delete("test"), {message: "Rate limit with name \"test\" does not exist"});
130+
});
131+
});
132+
});

0 commit comments

Comments
 (0)