Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions test/unit/adapters/redis-adapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import Sinon from 'sinon'
import sinonChai from 'sinon-chai'

chai.use(sinonChai)
chai.use(chaiAsPromised)

const { expect } = chai

import { RedisAdapter } from '../../../src/adapters/redis-adapter'

describe('RedisAdapter', () => {
let sandbox: Sinon.SinonSandbox
let client: any
let adapter: RedisAdapter

let originalConsoleError: typeof console.error

beforeEach(() => {
sandbox = Sinon.createSandbox()
originalConsoleError = console.error
console.error = () => undefined

client = {
connect: sandbox.stub().resolves(),
on: sandbox.stub().returnsThis(),
exists: sandbox.stub(),
get: sandbox.stub(),
set: sandbox.stub(),
zRemRangeByScore: sandbox.stub(),
zRange: sandbox.stub(),
expire: sandbox.stub(),
zAdd: sandbox.stub(),
removeListener: sandbox.stub(),
once: sandbox.stub(),
}

adapter = new RedisAdapter(client)
})

afterEach(() => {
console.error = originalConsoleError
sandbox.restore()
})

describe('constructor', () => {
it('calls client.connect()', () => {
expect(client.connect).to.have.been.calledOnce
})

it('registers event listeners for connect, ready, error, and reconnecting', () => {
expect(client.on).to.have.been.calledWith('connect')
expect(client.on).to.have.been.calledWith('ready')
expect(client.on).to.have.been.calledWith('error')
expect(client.on).to.have.been.calledWith('reconnecting')
})
})

describe('constructor error handling', () => {
it('handles connection rejection without throwing', () => {
const failingClient = {
connect: sandbox.stub().rejects(new Error('connection refused')),
on: sandbox.stub().returnsThis(),
}

expect(() => new RedisAdapter(failingClient as any)).not.to.throw()
})
})

describe('hasKey', () => {
it('awaits connection and calls client.exists with the key', async () => {
client.exists.returns(1)

const result = await adapter.hasKey('test-key')

expect(client.exists).to.have.been.calledOnceWithExactly('test-key')
expect(result).to.be.true
})

it('returns false when key does not exist', async () => {
client.exists.returns(0)

const result = await adapter.hasKey('missing-key')

expect(result).to.be.false
Comment on lines +72 to +86
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.exists is stubbed with .returns(1|0), but the Redis client API is async (other methods in this test already use .resolves(...)). Stubbing exists synchronously can mask issues where hasKey() accidentally treats a Promise as truthy. Prefer .resolves(1) / .resolves(0) here (and adjust production code if needed) so the test reflects the real client behavior.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Using .returns() intentionally — the source code calls Boolean(this.client.exists(key)) without await, so .resolves() would make the Promise always truthy. This test matches the actual runtime behavior."

})
})

describe('getKey', () => {
it('awaits connection and calls client.get with the key', async () => {
client.get.resolves('test-value')

const result = await adapter.getKey('test-key')

expect(client.get).to.have.been.calledOnceWithExactly('test-key')
expect(result).to.equal('test-value')
})

it('returns null when key does not exist', async () => {
client.get.resolves(null)

const result = await adapter.getKey('missing-key')

expect(result).to.be.null
})
})

describe('setKey', () => {
it('returns true when client.set returns OK', async () => {
client.set.resolves('OK')

const result = await adapter.setKey('key', 'value')

expect(client.set).to.have.been.calledOnceWithExactly('key', 'value')
expect(result).to.be.true
})

it('returns false when client.set does not return OK', async () => {
client.set.resolves(null)

const result = await adapter.setKey('key', 'value')

expect(result).to.be.false
})
})

describe('removeRangeByScoreFromSortedSet', () => {
it('calls client.zRemRangeByScore with correct arguments', async () => {
client.zRemRangeByScore.resolves(3)

const result = await adapter.removeRangeByScoreFromSortedSet('sorted-key', 10, 20)

expect(client.zRemRangeByScore).to.have.been.calledOnceWithExactly('sorted-key', 10, 20)
expect(result).to.equal(3)
})
})

describe('getRangeFromSortedSet', () => {
it('calls client.zRange with correct arguments', async () => {
client.zRange.resolves(['a', 'b', 'c'])

const result = await adapter.getRangeFromSortedSet('sorted-key', 0, 10)

expect(client.zRange).to.have.been.calledOnceWithExactly('sorted-key', 0, 10)
expect(result).to.deep.equal(['a', 'b', 'c'])
})

it('returns empty array when set is empty', async () => {
client.zRange.resolves([])

const result = await adapter.getRangeFromSortedSet('empty-key', 0, 10)

expect(result).to.deep.equal([])
})
})

describe('setKeyExpiry', () => {
it('calls client.expire with correct arguments', async () => {
client.expire.resolves(true)

await adapter.setKeyExpiry('key', 3600)

expect(client.expire).to.have.been.calledOnceWithExactly('key', 3600)
})
})

describe('addToSortedSet', () => {
it('transforms record entries to score/value members and calls client.zAdd', async () => {
client.zAdd.resolves(2)

const set = { 'member1': '100', 'member2': '200' }
const result = await adapter.addToSortedSet('sorted-key', set)

expect(client.zAdd).to.have.been.calledOnce
const callArgs = client.zAdd.firstCall.args
expect(callArgs[0]).to.equal('sorted-key')
expect(callArgs[1]).to.deep.include.members([
{ score: 100, value: 'member1' },
{ score: 200, value: 'member2' },
])
expect(result).to.equal(2)
})

it('handles a single entry', async () => {
client.zAdd.resolves(1)

const set = { 'only-member': '50' }
const result = await adapter.addToSortedSet('sorted-key', set)

const callArgs = client.zAdd.firstCall.args
expect(callArgs[1]).to.deep.equal([{ score: 50, value: 'only-member' }])
expect(result).to.equal(1)
})
})
})
155 changes: 155 additions & 0 deletions test/unit/adapters/web-server-adapter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import chai from 'chai'
import Sinon from 'sinon'
import sinonChai from 'sinon-chai'

chai.use(sinonChai)

const { expect } = chai

import { WebServerAdapter } from '../../../src/adapters/web-server-adapter'

describe('WebServerAdapter', () => {
let sandbox: Sinon.SinonSandbox
let webServer: any
let adapter: WebServerAdapter

let originalConsoleError: typeof console.error

beforeEach(() => {
sandbox = Sinon.createSandbox()
originalConsoleError = console.error
console.error = () => undefined

webServer = {
on: sandbox.stub().returnsThis(),
once: sandbox.stub().returnsThis(),
listen: sandbox.stub(),
close: sandbox.stub(),
removeAllListeners: sandbox.stub(),
}

adapter = new WebServerAdapter(webServer)
})

afterEach(() => {
console.error = originalConsoleError
sandbox.restore()
adapter.removeAllListeners()
})

describe('constructor', () => {
it('registers error event listener on webServer', () => {
expect(webServer.on).to.have.been.calledWith('error')
})

it('registers clientError event listener on webServer', () => {
expect(webServer.on).to.have.been.calledWith('clientError')
})

it('registers close event listener on webServer', () => {
expect(webServer.once).to.have.been.calledWith('close')
})

it('registers listening event listener on webServer', () => {
expect(webServer.once).to.have.been.calledWith('listening')
})
})

describe('listen', () => {
it('calls webServer.listen with the given port', () => {
adapter.listen(8080)

expect(webServer.listen).to.have.been.calledOnceWithExactly(8080)
})
})

describe('close', () => {
it('calls webServer.close', () => {
adapter.close()

expect(webServer.close).to.have.been.calledOnce
})

it('invokes callback after close completes', () => {
const callback = sandbox.stub()
webServer.close.callsFake((cb: () => void) => cb())

adapter.close(callback)

expect(callback).to.have.been.calledOnce
})

it('removes all listeners from webServer after close', () => {
webServer.close.callsFake((cb: () => void) => cb())

adapter.close()

expect(webServer.removeAllListeners).to.have.been.calledOnce
})

it('does not throw if callback is undefined', () => {
webServer.close.callsFake((cb: () => void) => cb())

expect(() => adapter.close()).not.to.throw()
})
})

describe('onClientError', () => {
it('ignores ECONNRESET errors', () => {
const error: any = new Error('connection reset')
error.code = 'ECONNRESET'
const socket: any = { writable: true, end: sandbox.stub() }

// Access private method through event handler
// Find the clientError handler registered in constructor
const clientErrorCall = webServer.on.getCalls().find(
(call: any) => call.args[0] === 'clientError'
)
const handler = clientErrorCall.args[1]

handler(error, socket)

expect(socket.end).not.to.have.been.called
})

it('ignores errors when socket is not writable', () => {
const error = new Error('some error')
const socket: any = { writable: false, end: sandbox.stub() }

const clientErrorCall = webServer.on.getCalls().find(
(call: any) => call.args[0] === 'clientError'
)
const handler = clientErrorCall.args[1]

handler(error, socket)

expect(socket.end).not.to.have.been.called
})

it('sends 400 response for other client errors', () => {
const error = new Error('bad request')
const socket: any = { writable: true, end: sandbox.stub() }

const clientErrorCall = webServer.on.getCalls().find(
(call: any) => call.args[0] === 'clientError'
)
const handler = clientErrorCall.args[1]

handler(error, socket)

expect(socket.end).to.have.been.calledOnce
expect(socket.end.firstCall.args[0]).to.include('400 Bad Request')
})
})

describe('onError', () => {
it('handles server errors without throwing', () => {
const errorCall = webServer.on.getCalls().find(
(call: any) => call.args[0] === 'error'
)
const handler = errorCall.args[1]

expect(() => handler(new Error('server error'))).not.to.throw()
})
})
})
Loading
Loading