Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/llms-txt-hub / apps/web/app/api/extension-feedback/__tests__/route.test.ts |
| Date | Not specified in source |
| Repository | Janady13/llms-txt-hub |
| Revision | e0f4bf978d4df168369c7601d9c5f5ea89e84377 |
| Branch | main |
| Path or archive member | apps/web/app/api/extension-feedback/__tests__/route.test.ts |
| Original SHA-256 | 56b5a1965ba76ca0f287ec0e9b116bdb62f2992a565f0656a10dc64ec027fdd6 |
| Git blob SHA | e7eb450fa3401fc0d081012b8393fd9cc4248b1b |
| Reading-copy SHA-256 | 56b5a1965ba76ca0f287ec0e9b116bdb62f2992a565f0656a10dc64ec027fdd6 |
Recovered source transcript
Download complete source text ↓240 lines · 6.6 KB
import { logger } from '@thedaviddias/logging'
import { GET, POST } from '@/app/api/extension-feedback/route'
import { getRawClient } from '@/lib/redis'
jest.mock('@sentry/nextjs', () => ({
captureException: jest.fn()
}))
jest.mock('@thedaviddias/logging', () => ({
logger: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
}
}))
jest.mock('@/lib/redis', () => ({
getRawClient: jest.fn()
}))
const createRequest = (body: unknown, ip = '203.0.113.200') =>
new Request('http://localhost/api/extension-feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-forwarded-for': ip
},
body: JSON.stringify(body)
})
const createGetRequest = (query = '', token?: string) =>
new Request(`http://localhost/api/extension-feedback${query}`, {
method: 'GET',
headers: token
? {
'x-feedback-token': token
}
: undefined
})
describe('/api/extension-feedback', () => {
const originalReadToken = process.env.EXTENSION_FEEDBACK_READ_TOKEN
beforeEach(() => {
jest.clearAllMocks()
;(getRawClient as jest.Mock).mockReturnValue(null)
delete process.env.EXTENSION_FEEDBACK_READ_TOKEN
})
afterAll(() => {
process.env.EXTENSION_FEEDBACK_READ_TOKEN = originalReadToken
})
it('accepts a valid uninstall feedback payload', async () => {
const response = await POST(
createRequest({
event: 'uninstall',
reason: 'Too noisy or distracting',
comment: 'It interrupted my workflow.',
version: '2.0.0',
lang: 'en-US',
submittedAt: '2026-03-03T20:00:00.000Z'
}) as any
)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.ok).toBe(true)
expect(logger.info).toHaveBeenCalled()
})
it('persists full feedback payload to redis when available', async () => {
const pipeline = {
lpush: jest.fn().mockReturnThis(),
ltrim: jest.fn().mockReturnThis(),
exec: jest.fn().mockResolvedValue([])
}
;(getRawClient as jest.Mock).mockReturnValue({
pipeline: () => pipeline
})
const response = await POST(
createRequest({
event: 'uninstall',
reason: 'Missing key features',
comment: 'Please add per-site exclusions.',
version: '2.1.0',
lang: 'en-US',
submittedAt: '2026-03-03T20:00:00.000Z'
}) as any
)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.ok).toBe(true)
expect(pipeline.lpush).toHaveBeenCalledWith(
'extension-feedback:uninstall:v1',
expect.stringContaining('"comment":"Please add per-site exclusions."')
)
expect(pipeline.ltrim).toHaveBeenCalledWith('extension-feedback:uninstall:v1', 0, 999)
expect(pipeline.exec).toHaveBeenCalled()
})
it('rejects payloads with missing reason', async () => {
const response = await POST(
createRequest({
event: 'uninstall',
version: '2.0.0',
submittedAt: '2026-03-03T20:00:00.000Z'
}) as any
)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.ok).toBe(false)
expect(data.error).toBe('Invalid feedback payload')
})
it('rejects payloads when comment exceeds max length', async () => {
const response = await POST(
createRequest({
event: 'uninstall',
reason: 'Other',
comment: 'a'.repeat(1001),
submittedAt: '2026-03-03T20:00:00.000Z'
}) as any
)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.ok).toBe(false)
})
it('applies per-IP rate limiting', async () => {
const ip = '203.0.113.77'
for (let i = 0; i < 10; i++) {
const allowed = await POST(
createRequest(
{
event: 'uninstall',
reason: 'I only needed it temporarily',
submittedAt: '2026-03-03T20:00:00.000Z'
},
ip
) as any
)
expect(allowed.status).toBe(200)
}
const blocked = await POST(
createRequest(
{
event: 'uninstall',
reason: 'I only needed it temporarily',
submittedAt: '2026-03-03T20:00:00.000Z'
},
ip
) as any
)
const data = await blocked.json()
expect(blocked.status).toBe(429)
expect(data.ok).toBe(false)
expect(data.error).toBe('Rate limit exceeded. Please try again later.')
})
it('returns a safe 500 response for unexpected failures', async () => {
;(logger.info as jest.Mock).mockImplementationOnce(() => {
throw new Error('unexpected logger failure')
})
const response = await POST(
createRequest({
event: 'uninstall',
reason: 'Other',
submittedAt: '2026-03-03T20:00:00.000Z'
}) as any
)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.ok).toBe(false)
expect(data.error).toBe('Unable to process feedback right now. Please try again later.')
expect(logger.error).toHaveBeenCalled()
})
it('returns 404 for feedback list when read token is not configured', async () => {
const response = await GET(createGetRequest('?limit=5') as any)
const data = await response.json()
expect(response.status).toBe(404)
expect(data.ok).toBe(false)
expect(data.error).toBe('Not found')
})
it('returns 401 for feedback list when token is invalid', async () => {
process.env.EXTENSION_FEEDBACK_READ_TOKEN = 'secret-read-token'
const response = await GET(createGetRequest('?limit=5', 'wrong-token') as any)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.ok).toBe(false)
expect(data.error).toBe('Unauthorized')
})
it('returns full feedback entries when token is valid', async () => {
process.env.EXTENSION_FEEDBACK_READ_TOKEN = 'secret-read-token'
;(getRawClient as jest.Mock).mockReturnValue({
lrange: jest.fn().mockResolvedValue([
JSON.stringify({
id: 'fb_1',
event: 'uninstall',
reason: 'Too noisy or distracting',
comment: 'Need quieter defaults on local sites.',
version: '2.1.0',
lang: 'en-US',
submittedAt: '2026-03-03T20:00:00.000Z',
receivedAt: '2026-03-03T20:00:05.000Z'
})
])
})
const response = await GET(createGetRequest('?limit=1', 'secret-read-token') as any)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.ok).toBe(true)
expect(data.count).toBe(1)
expect(data.feedback[0].comment).toBe('Need quieter defaults on local sites.')
})
})