Files
Genarrative/apps/mobile-shell/src/shell/navigation.test.ts
T
kdletters e0f53f2994 统一移动壳外链协议来源
让移动壳导航测试按共享 HostBridge 外链协议清单反查

增加移动壳配置门禁防止 WebView 外链协议在 shell 层重新硬编码

补充共享决策记录说明 WebView 外链协议必须复用共享归一化逻辑
2026-06-19 14:51:21 +08:00

117 lines
3.7 KiB
TypeScript

import { describe, expect, test } from 'vitest';
import { HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS } from '../../../../packages/shared/src/contracts/hostBridge';
import {
resolveMobileShellExternalUrl,
resolveMobileShellWebViewUrl,
shouldAcceptMobileShellHostBridgeMessage,
shouldOpenInMobileShellWebView,
} from './navigation';
describe('shouldOpenInMobileShellWebView', () => {
test('只允许主站同源页面留在移动壳 WebView 内', () => {
const allowedOrigin = 'https://app.genarrative.world';
expect(
shouldOpenInMobileShellWebView(
'https://app.genarrative.world/works/detail?work=PZ-1',
allowedOrigin,
),
).toBe(true);
expect(
shouldOpenInMobileShellWebView('/creation/puzzle', allowedOrigin),
).toBe(true);
expect(
shouldOpenInMobileShellWebView(
'http://app.genarrative.world/works/detail?work=PZ-1',
allowedOrigin,
),
).toBe(false);
expect(
shouldOpenInMobileShellWebView('about:blank', allowedOrigin),
).toBe(true);
});
test('外链和非网页协议必须离开带 HostBridge 的 WebView', () => {
const allowedOrigin = 'https://app.genarrative.world';
expect(
shouldOpenInMobileShellWebView('https://example.com/', allowedOrigin),
).toBe(false);
expect(
shouldOpenInMobileShellWebView('mailto:hi@example.com', allowedOrigin),
).toBe(false);
expect(
shouldOpenInMobileShellWebView('//example.com/evil', allowedOrigin),
).toBe(false);
expect(
shouldOpenInMobileShellWebView('javascript:alert(1)', allowedOrigin),
).toBe(false);
expect(shouldOpenInMobileShellWebView('not a url', allowedOrigin)).toBe(
false,
);
});
test('只有允许协议能交给系统外部应用打开', () => {
for (const protocol of HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS) {
const url =
protocol === 'mailto:'
? 'mailto:hi@example.com'
: protocol === 'tel:'
? 'tel:+12345678'
: `${protocol}//example.com/path`;
expect(resolveMobileShellExternalUrl(` ${url} `)).toBe(url);
}
expect(resolveMobileShellExternalUrl('javascript:alert(1)')).toBeNull();
expect(resolveMobileShellExternalUrl('file:///etc/passwd')).toBeNull();
expect(resolveMobileShellExternalUrl('/relative/path')).toBeNull();
});
test('HostBridge 主动导航只解析同源网页目标', () => {
const allowedOrigin = 'https://app.genarrative.world';
expect(
resolveMobileShellWebViewUrl('/works/detail?work=PZ-1', allowedOrigin),
).toBe('https://app.genarrative.world/works/detail?work=PZ-1');
expect(
resolveMobileShellWebViewUrl(
'https://app.genarrative.world/creation/puzzle#draft',
allowedOrigin,
),
).toBe('https://app.genarrative.world/creation/puzzle#draft');
expect(
resolveMobileShellWebViewUrl('https://example.com/', allowedOrigin),
).toBeNull();
expect(
resolveMobileShellWebViewUrl('about:blank', allowedOrigin),
).toBeNull();
});
test('HostBridge 消息只接受同源主站页面', () => {
const allowedOrigin = 'https://app.genarrative.world';
expect(
shouldAcceptMobileShellHostBridgeMessage(
'https://app.genarrative.world/creation/puzzle',
allowedOrigin,
),
).toBe(true);
expect(
shouldAcceptMobileShellHostBridgeMessage('about:blank', allowedOrigin),
).toBe(false);
expect(
shouldAcceptMobileShellHostBridgeMessage(
'https://example.com/evil',
allowedOrigin,
),
).toBe(false);
expect(
shouldAcceptMobileShellHostBridgeMessage(
'javascript:alert(1)',
allowedOrigin,
),
).toBe(false);
});
});