176 lines
5.2 KiB
TypeScript
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)
|
|
})
|
|
})
|