Files
archy-demo/neode-ui/src/stores/__tests__/app.test.ts
2026-03-17 02:14:04 +00:00

176 lines
5.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
// Mock the rpc-client module
vi.mock('@/api/rpc-client', () => ({
rpcClient: {
login: vi.fn(),
logout: vi.fn(),
call: vi.fn(),
installPackage: vi.fn(),
uninstallPackage: vi.fn(),
startPackage: vi.fn(),
stopPackage: vi.fn(),
restartPackage: vi.fn(),
updateServer: vi.fn(),
restartServer: vi.fn(),
shutdownServer: vi.fn(),
getMetrics: vi.fn(),
getMarketplace: vi.fn(),
},
}))
// Mock the websocket module
vi.mock('@/api/websocket', () => ({
wsClient: {
connect: vi.fn().mockResolvedValue(undefined),
disconnect: vi.fn(),
subscribe: vi.fn(),
isConnected: vi.fn().mockReturnValue(false),
onConnectionStateChange: vi.fn(),
},
applyDataPatch: vi.fn(),
}))
import { useAppStore } from '../app'
import { rpcClient } from '@/api/rpc-client'
import { wsClient } from '@/api/websocket'
const mockedRpc = vi.mocked(rpcClient)
const mockedWs = vi.mocked(wsClient)
describe('useAppStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
localStorage.clear()
mockedWs.isConnected.mockReturnValue(false)
})
it('starts with default unauthenticated state', () => {
const store = useAppStore()
expect(store.isAuthenticated).toBe(false)
expect(store.isConnected).toBe(false)
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
expect(store.data).toBeNull()
})
it('login succeeds and sets authenticated state', async () => {
mockedRpc.login.mockResolvedValue(null)
const store = useAppStore()
await store.login('password123')
expect(store.isAuthenticated).toBe(true)
expect(localStorage.getItem('neode-auth')).toBe('true')
expect(store.data).not.toBeNull()
expect(store.isLoading).toBe(false)
})
it('login handles TOTP requirement', async () => {
mockedRpc.login.mockResolvedValue({ requires_totp: true })
const store = useAppStore()
const result = await store.login('password123')
expect(result).toEqual({ requires_totp: true })
expect(store.isAuthenticated).toBe(false)
})
it('login sets error on failure', async () => {
mockedRpc.login.mockRejectedValue(new Error('Invalid password'))
const store = useAppStore()
await expect(store.login('wrong')).rejects.toThrow('Invalid password')
expect(store.error).toBe('Invalid password')
expect(store.isAuthenticated).toBe(false)
expect(store.isLoading).toBe(false)
})
it('logout clears all state', async () => {
mockedRpc.login.mockResolvedValue(null)
mockedRpc.logout.mockResolvedValue(undefined)
const store = useAppStore()
// Login first
await store.login('password123')
expect(store.isAuthenticated).toBe(true)
// Then logout
await store.logout()
expect(store.isAuthenticated).toBe(false)
expect(store.data).toBeNull()
expect(store.isConnected).toBe(false)
expect(localStorage.getItem('neode-auth')).toBeNull()
expect(mockedWs.disconnect).toHaveBeenCalled()
})
it('logout still clears state even if RPC fails', async () => {
mockedRpc.logout.mockRejectedValue(new Error('Network error'))
localStorage.setItem('neode-auth', 'true')
const store = useAppStore()
await store.logout()
expect(store.isAuthenticated).toBe(false)
expect(localStorage.getItem('neode-auth')).toBeNull()
})
it('checkSession returns true on valid session', async () => {
localStorage.setItem('neode-auth', 'true')
mockedRpc.call.mockResolvedValue('ping')
const store = useAppStore()
const valid = await store.checkSession()
expect(valid).toBe(true)
expect(store.isAuthenticated).toBe(true)
expect(store.data).not.toBeNull()
})
it('checkSession returns false when no auth in localStorage', async () => {
const store = useAppStore()
const valid = await store.checkSession()
expect(valid).toBe(false)
})
it('checkSession returns false and clears state on expired session', async () => {
localStorage.setItem('neode-auth', 'true')
mockedRpc.call.mockRejectedValue(new Error('401 Unauthorized'))
const store = useAppStore()
const valid = await store.checkSession()
expect(valid).toBe(false)
expect(store.isAuthenticated).toBe(false)
expect(localStorage.getItem('neode-auth')).toBeNull()
expect(mockedWs.disconnect).toHaveBeenCalled()
})
it('connectWebSocket subscribes and connects', async () => {
mockedWs.connect.mockResolvedValue(undefined)
// First call: not connected (triggers connect), second call: connected (after connect)
mockedWs.isConnected.mockReturnValueOnce(false).mockReturnValue(true)
const store = useAppStore()
await store.connectWebSocket()
expect(mockedWs.subscribe).toHaveBeenCalledOnce()
expect(mockedWs.onConnectionStateChange).toHaveBeenCalledOnce()
expect(mockedWs.connect).toHaveBeenCalledOnce()
})
it('needsSessionValidation returns true when auth but not validated', () => {
localStorage.setItem('neode-auth', 'true')
const store = useAppStore()
// isAuthenticated is true from localStorage, but sessionValidated is false
expect(store.needsSessionValidation()).toBe(true)
})
})