Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/tests/unit/kbx-navigation-hardening.contract.spec.ts
T

46 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { KbxNavigationResolvedEntry } from '@kbx/contracts'
import { buildKbxHomeQuickStart, buildKbxHomeWorkbench, matchKbxRoutePattern, resolveKbxSafeRecentPath, resolveKbxSafeWorkspacePath } from '@kbx/ui'
const entry=(screenId:string,path:string,homePriority?:number):KbxNavigationResolvedEntry=>({screenId,path,title:screenId,module:'ERP',section:'업무',permissions:[],menuVisible:true,favoriteAllowed:true,recentPolicy:'screen',homePriority})
describe('KBX navigation hardening',()=>{
it('deduplicates home quick start in favorite, tab, recent, priority order',()=>{
const a=entry('A','/a',30),b=entry('B','/b',10),c=entry('C','/c',20)
const result=buildKbxHomeQuickStart([a,b,c],[a],[{screenId:'B',path:'/b',title:'B',visitedAt:'2026-08-08T10:00:00Z'}],[{key:'C:/c',screenId:'C',title:'C',path:'/c',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z'}])
expect(result.map(x=>x.screenId)).toEqual(['A','C','B'])
})
it('never trusts a persisted route unless route mode and path boundary are both valid',()=>{
const base={...entry('A','/erp/items'),recentPolicy:'route' as const}
expect(resolveKbxSafeRecentPath(base,'/erp/items/123?tab=stock')).toBe('/erp/items/123')
expect(resolveKbxSafeRecentPath(base,'/erp/items-evil/123')).toBe('/erp/items')
expect(resolveKbxSafeRecentPath(base,'https://evil.example/x')).toBe('/erp/items')
expect(resolveKbxSafeRecentPath({...base,recentPolicy:'screen'},'/erp/items/123')).toBe('/erp/items')
})
it('keeps dirty/open work ahead of favorites and avoids duplicate home launch rows',()=>{
const a=entry('A','/a',30),b=entry('B','/b',10)
const result=buildKbxHomeWorkbench([a,b],[a],[],[
{key:'A:/a',screenId:'A',title:'A',path:'/a',openedAt:'2026-08-08T09:00:00Z',lastActivatedAt:'2026-08-08T11:00:00Z',dirty:true},
])
expect(result.filter(x=>x.screenId==='A')).toHaveLength(1)
expect(result[0]?.source).toBe('dirty')
})
it('validates workspace deep links against the registered route capability',()=>{
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/edit')).toBe(true)
expect(matchKbxRoutePattern('/oms/orders/:orderId/edit','/oms/orders/123/delete')).toBe(false)
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'/oms/orders/123/edit','/home')).toBe('/oms/orders/123/edit')
expect(resolveKbxSafeWorkspacePath(['/oms/orders/:orderId/edit'],'https://evil.example/oms/orders/123/edit','/home')).toBe('/home')
})
it('rejects oversized and control-character workspace paths',()=>{
const fallback='/home'
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],`/erp/items/${'a'.repeat(2100)}`,fallback)).toBe(fallback)
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/abc\n123',fallback)).toBe(fallback)
expect(resolveKbxSafeWorkspacePath(['/erp/items/:itemId'],'/erp/items/%2e%2e',fallback)).toBe(fallback)
})
})