Archipelago — open-source initial import

This commit is contained in:
Archipelago
2026-08-12 10:55:49 +00:00
commit 82ca09f8b9
1664 changed files with 359095 additions and 0 deletions
@@ -0,0 +1,154 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import { setActivePinia, createPinia } from 'pinia'
import { defineComponent } from 'vue'
// Mock the app store module
const mockStore = {
isAuthenticated: false,
isConnected: false,
isReconnecting: false,
needsSessionValidation: vi.fn().mockReturnValue(false),
checkSession: vi.fn().mockResolvedValue(false),
connectWebSocket: vi.fn().mockResolvedValue(undefined),
}
vi.mock('@/stores/app', () => ({
useAppStore: () => mockStore,
}))
const Stub = defineComponent({ template: '<div />' })
function createTestRouter() {
return createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Stub,
meta: { public: true },
children: [
{ path: '', component: Stub },
{ path: 'login', name: 'login', component: Stub },
{ path: 'onboarding/intro', name: 'onboarding-intro', component: Stub },
],
},
{
path: '/dashboard',
component: Stub,
children: [
{ path: '', name: 'home', component: Stub },
{ path: 'apps', name: 'apps', component: Stub },
{ path: 'settings', name: 'settings', component: Stub },
],
},
],
})
}
describe('Router Guards', () => {
let router: ReturnType<typeof createTestRouter>
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
mockStore.isAuthenticated = false
mockStore.isConnected = false
mockStore.isReconnecting = false
mockStore.needsSessionValidation.mockReturnValue(false)
mockStore.checkSession.mockResolvedValue(false)
router = createTestRouter()
// Add the same beforeEach guard as the real router
router.beforeEach(async (to) => {
const isPublic = to.meta.public
if (isPublic) {
if (to.path === '/login' && mockStore.isAuthenticated) {
if (mockStore.needsSessionValidation()) {
return true
}
return { name: 'home' }
}
return true
}
if (mockStore.needsSessionValidation()) {
mockStore.checkSession()
return true
}
if (!mockStore.isAuthenticated) {
const hasSession = await mockStore.checkSession()
if (hasSession) return true
return '/login'
}
if (!mockStore.isConnected && !mockStore.isReconnecting) {
mockStore.connectWebSocket()
}
return true
})
})
it('allows unauthenticated access to public routes', async () => {
await router.push('/login')
expect(router.currentRoute.value.path).toBe('/login')
})
it('redirects unauthenticated users from protected routes to login', async () => {
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/login')
})
it('allows authenticated users to access protected routes', async () => {
mockStore.isAuthenticated = true
await router.push('/dashboard/apps')
expect(router.currentRoute.value.path).toBe('/dashboard/apps')
})
it('redirects authenticated users from /login to home', async () => {
mockStore.isAuthenticated = true
mockStore.needsSessionValidation.mockReturnValue(false)
await router.push('/login')
expect(router.currentRoute.value.name).toBe('home')
})
it('validates stale session and allows access if valid', async () => {
mockStore.isAuthenticated = true
mockStore.needsSessionValidation.mockReturnValue(true)
await router.push('/dashboard/settings')
expect(router.currentRoute.value.path).toBe('/dashboard/settings')
expect(mockStore.checkSession).toHaveBeenCalled()
})
it('allows access to onboarding routes without auth', async () => {
await router.push('/onboarding/intro')
expect(router.currentRoute.value.path).toBe('/onboarding/intro')
})
it('triggers WebSocket connection for authenticated users without connection', async () => {
mockStore.isAuthenticated = true
mockStore.isConnected = false
mockStore.isReconnecting = false
await router.push('/dashboard')
expect(mockStore.connectWebSocket).toHaveBeenCalled()
})
it('does not reconnect WebSocket if already connected', async () => {
mockStore.isAuthenticated = true
mockStore.isConnected = true
await router.push('/dashboard')
expect(mockStore.connectWebSocket).not.toHaveBeenCalled()
})
it('does not reconnect WebSocket if already reconnecting', async () => {
mockStore.isAuthenticated = true
mockStore.isConnected = false
mockStore.isReconnecting = true
await router.push('/dashboard')
expect(mockStore.connectWebSocket).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,243 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createRouter, createWebHistory } from 'vue-router'
import { setActivePinia, createPinia } from 'pinia'
import { defineComponent } from 'vue'
// Mock the app store module
const mockStore = {
isAuthenticated: false,
isConnected: false,
isReconnecting: false,
needsSessionValidation: vi.fn().mockReturnValue(false),
checkSession: vi.fn().mockResolvedValue(false),
connectWebSocket: vi.fn().mockResolvedValue(undefined),
}
vi.mock('@/stores/app', () => ({
useAppStore: () => mockStore,
}))
const Stub = defineComponent({ template: '<div />' })
function createTestRouter() {
return createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: Stub,
meta: { public: true },
children: [
{ path: '', component: Stub },
{ path: 'login', name: 'login', component: Stub },
{ path: 'onboarding/intro', name: 'onboarding-intro', component: Stub },
{ path: 'onboarding/options', name: 'onboarding-options', component: Stub },
{ path: 'onboarding/path', name: 'onboarding-path', component: Stub },
{ path: 'onboarding/did', name: 'onboarding-did', component: Stub },
{ path: 'onboarding/identity', name: 'onboarding-identity', component: Stub },
{ path: 'onboarding/backup', name: 'onboarding-backup', component: Stub },
{ path: 'onboarding/verify', name: 'onboarding-verify', component: Stub },
{ path: 'onboarding/done', name: 'onboarding-done', component: Stub },
],
},
{
path: '/dashboard',
component: Stub,
children: [
{ path: '', name: 'home', component: Stub },
{ path: 'apps', name: 'apps', component: Stub },
{ path: 'settings', name: 'settings', component: Stub },
],
},
],
})
}
describe('Onboarding Routing Flow', () => {
let router: ReturnType<typeof createTestRouter>
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
mockStore.isAuthenticated = false
mockStore.isConnected = false
mockStore.isReconnecting = false
mockStore.needsSessionValidation.mockReturnValue(false)
mockStore.checkSession.mockResolvedValue(false)
router = createTestRouter()
// Add the same beforeEach guard as the real router
router.beforeEach(async (to) => {
const isPublic = to.meta.public
if (isPublic) {
if (to.path === '/login' && mockStore.isAuthenticated) {
if (mockStore.needsSessionValidation()) {
return true
}
return { name: 'home' }
}
return true
}
if (mockStore.needsSessionValidation()) {
mockStore.checkSession()
return true
}
if (!mockStore.isAuthenticated) {
const hasSession = await mockStore.checkSession()
if (hasSession) return true
return '/login'
}
if (!mockStore.isConnected && !mockStore.isReconnecting) {
mockStore.connectWebSocket()
}
return true
})
})
describe('unauthenticated users', () => {
it('redirects to /login when accessing /dashboard, not to /onboarding', async () => {
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/login')
expect(router.currentRoute.value.path).not.toContain('/onboarding')
})
it('redirects to /login when accessing /dashboard/apps', async () => {
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard/apps')
expect(router.currentRoute.value.path).toBe('/login')
})
it('redirects to /login when accessing /dashboard/settings', async () => {
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard/settings')
expect(router.currentRoute.value.path).toBe('/login')
})
})
describe('onboarding routes are accessible when authenticated', () => {
beforeEach(() => {
mockStore.isAuthenticated = true
})
it('allows access to /onboarding/intro', async () => {
await router.push('/onboarding/intro')
expect(router.currentRoute.value.path).toBe('/onboarding/intro')
expect(router.currentRoute.value.name).toBe('onboarding-intro')
})
it('allows access to /onboarding/options', async () => {
await router.push('/onboarding/options')
expect(router.currentRoute.value.path).toBe('/onboarding/options')
expect(router.currentRoute.value.name).toBe('onboarding-options')
})
it('allows access to /onboarding/path', async () => {
await router.push('/onboarding/path')
expect(router.currentRoute.value.path).toBe('/onboarding/path')
expect(router.currentRoute.value.name).toBe('onboarding-path')
})
})
describe('OnboardingDid route', () => {
it('is accessible when authenticated', async () => {
mockStore.isAuthenticated = true
await router.push('/onboarding/did')
expect(router.currentRoute.value.path).toBe('/onboarding/did')
expect(router.currentRoute.value.name).toBe('onboarding-did')
})
it('is accessible when unauthenticated (public route)', async () => {
mockStore.isAuthenticated = false
await router.push('/onboarding/did')
expect(router.currentRoute.value.path).toBe('/onboarding/did')
expect(router.currentRoute.value.name).toBe('onboarding-did')
})
})
describe('post-onboarding navigation to dashboard', () => {
it('allows authenticated users to navigate from onboarding to /dashboard', async () => {
mockStore.isAuthenticated = true
// Start at onboarding done page
await router.push('/onboarding/done')
expect(router.currentRoute.value.path).toBe('/onboarding/done')
// Navigate to dashboard (simulating post-onboarding completion)
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/dashboard')
expect(router.currentRoute.value.name).toBe('home')
})
it('allows authenticated users to navigate from onboarding/did to /dashboard', async () => {
mockStore.isAuthenticated = true
await router.push('/onboarding/did')
expect(router.currentRoute.value.path).toBe('/onboarding/did')
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/dashboard')
expect(router.currentRoute.value.name).toBe('home')
})
it('allows navigation through the full onboarding sequence', async () => {
mockStore.isAuthenticated = true
await router.push('/onboarding/intro')
expect(router.currentRoute.value.name).toBe('onboarding-intro')
await router.push('/onboarding/path')
expect(router.currentRoute.value.name).toBe('onboarding-path')
await router.push('/onboarding/did')
expect(router.currentRoute.value.name).toBe('onboarding-did')
await router.push('/onboarding/identity')
expect(router.currentRoute.value.name).toBe('onboarding-identity')
await router.push('/onboarding/backup')
expect(router.currentRoute.value.name).toBe('onboarding-backup')
await router.push('/onboarding/verify')
expect(router.currentRoute.value.name).toBe('onboarding-verify')
await router.push('/onboarding/done')
expect(router.currentRoute.value.name).toBe('onboarding-done')
})
})
describe('dashboard guard blocks unauthenticated access after onboarding', () => {
it('prevents unauthenticated navigation from onboarding/done to /dashboard', async () => {
// Start at onboarding done (public route, accessible without auth)
await router.push('/onboarding/done')
expect(router.currentRoute.value.path).toBe('/onboarding/done')
// Try to navigate to dashboard without auth
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/login')
})
it('prevents unauthenticated access to /dashboard even with stale session', async () => {
mockStore.isAuthenticated = false
mockStore.needsSessionValidation.mockReturnValue(false)
mockStore.checkSession.mockResolvedValue(false)
await router.push('/dashboard/apps')
expect(router.currentRoute.value.path).toBe('/login')
})
it('allows dashboard access when session check succeeds', async () => {
mockStore.isAuthenticated = false
mockStore.checkSession.mockResolvedValue(true)
await router.push('/dashboard')
expect(router.currentRoute.value.path).toBe('/dashboard')
})
})
})
@@ -0,0 +1,93 @@
import { describe, it, expect } from 'vitest'
/**
* Tests for router route definitions and configuration.
* Full guard tests are in guards.test.ts. This tests route structure.
*/
describe('router route definitions', () => {
it('has expected public routes', () => {
const publicPaths = ['/', '/login', '/onboarding/intro', '/onboarding/options',
'/onboarding/path', '/onboarding/did', '/onboarding/identity',
'/onboarding/backup', '/onboarding/verify', '/onboarding/done', '/recovery']
// These should all resolve (we test the path list itself)
expect(publicPaths.length).toBe(11)
publicPaths.forEach(p => {
expect(typeof p).toBe('string')
expect(p.startsWith('/')).toBe(true)
})
})
it('has expected dashboard routes', () => {
const dashPaths = [
'/dashboard', '/dashboard/apps', '/dashboard/marketplace',
'/dashboard/cloud', '/dashboard/server', '/dashboard/web5',
'/dashboard/settings', '/dashboard/chat', '/dashboard/monitoring',
]
dashPaths.forEach(p => {
expect(p.startsWith('/dashboard')).toBe(true)
})
})
it('has parameterized routes', () => {
const paramRoutes = [
{ path: '/dashboard/apps/:id', example: '/dashboard/apps/bitcoin-knots' },
{ path: '/dashboard/marketplace/:id', example: '/dashboard/marketplace/lnd' },
{ path: '/dashboard/cloud/:folderId', example: '/dashboard/cloud/photos' },
{ path: '/dashboard/goals/:goalId', example: '/dashboard/goals/sync-bitcoin' },
]
paramRoutes.forEach(r => {
const pattern = r.path.replace(/:(\w+)/g, '([^/]+)')
expect(new RegExp(`^${pattern}$`).test(r.example)).toBe(true)
})
})
it('containers routes redirect to apps', () => {
// The router redirects /dashboard/containers -> /dashboard/apps
// and /dashboard/containers/:id -> /dashboard/apps/:id
const redirectMap = {
'containers': 'apps',
'containers/bitcoin-knots': 'apps/bitcoin-knots',
}
Object.entries(redirectMap).forEach(([from, to]) => {
expect(to).toBe(from.replace('containers', 'apps'))
})
})
it('SESSION_CHECK_TIMEOUT_MS is a reasonable value', () => {
const SESSION_CHECK_TIMEOUT_MS = 8000
expect(SESSION_CHECK_TIMEOUT_MS).toBeGreaterThan(1000)
expect(SESSION_CHECK_TIMEOUT_MS).toBeLessThanOrEqual(15000)
})
})
describe('checkSessionWithTimeout logic', () => {
it('resolves with session result when fast', async () => {
const checkSession = () => Promise.resolve(true)
const result = await Promise.race([
checkSession(),
new Promise<boolean>((resolve) =>
setTimeout(() => resolve(false), 8000)
),
])
expect(result).toBe(true)
})
it('resolves false when session check fails', async () => {
const checkSession = () => Promise.reject(new Error('Network error'))
try {
await Promise.race([
checkSession(),
new Promise<boolean>((resolve) =>
setTimeout(() => resolve(false), 8000)
),
])
} catch {
// Expected - the catch in the real code returns false
expect(true).toBe(true)
}
})
})