test(settings): settingsGuard unit tests — tab gating scenarios
Extract guard logic to settingsGuard.ts for testability. Router beforeEach keeps async config.load() wrapper, delegates to sync guard. 14 test cases cover system/fine-tune/developer gates across cloud/self-hosted/tier/GPU profile combos.
This commit is contained in:
parent
feea057463
commit
3e41dbf030
3 changed files with 168 additions and 12 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { useAppConfigStore } from '../stores/appConfig'
|
import { useAppConfigStore } from '../stores/appConfig'
|
||||||
|
import { settingsGuard } from './settingsGuard'
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
|
|
@ -39,16 +40,5 @@ router.beforeEach(async (to, _from, next) => {
|
||||||
if (!to.path.startsWith('/settings/')) return next()
|
if (!to.path.startsWith('/settings/')) return next()
|
||||||
const config = useAppConfigStore()
|
const config = useAppConfigStore()
|
||||||
if (!config.loaded) await config.load()
|
if (!config.loaded) await config.load()
|
||||||
const tab = to.path.replace('/settings/', '')
|
settingsGuard(to, _from, next)
|
||||||
const devOverride = config.devTierOverride
|
|
||||||
const gpuProfiles = ['single-gpu', 'dual-gpu']
|
|
||||||
|
|
||||||
if (tab === 'system' && config.isCloud) return next('/settings/my-profile')
|
|
||||||
if (tab === 'fine-tune') {
|
|
||||||
const cloudBlocked = config.isCloud && config.tier !== 'premium'
|
|
||||||
const selfHostedBlocked = !config.isCloud && !gpuProfiles.includes(config.inferenceProfile)
|
|
||||||
if (cloudBlocked || selfHostedBlocked) return next('/settings/my-profile')
|
|
||||||
}
|
|
||||||
if (tab === 'developer' && !config.isDevMode && !devOverride) return next('/settings/my-profile')
|
|
||||||
next()
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
135
web/src/router/settings.guard.test.ts
Normal file
135
web/src/router/settings.guard.test.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { setActivePinia, createPinia } from 'pinia'
|
||||||
|
import { useAppConfigStore } from '../stores/appConfig'
|
||||||
|
import { settingsGuard } from './settingsGuard'
|
||||||
|
|
||||||
|
vi.mock('../composables/useApi', () => ({ useApiFetch: vi.fn() }))
|
||||||
|
|
||||||
|
describe('settingsGuard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('passes through non-settings routes immediately', () => {
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/review' }, {}, next)
|
||||||
|
// Guard only handles /settings/* — for non-settings routes the router
|
||||||
|
// calls next() before reaching settingsGuard, but the guard itself
|
||||||
|
// will still call next() with no redirect since no tab matches
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /settings/system in cloud mode', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = true
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/system' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith('/settings/my-profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/system in self-hosted mode', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = false
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/system' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /settings/fine-tune for non-GPU self-hosted', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = false
|
||||||
|
store.inferenceProfile = 'cpu'
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith('/settings/my-profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/fine-tune for single-gpu self-hosted', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = false
|
||||||
|
store.inferenceProfile = 'single-gpu'
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/fine-tune for dual-gpu self-hosted', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = false
|
||||||
|
store.inferenceProfile = 'dual-gpu'
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /settings/fine-tune on cloud when tier is not premium', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = true
|
||||||
|
store.tier = 'paid'
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith('/settings/my-profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/fine-tune on cloud when tier is premium', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = true
|
||||||
|
store.tier = 'premium'
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/fine-tune' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects /settings/developer when not dev mode and no override', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isDevMode = false
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/developer' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith('/settings/my-profile')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/developer when isDevMode is true', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isDevMode = true
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/developer' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/developer when dev_tier_override set in localStorage', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isDevMode = false
|
||||||
|
localStorage.setItem('dev_tier_override', 'premium')
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/developer' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/privacy in cloud mode', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = true
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/privacy' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/privacy in self-hosted mode', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = false
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/privacy' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows /settings/license in both modes', () => {
|
||||||
|
const store = useAppConfigStore()
|
||||||
|
store.isCloud = true
|
||||||
|
const next = vi.fn()
|
||||||
|
settingsGuard({ path: '/settings/license' }, {}, next)
|
||||||
|
expect(next).toHaveBeenCalledWith()
|
||||||
|
})
|
||||||
|
})
|
||||||
31
web/src/router/settingsGuard.ts
Normal file
31
web/src/router/settingsGuard.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useAppConfigStore } from '../stores/appConfig'
|
||||||
|
|
||||||
|
const GPU_PROFILES = ['single-gpu', 'dual-gpu']
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronous tab-gating logic for /settings/* routes.
|
||||||
|
* Called by the async router.beforeEach after config.load() has resolved.
|
||||||
|
* Reading devTierOverride from localStorage here (not only the store ref) ensures
|
||||||
|
* the guard reflects overrides set externally before the store hydrates.
|
||||||
|
*/
|
||||||
|
export function settingsGuard(
|
||||||
|
to: { path: string },
|
||||||
|
_from: unknown,
|
||||||
|
next: (to?: string) => void,
|
||||||
|
): void {
|
||||||
|
const config = useAppConfigStore()
|
||||||
|
const tab = to.path.replace('/settings/', '')
|
||||||
|
const devOverride = config.devTierOverride || localStorage.getItem('dev_tier_override')
|
||||||
|
|
||||||
|
if (tab === 'system' && config.isCloud) return next('/settings/my-profile')
|
||||||
|
|
||||||
|
if (tab === 'fine-tune') {
|
||||||
|
const cloudBlocked = config.isCloud && config.tier !== 'premium'
|
||||||
|
const selfHostedBlocked = !config.isCloud && !GPU_PROFILES.includes(config.inferenceProfile)
|
||||||
|
if (cloudBlocked || selfHostedBlocked) return next('/settings/my-profile')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tab === 'developer' && !config.isDevMode && !devOverride) return next('/settings/my-profile')
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue