feat: Archipelago demo stack (lightweight)
This commit is contained in:
175
neode-ui/src/stores/__tests__/app.test.ts
Normal file
175
neode-ui/src/stores/__tests__/app.test.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user