Skip to content

Commit 22ffbc3

Browse files
committed
test: add unit tests for WebSocket and Redis adapters
1 parent f53a471 commit 22ffbc3

4 files changed

Lines changed: 1281 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import chai from 'chai'
2+
import chaiAsPromised from 'chai-as-promised'
3+
import Sinon from 'sinon'
4+
import sinonChai from 'sinon-chai'
5+
6+
chai.use(sinonChai)
7+
chai.use(chaiAsPromised)
8+
9+
const { expect } = chai
10+
11+
import { RedisAdapter } from '../../../src/adapters/redis-adapter'
12+
13+
describe('RedisAdapter', () => {
14+
let sandbox: Sinon.SinonSandbox
15+
let client: any
16+
let adapter: RedisAdapter
17+
18+
let originalConsoleError: typeof console.error
19+
20+
beforeEach(() => {
21+
sandbox = Sinon.createSandbox()
22+
originalConsoleError = console.error
23+
console.error = () => undefined
24+
25+
client = {
26+
connect: sandbox.stub().resolves(),
27+
on: sandbox.stub().returnsThis(),
28+
exists: sandbox.stub(),
29+
get: sandbox.stub(),
30+
set: sandbox.stub(),
31+
zRemRangeByScore: sandbox.stub(),
32+
zRange: sandbox.stub(),
33+
expire: sandbox.stub(),
34+
zAdd: sandbox.stub(),
35+
removeListener: sandbox.stub(),
36+
once: sandbox.stub(),
37+
}
38+
39+
adapter = new RedisAdapter(client)
40+
})
41+
42+
afterEach(() => {
43+
console.error = originalConsoleError
44+
sandbox.restore()
45+
})
46+
47+
describe('constructor', () => {
48+
it('calls client.connect()', () => {
49+
expect(client.connect).to.have.been.calledOnce
50+
})
51+
52+
it('registers event listeners for connect, ready, error, and reconnecting', () => {
53+
expect(client.on).to.have.been.calledWith('connect')
54+
expect(client.on).to.have.been.calledWith('ready')
55+
expect(client.on).to.have.been.calledWith('error')
56+
expect(client.on).to.have.been.calledWith('reconnecting')
57+
})
58+
})
59+
60+
describe('constructor error handling', () => {
61+
it('handles connection rejection without throwing', () => {
62+
const failingClient = {
63+
connect: sandbox.stub().rejects(new Error('connection refused')),
64+
on: sandbox.stub().returnsThis(),
65+
}
66+
67+
expect(() => new RedisAdapter(failingClient as any)).not.to.throw()
68+
})
69+
})
70+
71+
describe('hasKey', () => {
72+
it('awaits connection and calls client.exists with the key', async () => {
73+
client.exists.returns(1)
74+
75+
const result = await adapter.hasKey('test-key')
76+
77+
expect(client.exists).to.have.been.calledOnceWithExactly('test-key')
78+
expect(result).to.be.true
79+
})
80+
81+
it('returns false when key does not exist', async () => {
82+
client.exists.returns(0)
83+
84+
const result = await adapter.hasKey('missing-key')
85+
86+
expect(result).to.be.false
87+
})
88+
})
89+
90+
describe('getKey', () => {
91+
it('awaits connection and calls client.get with the key', async () => {
92+
client.get.resolves('test-value')
93+
94+
const result = await adapter.getKey('test-key')
95+
96+
expect(client.get).to.have.been.calledOnceWithExactly('test-key')
97+
expect(result).to.equal('test-value')
98+
})
99+
100+
it('returns null when key does not exist', async () => {
101+
client.get.resolves(null)
102+
103+
const result = await adapter.getKey('missing-key')
104+
105+
expect(result).to.be.null
106+
})
107+
})
108+
109+
describe('setKey', () => {
110+
it('returns true when client.set returns OK', async () => {
111+
client.set.resolves('OK')
112+
113+
const result = await adapter.setKey('key', 'value')
114+
115+
expect(client.set).to.have.been.calledOnceWithExactly('key', 'value')
116+
expect(result).to.be.true
117+
})
118+
119+
it('returns false when client.set does not return OK', async () => {
120+
client.set.resolves(null)
121+
122+
const result = await adapter.setKey('key', 'value')
123+
124+
expect(result).to.be.false
125+
})
126+
})
127+
128+
describe('removeRangeByScoreFromSortedSet', () => {
129+
it('calls client.zRemRangeByScore with correct arguments', async () => {
130+
client.zRemRangeByScore.resolves(3)
131+
132+
const result = await adapter.removeRangeByScoreFromSortedSet('sorted-key', 10, 20)
133+
134+
expect(client.zRemRangeByScore).to.have.been.calledOnceWithExactly('sorted-key', 10, 20)
135+
expect(result).to.equal(3)
136+
})
137+
})
138+
139+
describe('getRangeFromSortedSet', () => {
140+
it('calls client.zRange with correct arguments', async () => {
141+
client.zRange.resolves(['a', 'b', 'c'])
142+
143+
const result = await adapter.getRangeFromSortedSet('sorted-key', 0, 10)
144+
145+
expect(client.zRange).to.have.been.calledOnceWithExactly('sorted-key', 0, 10)
146+
expect(result).to.deep.equal(['a', 'b', 'c'])
147+
})
148+
149+
it('returns empty array when set is empty', async () => {
150+
client.zRange.resolves([])
151+
152+
const result = await adapter.getRangeFromSortedSet('empty-key', 0, 10)
153+
154+
expect(result).to.deep.equal([])
155+
})
156+
})
157+
158+
describe('setKeyExpiry', () => {
159+
it('calls client.expire with correct arguments', async () => {
160+
client.expire.resolves(true)
161+
162+
await adapter.setKeyExpiry('key', 3600)
163+
164+
expect(client.expire).to.have.been.calledOnceWithExactly('key', 3600)
165+
})
166+
})
167+
168+
describe('addToSortedSet', () => {
169+
it('transforms record entries to score/value members and calls client.zAdd', async () => {
170+
client.zAdd.resolves(2)
171+
172+
const set = { 'member1': '100', 'member2': '200' }
173+
const result = await adapter.addToSortedSet('sorted-key', set)
174+
175+
expect(client.zAdd).to.have.been.calledOnce
176+
const callArgs = client.zAdd.firstCall.args
177+
expect(callArgs[0]).to.equal('sorted-key')
178+
expect(callArgs[1]).to.deep.include.members([
179+
{ score: 100, value: 'member1' },
180+
{ score: 200, value: 'member2' },
181+
])
182+
expect(result).to.equal(2)
183+
})
184+
185+
it('handles a single entry', async () => {
186+
client.zAdd.resolves(1)
187+
188+
const set = { 'only-member': '50' }
189+
const result = await adapter.addToSortedSet('sorted-key', set)
190+
191+
const callArgs = client.zAdd.firstCall.args
192+
expect(callArgs[1]).to.deep.equal([{ score: 50, value: 'only-member' }])
193+
expect(result).to.equal(1)
194+
})
195+
})
196+
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import chai from 'chai'
2+
import Sinon from 'sinon'
3+
import sinonChai from 'sinon-chai'
4+
5+
chai.use(sinonChai)
6+
7+
const { expect } = chai
8+
9+
import { WebServerAdapter } from '../../../src/adapters/web-server-adapter'
10+
11+
describe('WebServerAdapter', () => {
12+
let sandbox: Sinon.SinonSandbox
13+
let webServer: any
14+
let adapter: WebServerAdapter
15+
16+
let originalConsoleError: typeof console.error
17+
18+
beforeEach(() => {
19+
sandbox = Sinon.createSandbox()
20+
originalConsoleError = console.error
21+
console.error = () => undefined
22+
23+
webServer = {
24+
on: sandbox.stub().returnsThis(),
25+
once: sandbox.stub().returnsThis(),
26+
listen: sandbox.stub(),
27+
close: sandbox.stub(),
28+
removeAllListeners: sandbox.stub(),
29+
}
30+
31+
adapter = new WebServerAdapter(webServer)
32+
})
33+
34+
afterEach(() => {
35+
console.error = originalConsoleError
36+
sandbox.restore()
37+
adapter.removeAllListeners()
38+
})
39+
40+
describe('constructor', () => {
41+
it('registers error event listener on webServer', () => {
42+
expect(webServer.on).to.have.been.calledWith('error')
43+
})
44+
45+
it('registers clientError event listener on webServer', () => {
46+
expect(webServer.on).to.have.been.calledWith('clientError')
47+
})
48+
49+
it('registers close event listener on webServer', () => {
50+
expect(webServer.once).to.have.been.calledWith('close')
51+
})
52+
53+
it('registers listening event listener on webServer', () => {
54+
expect(webServer.once).to.have.been.calledWith('listening')
55+
})
56+
})
57+
58+
describe('listen', () => {
59+
it('calls webServer.listen with the given port', () => {
60+
adapter.listen(8080)
61+
62+
expect(webServer.listen).to.have.been.calledOnceWithExactly(8080)
63+
})
64+
})
65+
66+
describe('close', () => {
67+
it('calls webServer.close', () => {
68+
adapter.close()
69+
70+
expect(webServer.close).to.have.been.calledOnce
71+
})
72+
73+
it('invokes callback after close completes', () => {
74+
const callback = sandbox.stub()
75+
webServer.close.callsFake((cb: () => void) => cb())
76+
77+
adapter.close(callback)
78+
79+
expect(callback).to.have.been.calledOnce
80+
})
81+
82+
it('removes all listeners from webServer after close', () => {
83+
webServer.close.callsFake((cb: () => void) => cb())
84+
85+
adapter.close()
86+
87+
expect(webServer.removeAllListeners).to.have.been.calledOnce
88+
})
89+
90+
it('does not throw if callback is undefined', () => {
91+
webServer.close.callsFake((cb: () => void) => cb())
92+
93+
expect(() => adapter.close()).not.to.throw()
94+
})
95+
})
96+
97+
describe('onClientError', () => {
98+
it('ignores ECONNRESET errors', () => {
99+
const error: any = new Error('connection reset')
100+
error.code = 'ECONNRESET'
101+
const socket: any = { writable: true, end: sandbox.stub() }
102+
103+
// Access private method through event handler
104+
// Find the clientError handler registered in constructor
105+
const clientErrorCall = webServer.on.getCalls().find(
106+
(call: any) => call.args[0] === 'clientError'
107+
)
108+
const handler = clientErrorCall.args[1]
109+
110+
handler(error, socket)
111+
112+
expect(socket.end).not.to.have.been.called
113+
})
114+
115+
it('ignores errors when socket is not writable', () => {
116+
const error = new Error('some error')
117+
const socket: any = { writable: false, end: sandbox.stub() }
118+
119+
const clientErrorCall = webServer.on.getCalls().find(
120+
(call: any) => call.args[0] === 'clientError'
121+
)
122+
const handler = clientErrorCall.args[1]
123+
124+
handler(error, socket)
125+
126+
expect(socket.end).not.to.have.been.called
127+
})
128+
129+
it('sends 400 response for other client errors', () => {
130+
const error = new Error('bad request')
131+
const socket: any = { writable: true, end: sandbox.stub() }
132+
133+
const clientErrorCall = webServer.on.getCalls().find(
134+
(call: any) => call.args[0] === 'clientError'
135+
)
136+
const handler = clientErrorCall.args[1]
137+
138+
handler(error, socket)
139+
140+
expect(socket.end).to.have.been.calledOnce
141+
expect(socket.end.firstCall.args[0]).to.include('400 Bad Request')
142+
})
143+
})
144+
145+
describe('onError', () => {
146+
it('handles server errors without throwing', () => {
147+
const errorCall = webServer.on.getCalls().find(
148+
(call: any) => call.args[0] === 'error'
149+
)
150+
const handler = errorCall.args[1]
151+
152+
expect(() => handler(new Error('server error'))).not.to.throw()
153+
})
154+
})
155+
})

0 commit comments

Comments
 (0)