Skip to content

Commit

Permalink
test: Added unit and integration test
Browse files Browse the repository at this point in the history
  • Loading branch information
Adil committed Sep 21, 2024
1 parent 6ef94f0 commit 69bce2c
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 1 deletion.
30 changes: 29 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/jest": "^27.4.0",
"@types/mime": "1.3.5",
"@types/node": "^17.0.8",
"@types/supertest": "^6.0.2",
"babel-preset-medusa-package": "^1.1.19",
"cross-env": "^7.0.3",
"eslint": "^9.11.0",
Expand All @@ -53,9 +54,36 @@
"pinst": "^3.0.0",
"prettier": "^3.3.3",
"rimraf": "^3.0.2",
"supertest": "^7.0.0",
"ts-jest": "^27.0.7",
"ts-loader": "^9.2.6",
"typescript": "^4.5.2"
},
"prettier": "@epic-web/config/prettier"
"prettier": "@epic-web/config/prettier",
"jest": {
"globals": {
"ts-jest": {
"tsconfig": "tsconfig.spec.json"
}
},
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"testPathIgnorePatterns": [
"/node_modules/",
"<rootDir>/node_modules/"
],
"rootDir": "src",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$",
"transform": {
".ts": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "./coverage",
"testEnvironment": "node"
}
}
75 changes: 75 additions & 0 deletions src/api/__tests__/server-integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import express from 'express'
import { type Redis } from 'ioredis'
import { type MedusaContainer } from 'medusa-core-utils'
import request from 'supertest'

import RateLimitService from '../../services/rate-limit'
import defaultRateLimit from '../middlewares/default-rate-limit'

const mockRedisClient = {
incr: jest.fn(),
expire: jest.fn(),
get: jest.fn(),
del: jest.fn(),
ttl: jest.fn(),
} as unknown as jest.Mocked<Redis>

const mockContainer = {
resolve: jest
.fn()
.mockReturnValue(
new RateLimitService(
{ redisClient: mockRedisClient },
{ limit: 5, window: 60 },
),
),
}

const app = express()
app.use((req, res, next) => {
req.scope = mockContainer as unknown as jest.Mocked<MedusaContainer>
next()
})
app.use(defaultRateLimit)
app.get('/test', (req, res) => {
res.status(200).send('OK')
})

describe('Rate Limit Middleware', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('should allow requests under the limit', async () => {
mockRedisClient.incr.mockResolvedValueOnce(1)
mockRedisClient.expire.mockResolvedValueOnce(1)

const response = await request(app).get('/test')

expect(response.status).toBe(200)
expect(response.text).toBe('OK')
})

it('should block requests over the limit', async () => {
mockRedisClient.incr.mockResolvedValueOnce(6)
mockRedisClient.ttl.mockResolvedValueOnce(60)

const response = await request(app).get('/test')

expect(response.status).toBe(429)
expect(response.body).toEqual({
error: 'Too many requests, please try again later.',
})
})

it('should return the correct headers', async () => {
mockRedisClient.incr.mockResolvedValueOnce(1)
mockRedisClient.expire.mockResolvedValueOnce(60)
mockRedisClient.get.mockResolvedValueOnce('1')

const response = await request(app).get('/test')

expect(response.headers['x-ratelimit-limit']).toBe('5')
expect(response.headers['x-ratelimit-remaining']).toBe('4')
})
})
83 changes: 83 additions & 0 deletions src/services/__tests__/rate-limit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { type PluginOptions } from '../../types/options'
import { type Redis } from 'ioredis'

import RateLimitService from '../rate-limit'

const mockRedisClient = {
incr: jest.fn(),
expire: jest.fn(),
get: jest.fn(),
del: jest.fn(),
ttl: jest.fn(),
} as unknown as jest.Mocked<Redis>

const defaultOptions: PluginOptions = {
limit: 5,
window: 60,
}

describe('RateLimitService', () => {
let rateLimitService: RateLimitService

beforeEach(() => {
rateLimitService = new RateLimitService(
{ redisClient: mockRedisClient },
defaultOptions,
)
})

afterEach(() => {
jest.clearAllMocks()
})

it('should increment the key and set expiry if it is the first request', async () => {
mockRedisClient.incr.mockResolvedValueOnce(1)
mockRedisClient.expire.mockResolvedValueOnce(1)

const result = await rateLimitService.limit('test-key')

expect(mockRedisClient.incr).toHaveBeenCalledWith('test-key')
expect(mockRedisClient.expire).toHaveBeenCalledWith('test-key', 60)
expect(result).toBe(true)
})

it('should return false if the limit is exceeded', async () => {
mockRedisClient.incr.mockResolvedValueOnce(6)

const result = await rateLimitService.limit('test-key')

expect(mockRedisClient.incr).toHaveBeenCalledWith('test-key')
expect(result).toBe(false)
})

it('should return the number of remaining attempts', async () => {
mockRedisClient.get.mockResolvedValueOnce('3')

const remainingAttempts =
await rateLimitService.getRemainingAttempts('test-key')

expect(mockRedisClient.get).toHaveBeenCalledWith('test-key')
expect(remainingAttempts).toBe(2)
})

it('should reset the limit for the given key', async () => {
await rateLimitService.resetLimit('test-key')

expect(mockRedisClient.del).toHaveBeenCalledWith('test-key')
})

it('should return the time to live for the given key', async () => {
mockRedisClient.ttl.mockResolvedValueOnce(30)

const ttl = await rateLimitService.ttl('test-key')

expect(mockRedisClient.ttl).toHaveBeenCalledWith('test-key')
expect(ttl).toBe(30)
})

it('should return the default options', () => {
const options = rateLimitService.getOptions()

expect(options).toEqual(defaultOptions)
})
})
5 changes: 5 additions & 0 deletions tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["dist", "node_modules"]
}

0 comments on commit 69bce2c

Please sign in to comment.