Skip to content

Commit a60662d

Browse files
committed
feat: Add Docker Compose configuration and Redis integration tests
1 parent 7e8cb4b commit a60662d

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

.github/workflows/ci.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ jobs:
3131
node-version: [lts/*, latest]
3232
os: [ubuntu-latest, windows-latest, macos-latest]
3333
runs-on: ${{ matrix.os }}
34+
services:
35+
redis:
36+
image: redis:7
37+
ports:
38+
- 6379:6379
39+
redis-cluster:
40+
image: grokzen/redis-cluster:7.0.0
41+
env:
42+
IP: 0.0.0.0
43+
ports:
44+
- 7000-7005:7000-7005
3445
steps:
3546
- name: Checkout the repository
3647
uses: actions/checkout@v3
@@ -42,6 +53,9 @@ jobs:
4253
run: |
4354
npm ci
4455
npm run test:lib
56+
env:
57+
REDIS_PORT: 6379
58+
REDIS_CLUSTER_PORT: 7000
4559
publish:
4660
name: Publish
4761
needs: [lint, test-library]

docker-compose.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
version: '3'
2+
services:
3+
redis:
4+
image: redis:7
5+
ports:
6+
- '6385:6379'
7+
8+
redis-cluster:
9+
image: grokzen/redis-cluster:7.0.0
10+
environment:
11+
- IP=0.0.0.0
12+
ports:
13+
- '7010-7015:7000-7005'

test/redis-integration-test.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import process from 'node:process'
2+
import { describe, it, expect, beforeAll, afterAll, jest } from '@jest/globals'
3+
import { Redis, Cluster } from 'ioredis'
4+
import { type Options as RateLimitOptions } from 'express-rate-limit'
5+
import { RedisStore } from '../source/index.js'
6+
import type {
7+
SendCommandClusterDetails,
8+
RedisReply,
9+
Options as RedisOptions,
10+
} from '../source/types.js'
11+
12+
jest.setTimeout(30_000)
13+
14+
describe('Redis Integration Tests', () => {
15+
describe('Single Redis Instance', () => {
16+
let client: Redis
17+
let store: RedisStore
18+
19+
beforeAll(async () => {
20+
const host = process.env.REDIS_HOST ?? 'localhost'
21+
const port = Number(process.env.REDIS_PORT ?? 6385)
22+
23+
client = new Redis({
24+
host,
25+
port,
26+
lazyConnect: true,
27+
})
28+
await client.connect().catch(() => {
29+
console.warn('Skipping Redis tests - connection failed')
30+
})
31+
})
32+
33+
afterAll(async () => {
34+
await client?.quit()
35+
})
36+
37+
it('should work with sendCommand', async () => {
38+
if (client.status !== 'ready') {
39+
console.warn('Redis not ready, skipping test')
40+
return
41+
}
42+
43+
store = new RedisStore({
44+
async sendCommand(...args: string[]) {
45+
const result = await client.call(args[0], ...args.slice(1))
46+
return result as RedisReply
47+
},
48+
} as RedisOptions)
49+
store.init({ windowMs: 1000 } as RateLimitOptions)
50+
51+
const key = 'test-single'
52+
await store.resetKey(key)
53+
54+
const result1 = await store.increment(key)
55+
expect(result1.totalHits).toBe(1)
56+
57+
const result2 = await store.increment(key)
58+
expect(result2.totalHits).toBe(2)
59+
60+
await store.resetKey(key)
61+
const result3 = await store.increment(key)
62+
expect(result3.totalHits).toBe(1)
63+
})
64+
65+
it('should work with decrement', async () => {
66+
if (client.status !== 'ready') return
67+
68+
const key = 'test-single-decr'
69+
await store.resetKey(key)
70+
71+
await store.increment(key) // 1
72+
await store.increment(key) // 2
73+
await store.decrement(key) // 1
74+
75+
const result = await store.increment(key) // 2
76+
expect(result.totalHits).toBe(2)
77+
})
78+
79+
it('should work with get', async () => {
80+
if (client.status !== 'ready') return
81+
82+
const key = 'test-single-get'
83+
await store.resetKey(key)
84+
85+
await store.increment(key)
86+
const info = await store.get(key)
87+
expect(info).toBeDefined()
88+
expect(info?.totalHits).toBe(1)
89+
expect(info?.resetTime).toBeInstanceOf(Date)
90+
})
91+
92+
it('should handle TTL correctly', async () => {
93+
if (client.status !== 'ready') return
94+
95+
const key = 'test-single-ttl'
96+
// Initialize with short window
97+
const shortStore = new RedisStore({
98+
async sendCommand(...args: string[]) {
99+
const result = await client.call(args[0], ...args.slice(1))
100+
return result as RedisReply
101+
},
102+
} as RedisOptions)
103+
shortStore.init({ windowMs: 1000 } as RateLimitOptions)
104+
105+
await shortStore.resetKey(key)
106+
await shortStore.increment(key)
107+
108+
// Wait for expiration (1.1s)
109+
await new Promise<void>((resolve) => {
110+
setTimeout(resolve, 1100)
111+
})
112+
113+
const result = await shortStore.increment(key)
114+
expect(result.totalHits).toBe(1)
115+
})
116+
})
117+
118+
describe('Redis Cluster', () => {
119+
let client: Cluster
120+
let store: RedisStore
121+
122+
beforeAll(async () => {
123+
const host = process.env.REDIS_CLUSTER_HOST ?? 'localhost'
124+
const port = Number(process.env.REDIS_CLUSTER_PORT ?? 7010)
125+
126+
client = new Cluster([{ host, port }], {
127+
redisOptions: { connectTimeout: 2000 },
128+
clusterRetryStrategy: () => null,
129+
lazyConnect: true,
130+
})
131+
try {
132+
await client.connect()
133+
} catch {
134+
console.warn('Skipping Redis Cluster tests - connection failed')
135+
}
136+
})
137+
138+
afterAll(async () => {
139+
await client?.quit()
140+
})
141+
142+
it('should work with sendCommandCluster', async () => {
143+
if (client.status !== 'ready') {
144+
console.warn('Redis Cluster not ready, skipping test')
145+
return
146+
}
147+
148+
store = new RedisStore({
149+
async sendCommandCluster(details: SendCommandClusterDetails) {
150+
const { command } = details
151+
const result = await client.call(command[0], ...command.slice(1))
152+
return result as RedisReply
153+
},
154+
} as RedisOptions)
155+
store.init({ windowMs: 1000 } as RateLimitOptions)
156+
157+
const key = 'test-cluster'
158+
await store.resetKey(key)
159+
160+
const result1 = await store.increment(key)
161+
expect(result1.totalHits).toBe(1)
162+
163+
const result2 = await store.increment(key)
164+
expect(result2.totalHits).toBe(2)
165+
166+
await store.resetKey(key)
167+
const result3 = await store.increment(key)
168+
expect(result3.totalHits).toBe(1)
169+
})
170+
171+
it('should work with decrement', async () => {
172+
if (client.status !== 'ready') return
173+
174+
const key = 'test-cluster-decr'
175+
await store.resetKey(key)
176+
177+
await store.increment(key) // 1
178+
await store.increment(key) // 2
179+
await store.decrement(key) // 1
180+
181+
const result = await store.increment(key) // 2
182+
expect(result.totalHits).toBe(2)
183+
})
184+
185+
it('should work with get', async () => {
186+
if (client.status !== 'ready') return
187+
188+
const key = 'test-cluster-get'
189+
await store.resetKey(key)
190+
191+
await store.increment(key)
192+
const info = await store.get(key)
193+
expect(info).toBeDefined()
194+
expect(info?.totalHits).toBe(1)
195+
expect(info?.resetTime).toBeInstanceOf(Date)
196+
})
197+
198+
it('should handle TTL correctly', async () => {
199+
if (client.status !== 'ready') return
200+
201+
const key = 'test-cluster-ttl'
202+
// Initialize with short window
203+
const shortStore = new RedisStore({
204+
async sendCommandCluster(details: SendCommandClusterDetails) {
205+
const { command } = details
206+
const result = await client.call(command[0], ...command.slice(1))
207+
return result as RedisReply
208+
},
209+
} as RedisOptions)
210+
shortStore.init({ windowMs: 1000 } as RateLimitOptions)
211+
212+
await shortStore.resetKey(key)
213+
await shortStore.increment(key)
214+
215+
// Wait for expiration (1.1s)
216+
await new Promise<void>((resolve) => {
217+
setTimeout(resolve, 1100)
218+
})
219+
220+
const result = await shortStore.increment(key)
221+
expect(result.totalHits).toBe(1)
222+
})
223+
})
224+
})

0 commit comments

Comments
 (0)