241 lines
6.5 KiB
TypeScript
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' },
|
|
)
|
|
})
|
|
})
|