Skip to content
Closed
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
164 changes: 164 additions & 0 deletions packages/core/src/__tests__/middleware/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,170 @@ describe('requireAuth middleware - Error Handling', () => {
})
})

describe('JWT secret externalization (env var)', () => {
it('should generate token with custom secret', async () => {
const customSecret = 'my-custom-secret-key-from-env'
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', customSecret
)

expect(token).toBeTruthy()
expect(token.split('.')).toHaveLength(3)
})

it('should verify token with matching custom secret', async () => {
const customSecret = 'my-custom-secret-key-from-env'
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', customSecret
)

const payload = await AuthManager.verifyToken(token, customSecret)
expect(payload).not.toBeNull()
expect(payload?.userId).toBe('user-123')
expect(payload?.email).toBe('test@example.com')
})

it('should reject token when verified with wrong secret', async () => {
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', 'secret-a'
)

const payload = await AuthManager.verifyToken(token, 'secret-b')
expect(payload).toBeNull()
})

it('should reject custom-secret token when verified with fallback', async () => {
const customSecret = 'my-custom-secret-key-from-env'
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', customSecret
)

// Verify without providing secret (uses fallback)
const payload = await AuthManager.verifyToken(token)
expect(payload).toBeNull()
})

it('should verify fallback-secret token without providing secret', async () => {
// Generate without custom secret (uses fallback)
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin'
)

// Verify without custom secret (uses same fallback)
const payload = await AuthManager.verifyToken(token)
expect(payload).not.toBeNull()
expect(payload?.userId).toBe('user-123')
})
})

describe('requireAuth middleware - JWT_SECRET from env', () => {
let mockNext: Next

beforeEach(() => {
mockNext = vi.fn()
})

it('should use JWT_SECRET from env when verifying tokens', async () => {
const envSecret = 'env-jwt-secret-12345'
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', envSecret
)

const mockContext: any = {
req: {
header: vi.fn().mockImplementation((name: string) => {
if (name === 'Authorization') return `Bearer ${token}`
return undefined
}),
raw: { headers: new Headers() }
},
set: vi.fn(),
json: vi.fn(),
redirect: vi.fn(),
env: { JWT_SECRET: envSecret }
}

const middleware = requireAuth()
await middleware(mockContext as Context, mockNext)

expect(mockContext.set).toHaveBeenCalledWith('user', expect.objectContaining({
userId: 'user-123',
email: 'test@example.com',
role: 'admin'
}))
expect(mockNext).toHaveBeenCalled()
})

it('should reject token signed with different secret than env JWT_SECRET', async () => {
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'admin', 'wrong-secret'
)

const mockContext: any = {
req: {
header: vi.fn().mockImplementation((name: string) => {
if (name === 'Authorization') return `Bearer ${token}`
return undefined
}),
raw: { headers: new Headers() }
},
set: vi.fn(),
json: vi.fn().mockReturnValue({ error: 'Invalid or expired token' }),
redirect: vi.fn(),
env: { JWT_SECRET: 'correct-env-secret' }
}

const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})

const middleware = requireAuth()
await middleware(mockContext as Context, mockNext)

expect(mockContext.json).toHaveBeenCalledWith(
{ error: 'Invalid or expired token' },
401
)
expect(mockNext).not.toHaveBeenCalled()

consoleSpy.mockRestore()
})
})

describe('optionalAuth middleware - JWT_SECRET from env', () => {
let mockNext: Next

beforeEach(() => {
mockNext = vi.fn()
})

it('should use JWT_SECRET from env when verifying tokens', async () => {
const envSecret = 'env-jwt-secret-12345'
const token = await AuthManager.generateToken(
'user-123', 'test@example.com', 'user', envSecret
)

const mockContext: any = {
req: {
header: vi.fn().mockImplementation((name: string) => {
if (name === 'Authorization') return `Bearer ${token}`
return undefined
}),
raw: { headers: new Headers() }
},
set: vi.fn(),
env: { JWT_SECRET: envSecret }
}

const middleware = optionalAuth()
await middleware(mockContext as Context, mockNext)

expect(mockContext.set).toHaveBeenCalledWith('user', expect.objectContaining({
userId: 'user-123',
role: 'user'
}))
expect(mockNext).toHaveBeenCalled()
})
})

describe('requireRole middleware - Browser Redirects', () => {
let mockContext: any
let mockNext: Next
Expand Down
220 changes: 220 additions & 0 deletions packages/core/src/__tests__/middleware/cors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { cors } from 'hono/cors'

/**
* Tests for the CORS origin allowlist implementation.
*
* The CORS middleware is configured inline in routes/api.ts using hono/cors.
* We recreate the same origin callback logic here to test it in isolation.
*/

// Replicate the exact origin callback from routes/api.ts
function createCorsOriginCallback() {
return (origin: string, c: any): string | null => {
const allowed = (c.env as any)?.CORS_ORIGINS as string | undefined
if (!allowed) return null
const list = allowed.split(',').map((s: string) => s.trim())
return list.includes(origin) ? origin : null
}
}

describe('CORS origin allowlist', () => {
function createApp(corsOrigins?: string) {
const app = new Hono<{ Bindings: { CORS_ORIGINS?: string } }>()

app.use(
'*',
cors({
origin: createCorsOriginCallback(),
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
})
)

app.get('/api/test', (c) => c.json({ ok: true }))

return app
}

describe('when CORS_ORIGINS is not set', () => {
it('should not include Access-Control-Allow-Origin header', async () => {
const app = createApp()
const res = await app.request('/api/test', {
headers: { Origin: 'https://evil.com' },
})

expect(res.status).toBe(200)
expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull()
})

it('should reject preflight requests from any origin', async () => {
const app = createApp()
const res = await app.request('/api/test', {
method: 'OPTIONS',
headers: {
Origin: 'https://evil.com',
'Access-Control-Request-Method': 'POST',
},
})

expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull()
})
})

describe('when CORS_ORIGINS is set to a single origin', () => {
const ORIGINS = 'https://myapp.com'

it('should allow requests from the configured origin', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{ headers: { Origin: 'https://myapp.com' } },
{ CORS_ORIGINS: ORIGINS }
)

expect(res.headers.get('Access-Control-Allow-Origin')).toBe(
'https://myapp.com'
)
})

it('should reject requests from a non-configured origin', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{ headers: { Origin: 'https://evil.com' } },
{ CORS_ORIGINS: ORIGINS }
)

expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull()
})
})

describe('when CORS_ORIGINS is set to multiple origins', () => {
const ORIGINS = 'https://app1.com, https://app2.com, http://localhost:8787'

it('should allow requests from any listed origin', async () => {
const app = createApp(ORIGINS)

const res1 = await app.request(
'/api/test',
{ headers: { Origin: 'https://app1.com' } },
{ CORS_ORIGINS: ORIGINS }
)
expect(res1.headers.get('Access-Control-Allow-Origin')).toBe(
'https://app1.com'
)

const res2 = await app.request(
'/api/test',
{ headers: { Origin: 'https://app2.com' } },
{ CORS_ORIGINS: ORIGINS }
)
expect(res2.headers.get('Access-Control-Allow-Origin')).toBe(
'https://app2.com'
)

const res3 = await app.request(
'/api/test',
{ headers: { Origin: 'http://localhost:8787' } },
{ CORS_ORIGINS: ORIGINS }
)
expect(res3.headers.get('Access-Control-Allow-Origin')).toBe(
'http://localhost:8787'
)
})

it('should reject requests from unlisted origins', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{ headers: { Origin: 'https://evil.com' } },
{ CORS_ORIGINS: ORIGINS }
)

expect(res.headers.get('Access-Control-Allow-Origin')).toBeNull()
})
})

describe('preflight requests', () => {
const ORIGINS = 'https://myapp.com'

it('should handle OPTIONS preflight for allowed origin', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{
method: 'OPTIONS',
headers: {
Origin: 'https://myapp.com',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type',
},
},
{ CORS_ORIGINS: ORIGINS }
)

expect(res.headers.get('Access-Control-Allow-Origin')).toBe(
'https://myapp.com'
)
expect(res.headers.get('Access-Control-Allow-Methods')).toContain('POST')
})

it('should include X-API-Key in allowed headers', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{
method: 'OPTIONS',
headers: {
Origin: 'https://myapp.com',
'Access-Control-Request-Method': 'GET',
'Access-Control-Request-Headers': 'X-API-Key',
},
},
{ CORS_ORIGINS: ORIGINS }
)

expect(res.headers.get('Access-Control-Allow-Headers')).toContain(
'X-API-Key'
)
})
})

describe('allowed methods', () => {
const ORIGINS = 'https://myapp.com'

it('should allow GET, POST, PUT, DELETE, OPTIONS methods', async () => {
const app = createApp(ORIGINS)
const res = await app.request(
'/api/test',
{
method: 'OPTIONS',
headers: {
Origin: 'https://myapp.com',
'Access-Control-Request-Method': 'DELETE',
},
},
{ CORS_ORIGINS: ORIGINS }
)

const allowedMethods = res.headers.get('Access-Control-Allow-Methods')
expect(allowedMethods).toContain('GET')
expect(allowedMethods).toContain('POST')
expect(allowedMethods).toContain('PUT')
expect(allowedMethods).toContain('DELETE')
expect(allowedMethods).toContain('OPTIONS')
})
})

describe('same-origin requests', () => {
it('should work normally without Origin header (same-origin)', async () => {
const app = createApp('https://myapp.com')
const res = await app.request('/api/test', {}, { CORS_ORIGINS: 'https://myapp.com' })

expect(res.status).toBe(200)
const body = await res.json()
expect(body).toEqual({ ok: true })
})
})
})
Loading