Files
user-system/frontend/admin/src/services/auth.test.ts

241 lines
6.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const getMock = vi.fn()
const postMock = vi.fn()
const refreshSessionBundleMock = vi.fn()
vi.mock('@/lib/http/client', () => ({
get: getMock,
post: postMock,
refreshSessionBundle: refreshSessionBundleMock,
}))
describe('auth service', () => {
beforeEach(() => {
getMock.mockReset()
postMock.mockReset()
refreshSessionBundleMock.mockReset()
postMock.mockResolvedValue(undefined)
refreshSessionBundleMock.mockResolvedValue(undefined)
})
it('loads public auth capabilities without auth headers', async () => {
const { getAuthCapabilities } = await import('./auth')
await getAuthCapabilities()
expect(getMock).toHaveBeenCalledWith('/auth/capabilities', undefined, { auth: false })
})
it('normalizes null oauth provider lists from auth capabilities', async () => {
getMock.mockResolvedValue({
password: true,
email_activation: false,
email_code: false,
sms_code: false,
password_reset: false,
admin_bootstrap_required: undefined,
oauth_providers: null,
})
const { getAuthCapabilities } = await import('./auth')
const result = await getAuthCapabilities()
expect(result.admin_bootstrap_required).toBe(false)
expect(result.email_activation).toBe(false)
expect(result.oauth_providers).toEqual([])
})
it('preserves admin bootstrap status from auth capabilities', async () => {
getMock.mockResolvedValue({
password: true,
email_activation: true,
email_code: false,
sms_code: false,
password_reset: false,
admin_bootstrap_required: true,
oauth_providers: [],
})
const { getAuthCapabilities } = await import('./auth')
const result = await getAuthCapabilities()
expect(result.admin_bootstrap_required).toBe(true)
expect(result.email_activation).toBe(true)
})
it('requests oauth authorization url without auth headers', async () => {
const { getOAuthAuthorizationUrl } = await import('./auth')
await getOAuthAuthorizationUrl('github', 'https://admin.example.com/login/oauth/callback')
expect(getMock).toHaveBeenCalledWith(
'/auth/oauth/github',
{ return_to: 'https://admin.example.com/login/oauth/callback' },
{ auth: false },
)
})
it('exchanges oauth handoff code without auth headers', async () => {
const { exchangeOAuthHandoff } = await import('./auth')
await exchangeOAuthHandoff('handoff-code')
expect(postMock).toHaveBeenCalledWith(
'/auth/oauth/exchange',
{ code: 'handoff-code' },
{ auth: false, credentials: 'include' },
)
})
it('verifies password-login totp with the temporary challenge token', async () => {
const { verifyTOTPAfterPasswordLogin } = await import('./auth')
await verifyTOTPAfterPasswordLogin({
user_id: 42,
code: '123456',
device_id: 'device-1',
temp_token: 'temp-token-demo',
})
expect(postMock).toHaveBeenCalledWith(
'/auth/login/totp-verify',
{
user_id: 42,
code: '123456',
device_id: 'device-1',
temp_token: 'temp-token-demo',
},
{ auth: false, credentials: 'include' },
)
})
it('submits public registration without auth headers', async () => {
const { register } = await import('./auth')
await register({
username: 'new-user',
password: 'SecurePass123!',
email: 'new-user@example.com',
nickname: 'New User',
})
expect(postMock).toHaveBeenCalledWith(
'/auth/register',
{
username: 'new-user',
password: 'SecurePass123!',
email: 'new-user@example.com',
nickname: 'New User',
},
{ auth: false },
)
})
it('submits first-admin bootstrap with bootstrap secret header', async () => {
const { bootstrapAdmin } = await import('./auth')
await bootstrapAdmin({
username: 'bootstrap_admin',
password: 'Bootstrap123!@#',
email: 'bootstrap_admin@example.com',
nickname: 'Bootstrap Admin',
bootstrap_secret: 'bootstrap-secret-demo',
})
expect(postMock).toHaveBeenCalledWith(
'/auth/bootstrap-admin',
{
username: 'bootstrap_admin',
password: 'Bootstrap123!@#',
email: 'bootstrap_admin@example.com',
nickname: 'Bootstrap Admin',
},
{
auth: false,
credentials: 'include',
headers: {
'X-Bootstrap-Secret': 'bootstrap-secret-demo',
},
},
)
})
it('activates email accounts without auth headers', async () => {
const { activateEmail } = await import('./auth')
await activateEmail('activation-token')
expect(postMock).toHaveBeenCalledWith(
'/auth/activate-email',
{ token: 'activation-token' },
{ auth: false },
)
})
it('resends activation emails without auth headers', async () => {
const { resendActivationEmail } = await import('./auth')
await resendActivationEmail({ email: 'new-user@example.com' })
expect(postMock).toHaveBeenCalledWith(
'/auth/resend-activation',
{ email: 'new-user@example.com' },
{ auth: false },
)
})
it('sends sms purpose instead of the deprecated scene field', async () => {
const { sendSmsCode } = await import('./auth')
await sendSmsCode({
phone: '13812345678',
purpose: 'register',
})
expect(postMock).toHaveBeenCalledWith(
'/auth/send-code',
{
phone: '13812345678',
purpose: 'register',
},
{ auth: false },
)
})
it('sends refresh_token when logging out with a persisted session', async () => {
const { logout } = await import('./auth')
await logout('refresh-token-demo')
expect(postMock).toHaveBeenCalledWith(
'/auth/logout',
{
refresh_token: 'refresh-token-demo',
},
{ credentials: 'include' },
)
})
it('omits the request body when no refresh_token is available', async () => {
const { logout } = await import('./auth')
await logout()
expect(postMock).toHaveBeenCalledWith('/auth/logout', undefined, { credentials: 'include' })
})
it('refreshes the session through the shared refresh single-flight when no body token is supplied', async () => {
const { refreshSession } = await import('./auth')
await refreshSession()
expect(refreshSessionBundleMock).toHaveBeenCalledTimes(1)
expect(postMock).not.toHaveBeenCalledWith(
'/auth/refresh',
undefined,
{ auth: false, credentials: 'include' },
)
})
})