first commit 🎉
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import { describe, expect, it, beforeEach, mock } from 'bun:test'
|
||||
import { UnauthenticatedError, InternalServerError } from '~/helpers/errors'
|
||||
|
||||
// Mock dependencies
|
||||
mock.module('@/user/queries/query', () => ({
|
||||
default: {
|
||||
getUserByEmail: mock(async (email: string) => {
|
||||
if (email === 'test@example.com') {
|
||||
return {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'admin',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
}
|
||||
}
|
||||
if (email === 'volunteer@example.com') {
|
||||
return {
|
||||
id: 'volunteer-123',
|
||||
name: 'Test Volunteer',
|
||||
email: 'volunteer@example.com',
|
||||
role: 'volunteer',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
getVolunteerById: mock(async ({ id }: { id: string }) => {
|
||||
if (id === 'volunteer-123') {
|
||||
return {
|
||||
id: 'vol-456',
|
||||
partnerId: 'partner-789',
|
||||
name: 'Test Volunteer',
|
||||
phone: '+1234567890',
|
||||
gender: 'male',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/utils', () => ({
|
||||
passwordVerify: mock((password: string, _hash: string) => {
|
||||
return password === 'correctpassword'
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/jwt', () => ({
|
||||
sign: mock(async (payload: unknown) => {
|
||||
return `jwt.token.${Buffer.from(JSON.stringify(payload)).toString('base64')}`
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('~/plugins', () => ({
|
||||
logger: {
|
||||
debug: mock(() => undefined),
|
||||
info: mock(() => undefined),
|
||||
},
|
||||
}))
|
||||
|
||||
import CommandService from '~/modules/auth/commands/service'
|
||||
|
||||
describe('Auth CommandService', () => {
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
mock.restore()
|
||||
|
||||
// Reapply main mocks
|
||||
mock.module('@/user/queries/query', () => ({
|
||||
default: {
|
||||
getUserByEmail: mock(async (email: string) => {
|
||||
if (email === 'test@example.com') {
|
||||
return {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'admin',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
}
|
||||
}
|
||||
if (email === 'volunteer@example.com') {
|
||||
return {
|
||||
id: 'volunteer-123',
|
||||
name: 'Test Volunteer',
|
||||
email: 'volunteer@example.com',
|
||||
role: 'volunteer',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
getVolunteerById: mock(async ({ id }: { id: string }) => {
|
||||
if (id === 'volunteer-123') {
|
||||
return {
|
||||
id: 'vol-456',
|
||||
partnerId: 'partner-789',
|
||||
name: 'Test Volunteer',
|
||||
phone: '+1234567890',
|
||||
gender: 'male',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/utils', () => ({
|
||||
passwordVerify: mock((password: string, hash: string) => {
|
||||
return password === 'correctpassword'
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/jwt', () => ({
|
||||
sign: mock(async (payload: any) => {
|
||||
return `jwt.token.${Buffer.from(JSON.stringify(payload)).toString('base64')}`
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('~/plugins', () => ({
|
||||
logger: {
|
||||
debug: mock(() => {}),
|
||||
info: mock(() => {}),
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
const result = await CommandService.login({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('accessToken')
|
||||
expect(typeof result.accessToken).toBe('string')
|
||||
expect(result.accessToken).toMatch(/^jwt\.token\./)
|
||||
})
|
||||
|
||||
it('should throw UnauthenticatedError for non-existent user', async () => {
|
||||
expect(async () => {
|
||||
await CommandService.login({
|
||||
email: 'nonexistent@example.com',
|
||||
password: 'anypassword',
|
||||
})
|
||||
}).toThrow(UnauthenticatedError)
|
||||
})
|
||||
|
||||
it('should throw UnauthenticatedError for incorrect password', async () => {
|
||||
expect(async () => {
|
||||
await CommandService.login({
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword',
|
||||
})
|
||||
}).toThrow(UnauthenticatedError)
|
||||
})
|
||||
|
||||
it('should login volunteer user successfully', async () => {
|
||||
const result = await CommandService.login({
|
||||
email: 'volunteer@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
|
||||
expect(result).toHaveProperty('accessToken')
|
||||
expect(typeof result.accessToken).toBe('string')
|
||||
|
||||
// Decode the JWT payload to verify volunteer data is included
|
||||
const payloadBase64 = result.accessToken.split('.')[2]
|
||||
const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString())
|
||||
expect(payload.user).toHaveProperty('volunteer')
|
||||
expect(payload.user.volunteer.id).toBe('vol-456')
|
||||
})
|
||||
|
||||
it('should throw InternalServerError when volunteer detail not found', async () => {
|
||||
// Mock getUserByEmail to return a volunteer user but getVolunteerById to return null
|
||||
const mockUserQuery = await import('~/modules/user/queries/query')
|
||||
mockUserQuery.default.getUserByEmail = mock(async () => ({
|
||||
id: 'volunteer-without-detail',
|
||||
name: 'Volunteer Without Detail',
|
||||
email: 'volunteer-no-detail@example.com',
|
||||
role: 'volunteer',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
})) as any
|
||||
mockUserQuery.default.getVolunteerById = mock(async () => null) as any
|
||||
|
||||
expect(async () => {
|
||||
await CommandService.login({
|
||||
email: 'volunteer-no-detail@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
}).toThrow(InternalServerError)
|
||||
})
|
||||
|
||||
it('should generate unique sub for each login', async () => {
|
||||
const result1 = await CommandService.login({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
|
||||
// Wait a millisecond to ensure different timestamp
|
||||
await new Promise((resolve) => setTimeout(resolve, 1))
|
||||
|
||||
const result2 = await CommandService.login({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
|
||||
// Decode both tokens to compare sub values
|
||||
const payload1Base64 = result1.accessToken.split('.')[2]
|
||||
const payload1 = JSON.parse(Buffer.from(payload1Base64, 'base64').toString())
|
||||
|
||||
const payload2Base64 = result2.accessToken.split('.')[2]
|
||||
const payload2 = JSON.parse(Buffer.from(payload2Base64, 'base64').toString())
|
||||
|
||||
expect(payload1.sub).not.toBe(payload2.sub)
|
||||
})
|
||||
|
||||
it('should include correct user data in JWT payload', async () => {
|
||||
const result = await CommandService.login({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
})
|
||||
|
||||
const payloadBase64 = result.accessToken.split('.')[2]
|
||||
const payload = JSON.parse(Buffer.from(payloadBase64, 'base64').toString())
|
||||
|
||||
expect(payload.user.id).toBe('user-123')
|
||||
expect(payload.user.name).toBe('Test User')
|
||||
expect(payload.user.email).toBe('test@example.com')
|
||||
expect(payload.user.role).toBe('admin')
|
||||
expect(payload.user).not.toHaveProperty('password')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
import { describe, expect, it, beforeEach, mock } from 'bun:test'
|
||||
import { Elysia } from 'elysia'
|
||||
import { router } from '~/modules/auth/router'
|
||||
|
||||
// Mock dependencies
|
||||
mock.module('~/middlewares', () => ({
|
||||
basicAuthMiddleware: mock((headers: Record<string, string | undefined>, set: { status?: number }) => {
|
||||
if (!headers.authorization || headers.authorization !== 'Basic dGVzdDp0ZXN0') {
|
||||
set.status = 401
|
||||
throw new Error('Unauthenticated.')
|
||||
}
|
||||
}),
|
||||
bearerAuthMiddleware: mock(async (headers: Record<string, string | undefined>, set: { status?: number }) => {
|
||||
if (!headers.authorization || !headers.authorization.startsWith('Bearer ')) {
|
||||
set.status = 401
|
||||
throw new Error('Unauthenticated.')
|
||||
}
|
||||
return {
|
||||
id: 123,
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
phone: null,
|
||||
image: null,
|
||||
googleId: null,
|
||||
role: 'admin',
|
||||
status: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('@/user/queries/query', () => ({
|
||||
default: {
|
||||
getUserByEmail: mock(async (email: string) => {
|
||||
if (email === 'test@example.com') {
|
||||
return {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'admin',
|
||||
image: null,
|
||||
googleId: null,
|
||||
password: '$2a$10$mockhashedpassword',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
getVolunteerById: mock(async ({ id }: { id: string }) => {
|
||||
if (id === 'volunteer-123') {
|
||||
return {
|
||||
id: 'vol-456',
|
||||
partnerId: 'partner-789',
|
||||
name: 'Test Volunteer',
|
||||
phone: '+1234567890',
|
||||
gender: 'male',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/utils', () => ({
|
||||
passwordVerify: mock((password: string, _hash: string) => {
|
||||
return password === 'correctpassword'
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module('~/helpers/jwt', () => ({
|
||||
sign: mock(async (_payload: unknown) => 'mock-access-token-12345'),
|
||||
}))
|
||||
|
||||
describe('Auth Router', () => {
|
||||
let app: Elysia
|
||||
|
||||
beforeEach(() => {
|
||||
app = new Elysia().use(router)
|
||||
})
|
||||
|
||||
describe('POST /auth/v1/login', () => {
|
||||
it('should login successfully with valid credentials', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic dGVzdDp0ZXN0', // test:test in base64
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.message).toBe('Logged in.')
|
||||
expect(data.data.accessToken).toBe('mock-access-token-12345')
|
||||
})
|
||||
|
||||
it('should fail login with invalid basic auth', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic aW52YWxpZA==', // invalid:invalid in base64
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'test@example.com',
|
||||
password: 'correctpassword',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('should fail login without authorization header', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('should validate request body schema', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Basic dGVzdDp0ZXN0',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: 'invalid-email',
|
||||
password: 'short',
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(422)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /auth/v1/info', () => {
|
||||
it('should get user info with valid bearer token', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/info', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Bearer valid-token-123',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
const data = await response.json()
|
||||
expect(data.message).toBe('Get user info.')
|
||||
expect(data.data.id).toBe(123)
|
||||
expect(data.data.email).toBe('test@example.com')
|
||||
})
|
||||
|
||||
it('should fail with invalid bearer token', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/info', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'Invalid token-123',
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('should fail without authorization header', async () => {
|
||||
const response = await app.handle(
|
||||
new Request('http://localhost/auth/v1/info', {
|
||||
method: 'GET',
|
||||
}),
|
||||
)
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,220 @@
|
||||
import { describe, expect, it } from 'bun:test'
|
||||
import { loginBody, getInfoResponse, loginResponseSchema, getInfoSchema } from '~/modules/auth/schema'
|
||||
|
||||
describe('Auth Schema', () => {
|
||||
describe('loginBody', () => {
|
||||
it('should validate correct login body', () => {
|
||||
const validBody = {
|
||||
email: 'test@example.com',
|
||||
password: 'password123',
|
||||
}
|
||||
|
||||
expect(() => loginBody.parse(validBody)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid email format', () => {
|
||||
const invalidBody = {
|
||||
email: 'invalid-email',
|
||||
password: 'password123',
|
||||
}
|
||||
|
||||
expect(() => loginBody.parse(invalidBody)).toThrow()
|
||||
})
|
||||
|
||||
it('should reject short password', () => {
|
||||
const invalidBody = {
|
||||
email: 'test@example.com',
|
||||
password: 'short',
|
||||
}
|
||||
|
||||
expect(() => loginBody.parse(invalidBody)).toThrow()
|
||||
})
|
||||
|
||||
it('should reject missing email', () => {
|
||||
const invalidBody = {
|
||||
password: 'password123',
|
||||
}
|
||||
|
||||
expect(() => loginBody.parse(invalidBody)).toThrow()
|
||||
})
|
||||
|
||||
it('should reject missing password', () => {
|
||||
const invalidBody = {
|
||||
email: 'test@example.com',
|
||||
}
|
||||
|
||||
expect(() => loginBody.parse(invalidBody)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getInfoResponse', () => {
|
||||
it('should validate admin user info', () => {
|
||||
const adminUser = {
|
||||
id: 123,
|
||||
name: 'Admin User',
|
||||
email: 'admin@example.com',
|
||||
phone: null,
|
||||
image: null,
|
||||
googleId: null,
|
||||
role: 'admin',
|
||||
status: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
expect(() => getInfoResponse.parse(adminUser)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should validate volunteer user info', () => {
|
||||
const volunteerUser = {
|
||||
id: 456,
|
||||
name: 'Volunteer User',
|
||||
email: 'volunteer@example.com',
|
||||
phone: '+1234567890',
|
||||
image: null,
|
||||
googleId: null,
|
||||
role: 'volunteer',
|
||||
status: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
expect(() => getInfoResponse.parse(volunteerUser)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject invalid role', () => {
|
||||
const invalidUser = {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'invalid-role',
|
||||
image: null,
|
||||
googleId: null,
|
||||
}
|
||||
|
||||
expect(() => getInfoResponse.parse(invalidUser)).toThrow()
|
||||
})
|
||||
|
||||
it('should reject volunteer without volunteer data', () => {
|
||||
const invalidVolunteer = {
|
||||
role: 'volunteer',
|
||||
}
|
||||
|
||||
expect(() => getInfoResponse.parse(invalidVolunteer)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('loginResponseSchema', () => {
|
||||
it('should have correct 200 response structure', () => {
|
||||
const response200 = loginResponseSchema[200]
|
||||
expect(response200).toBeDefined()
|
||||
|
||||
const validResponse = {
|
||||
message: 'Logged in.',
|
||||
data: {
|
||||
accessToken: 'some-jwt-token',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => response200.parse(validResponse)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should have correct 401 response structure', () => {
|
||||
const response401 = loginResponseSchema[401]
|
||||
expect(response401).toBeDefined()
|
||||
|
||||
const validError = {
|
||||
message: 'Invalid username or password.',
|
||||
}
|
||||
|
||||
expect(() => response401.parse(validError)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject 200 response with wrong message', () => {
|
||||
const response200 = loginResponseSchema[200]
|
||||
const invalidResponse = {
|
||||
message: 'Wrong message.',
|
||||
data: {
|
||||
accessToken: 'some-jwt-token',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => response200.parse(invalidResponse)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getInfoSchema', () => {
|
||||
it('should have correct 200 response structure for admin', () => {
|
||||
const response200 = getInfoSchema[200]
|
||||
expect(response200).toBeDefined()
|
||||
|
||||
const validResponse = {
|
||||
message: 'Get user info.',
|
||||
data: {
|
||||
id: 123,
|
||||
name: 'Admin User',
|
||||
email: 'admin@example.com',
|
||||
phone: null,
|
||||
image: null,
|
||||
googleId: null,
|
||||
role: 'admin',
|
||||
status: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => response200.parse(validResponse)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should have correct 200 response structure for volunteer', () => {
|
||||
const response200 = getInfoSchema[200]
|
||||
|
||||
const validResponse = {
|
||||
message: 'Get user info.',
|
||||
data: {
|
||||
id: 789,
|
||||
name: 'Volunteer User',
|
||||
email: 'volunteer@example.com',
|
||||
phone: '+1234567890',
|
||||
image: null,
|
||||
googleId: null,
|
||||
role: 'volunteer',
|
||||
status: true,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => response200.parse(validResponse)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should have correct 401 response structure', () => {
|
||||
const response401 = getInfoSchema[401]
|
||||
expect(response401).toBeDefined()
|
||||
|
||||
const validError = {
|
||||
message: 'Unauthenticated.',
|
||||
}
|
||||
|
||||
expect(() => response401.parse(validError)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should reject 200 response with wrong message', () => {
|
||||
const response200 = getInfoSchema[200]
|
||||
const invalidResponse = {
|
||||
message: 'Wrong message.',
|
||||
data: {
|
||||
id: 'user-123',
|
||||
name: 'Test User',
|
||||
email: 'test@example.com',
|
||||
role: 'admin',
|
||||
image: null,
|
||||
googleId: null,
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => response200.parse(invalidResponse)).toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user