Files
apes-sidebar/tests/html-route.spec.ts
T
Menghuan1918 302c20109f feat: 内置 HTML 预览器 + 沙箱浏览器 tab(v0.5.0)
- html viewer:.html/.htm 沙箱 iframe 预览(相对资源经 /sidebar/html 路径编码路由加载,CSP sandbox + 信任围栏 + cwd 边界),预览/编辑切换
- browser tab:+ 菜单多开(browser:<n>),地址栏后退/前进/刷新/在浏览器中打开(15px 图标),URL 与标题按会话持久化(patchTab + nextBrowser)
- 安全:iframe 一律不透明源沙箱(无 allow-same-origin/top-navigation、no-referrer、权限策略全禁、禁 srcdoc);地址栏仅 http(s)、拒绝 localhost 等本机地址;界面实时沙箱状态(关闭红色警示)+ 临时解锁/恢复
- 设置:htmlViewerNoSandbox / htmlViewerDefaultUnsafe / browserNoSandbox / browserInterceptLinks 四个声明式二级设置(默认安全)
- 外链拦截:聊天/界面 http(s) 外链默认在侧边栏打开(openTab seed.url),Ctrl/Cmd 点击放行
- 嵌入拒绝面板:browser.probe 路由探测 X-Frame-Options/frame-ancestors,被拒站点显示原因 + 在浏览器中打开/仍然加载
- 设置页可滚动;内置 7 tab + 9 viewer;新增设计文档;268 个测试全绿
2026-08-11 19:28:48 +08:00

111 lines
4.4 KiB
TypeScript

/**
* /sidebar/html route vocabulary tests: the path-encoded URL scheme must
* round-trip absolute paths (POSIX and Windows), survive special characters,
* and refuse malformed input — and crucially, a relative asset reference
* resolved against an encoded document URL stays inside the same route with
* the session scope intact (the WHY of the path-encoding design).
*/
import { describe, expect, it } from 'vitest'
import { decodeHtmlUrl, encodeHtmlUrl } from '../src/html-route.ts'
describe('encodeHtmlUrl', () => {
it('encodes a POSIX absolute path into route segments', () => {
expect(encodeHtmlUrl('sess-1', '/Users/me/proj/index.html'))
.toBe('/sidebar/html/sess-1/Users/me/proj/index.html')
})
it('encodes a Windows absolute path (drive colon percent-encoded)', () => {
expect(encodeHtmlUrl('sess-1', 'C:\\Users\\me\\a.html'))
.toBe('/sidebar/html/sess-1/C%3A/Users/me/a.html')
})
it('percent-encodes special characters in segments', () => {
expect(encodeHtmlUrl('s-1', '/a b/中文/100%.html'))
.toBe('/sidebar/html/s-1/a%20b/%E4%B8%AD%E6%96%87/100%25.html')
})
it('ignores leading/trailing slashes (files only)', () => {
expect(encodeHtmlUrl('s', '/a//b/x.html')).toBe('/sidebar/html/s/a/b/x.html')
})
})
describe('decodeHtmlUrl', () => {
it('decodes a POSIX round-trip', () => {
const url = encodeHtmlUrl('sess-1', '/Users/me/proj/index.html')
expect(decodeHtmlUrl(url)).toEqual({
ok: true,
ref: { sessionId: 'sess-1', path: '/Users/me/proj/index.html' },
})
})
it('decodes a Windows round-trip', () => {
const url = encodeHtmlUrl('sess-1', 'C:\\Users\\me\\a.html')
expect(decodeHtmlUrl(url)).toEqual({
ok: true,
ref: { sessionId: 'sess-1', path: '/C:/Users/me/a.html' },
})
})
it('decodes special characters round-trip', () => {
const url = encodeHtmlUrl('s-1', '/a b/中文/100%.html')
const result = decodeHtmlUrl(url)
expect(result.ok).toBe(true)
if (result.ok) expect(result.ref.path).toBe('/a b/中文/100%.html')
})
it('refuses a wrong prefix (404)', () => {
expect(decodeHtmlUrl('/sidebar/api/fs.read')).toEqual({
ok: false, status: 404, message: 'not an html route',
})
})
it('refuses an empty or double-slash path (400)', () => {
expect(decodeHtmlUrl('/sidebar/html/').ok).toBe(false)
expect(decodeHtmlUrl('/sidebar/html//s/a.html').ok).toBe(false)
})
it('refuses malformed percent encoding (400)', () => {
expect(decodeHtmlUrl('/sidebar/html/s/%E0%A4%A').ok).toBe(false)
})
it('refuses a missing sessionId or file path (400)', () => {
expect(decodeHtmlUrl('/sidebar/html//a.html')).toEqual({
ok: false, status: 400, message: 'sessionId and file path are required',
})
expect(decodeHtmlUrl('/sidebar/html/s/')).toEqual({
ok: false, status: 400, message: 'sessionId and file path are required',
})
})
it('keeps encoded traversal segments for the isWithin fence to bound', () => {
// The decoder is not a security boundary by itself: an encoded `..`
// decodes to `..` and the HOST refuses it via requireAbsolute +
// isWithin(cwd) (the decoded path resolves outside the cwd).
const result = decodeHtmlUrl('/sidebar/html/s/Users/me/../../etc/passwd')
expect(result.ok).toBe(true)
if (result.ok) expect(result.ref.path).toBe('/Users/me/../../etc/passwd')
})
})
describe('relative asset resolution stays in-route', () => {
it('a relative reference against an encoded document URL keeps the session scope', () => {
// WHATWG URL resolution drops the QUERY of a path-relative reference,
// which is why the route is path-encoded: resolving ./style.css against
// the document URL must land back on the same route with the same
// session prefix.
const doc = encodeHtmlUrl('sess-1', '/Users/me/proj/index.html')
const asset = new URL('./style.css', `http://h${doc}`).pathname
expect(asset).toBe('/sidebar/html/sess-1/Users/me/proj/style.css')
expect(decodeHtmlUrl(asset)).toEqual({
ok: true,
ref: { sessionId: 'sess-1', path: '/Users/me/proj/style.css' },
})
})
it('deeper and parent-relative references resolve inside the route', () => {
const doc = `http://h${encodeHtmlUrl('s', '/a/b/index.html')}`
expect(new URL('img/x.png', doc).pathname).toBe('/sidebar/html/s/a/b/img/x.png')
expect(new URL('../c.css', doc).pathname).toBe('/sidebar/html/s/a/c.css')
})
})