first commit 🎉

This commit is contained in:
2025-10-08 11:37:54 +07:00
commit 4ec61fa51e
61 changed files with 4558 additions and 0 deletions
+244
View File
@@ -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')
})
})
})
+198
View File
@@ -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)
})
})
})
+220
View File
@@ -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()
})
})
})