199 lines
8.4 KiB
JavaScript
199 lines
8.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
createRouteAssetRecovery,
|
|
isRouteAssetLoadError,
|
|
createLoginRedirectHandler,
|
|
getCurrentLoginRedirect,
|
|
getSafeLoginRedirect,
|
|
createSetupLocation
|
|
} from "../../frontend/src/utils/navigation.mjs";
|
|
|
|
test("登录重定向只接受非登录页的站内路径", () => {
|
|
assert.equal(getSafeLoginRedirect("/list?tag=inbox"), "/list?tag=inbox");
|
|
assert.equal(getSafeLoginRedirect("https://example.com"), "/");
|
|
assert.equal(getSafeLoginRedirect("//example.com"), "/");
|
|
assert.equal(getSafeLoginRedirect("/%2fexample.com"), "/");
|
|
assert.equal(getSafeLoginRedirect("/login?redirect=/list"), "/");
|
|
assert.equal(getSafeLoginRedirect("/%6Cogin-verification"), "/");
|
|
assert.equal(getSafeLoginRedirect("/login/extra"), "/");
|
|
});
|
|
|
|
test('模块与样式加载错误才进入资源恢复流程', () => {
|
|
for (const message of [
|
|
'Unable to preload CSS for /assets/old.css',
|
|
'Failed to fetch dynamically imported module: https://example.com/assets/old.js',
|
|
'error loading dynamically imported module',
|
|
'Importing a module script failed.'
|
|
]) assert.equal(isRouteAssetLoadError(new Error(message)), true);
|
|
assert.equal(isRouteAssetLoadError(new Error('Network Error')), false);
|
|
assert.equal(isRouteAssetLoadError(new Error('邮件发送失败')), false);
|
|
assert.equal(isRouteAssetLoadError(null), false);
|
|
});
|
|
|
|
test('资源恢复保留目标路径且同一构建只自动刷新一次', () => {
|
|
const values = new Map();
|
|
const storage = {getItem: key => values.get(key), setItem: (key, value) => values.set(key, value)};
|
|
const paths = [];
|
|
const options = {buildID: 'release-a', getStorage: () => storage, reload: path => paths.push(path)};
|
|
const recover = createRouteAssetRecovery(options);
|
|
const error = new Error('Unable to preload CSS for /assets/old.css');
|
|
const from = {name: 'list'};
|
|
const to = {name: 'editer', fullPath: '/editer?draft=20'};
|
|
assert.equal(recover(error, to, from), true);
|
|
assert.equal(recover(error, to, from), true);
|
|
assert.deepEqual(paths, ['/editer?draft=20']);
|
|
assert.equal(createRouteAssetRecovery(options)(error, to, from), false);
|
|
assert.equal(createRouteAssetRecovery({...options, buildID: 'release-b'})(error, to, from), true);
|
|
assert.equal(paths.length, 2);
|
|
});
|
|
|
|
test('旧邮件页登录资源失效时恢复安全登录目标且不重复刷新', () => {
|
|
for (const [redirect, expected] of [
|
|
['/detail/21', '/detail/21'],
|
|
['/list?tag=inbox', '/list?tag=inbox'],
|
|
['https://example.com', '/'],
|
|
['//example.com', '/'],
|
|
['/login?redirect=/list', '/'],
|
|
[undefined, '/']
|
|
]) {
|
|
const values = new Map();
|
|
const paths = [];
|
|
const options = {
|
|
buildID: 'release-login',
|
|
getStorage: () => ({getItem: key => values.get(key), setItem: (key, value) => values.set(key, value)}),
|
|
reload: path => paths.push(path)
|
|
};
|
|
const error = new Error('Unable to preload CSS for /assets/Login-old.css');
|
|
const to = {name: 'login', path: '/login', fullPath: '/login', query: {redirect}};
|
|
const from = {name: 'list'};
|
|
const recover = createRouteAssetRecovery(options);
|
|
assert.equal(recover(error, to, from), true);
|
|
assert.equal(recover(error, to, from), true);
|
|
assert.deepEqual(paths, [`/login?${new URLSearchParams({redirect: expected})}`]);
|
|
assert.equal(createRouteAssetRecovery(options)(error, to, from), false);
|
|
}
|
|
});
|
|
|
|
test('登录资源恢复不刷新编辑表单、不恢复验证步骤或离线页面', () => {
|
|
const recover = createRouteAssetRecovery({
|
|
buildID: 'release-login',
|
|
getStorage: () => assert.fail('不应访问存储'),
|
|
reload: () => assert.fail('不应刷新')
|
|
});
|
|
const error = new Error('Unable to preload CSS for /assets/Login-old.css');
|
|
const to = {name: 'login', path: '/login', fullPath: '/login', query: {redirect: '/list'}};
|
|
assert.equal(recover(error, to, {name: 'editer'}), false);
|
|
assert.equal(recover(error, to, {name: 'login'}), false);
|
|
assert.equal(recover(error, to, {name: 'list'}, {editing: true}), false);
|
|
assert.equal(recover(error, to, {name: 'list'}, {online: false}), false);
|
|
assert.equal(recover(error, {...to, name: 'login-verification'}, {name: 'list'}), false);
|
|
assert.equal(recover(error, {...to, path: '//example.com'}, {name: 'list'}), false);
|
|
});
|
|
|
|
test('编辑页、设置表单、离线和非法目标不会自动刷新', () => {
|
|
let storageCalls = 0;
|
|
const recover = createRouteAssetRecovery({
|
|
buildID: 'release-a',
|
|
getStorage: () => { storageCalls += 1; throw new Error('不应访问存储'); },
|
|
reload: () => assert.fail('不应刷新')
|
|
});
|
|
const error = new Error('Unable to preload CSS for /assets/old.css');
|
|
const to = {name: 'detail', fullPath: '/detail/21'};
|
|
for (const name of ['editer', 'setup', 'login', 'force-password-change', undefined]) {
|
|
assert.equal(recover(error, to, {name}), false);
|
|
}
|
|
assert.equal(recover(error, to, {name: 'list'}, {editing: true}), false);
|
|
assert.equal(recover(error, to, {name: 'list'}, {online: false}), false);
|
|
for (const fullPath of ['//example.com', '/%2fexample.com', 'https://example.com', '/login', '']) {
|
|
assert.equal(recover(error, {...to, fullPath}, {name: 'list'}), false);
|
|
}
|
|
assert.equal(recover(new Error('Network Error'), to, {name: 'list'}), false);
|
|
assert.equal(storageCalls, 0);
|
|
});
|
|
|
|
test('存储不可用或导航异常时交回手动处理', () => {
|
|
const error = new Error('Unable to preload CSS for /assets/old.css');
|
|
const to = {name: 'detail', fullPath: '/detail/21'};
|
|
for (const getStorage of [
|
|
() => { throw new Error('存储不可用'); },
|
|
() => ({getItem: () => null, setItem: () => { throw new Error('写入失败'); }})
|
|
]) {
|
|
const recover = createRouteAssetRecovery({buildID: 'release-a', getStorage, reload: () => assert.fail('不应刷新')});
|
|
assert.equal(recover(error, to, {name: 'list'}), false);
|
|
}
|
|
const recover = createRouteAssetRecovery({
|
|
buildID: 'release-a',
|
|
getStorage: () => ({getItem: () => null, setItem: () => {}}),
|
|
reload: () => { throw new Error('导航失败'); }
|
|
});
|
|
assert.equal(recover(error, to, {name: 'list'}), false);
|
|
});
|
|
|
|
test("已在登录页时保留原有安全重定向", () => {
|
|
assert.equal(getCurrentLoginRedirect({
|
|
name: "login",
|
|
fullPath: "/login?redirect=/list",
|
|
query: {redirect: "/list"}
|
|
}), "/list");
|
|
|
|
assert.equal(getCurrentLoginRedirect({
|
|
name: "login",
|
|
fullPath: "/login?redirect=/login",
|
|
query: {redirect: "/login"}
|
|
}), "/");
|
|
});
|
|
|
|
test("初始化跳转使用安全的站内重定向", () => {
|
|
assert.deepEqual(createSetupLocation("/setup?redirect=/list"), {
|
|
name: "setup",
|
|
query: {redirect: "/"}
|
|
});
|
|
assert.deepEqual(createSetupLocation("/list?tag=inbox"), {
|
|
name: "setup",
|
|
query: {redirect: "/list?tag=inbox"}
|
|
});
|
|
});
|
|
|
|
test("并发 403 只执行一次登录跳转且不会嵌套 redirect", async () => {
|
|
let replaceCount = 0;
|
|
let finishNavigation;
|
|
const router = {
|
|
currentRoute: {
|
|
value: {
|
|
name: "list",
|
|
fullPath: "/list?tag=inbox",
|
|
query: {}
|
|
}
|
|
},
|
|
replace(location) {
|
|
replaceCount += 1;
|
|
return new Promise((resolve) => {
|
|
finishNavigation = () => {
|
|
router.currentRoute.value = {
|
|
name: "login",
|
|
fullPath: "/login?redirect=/list?tag=inbox",
|
|
query: location.query
|
|
};
|
|
resolve();
|
|
};
|
|
});
|
|
}
|
|
};
|
|
|
|
const redirectToLogin = createLoginRedirectHandler(router);
|
|
const first = redirectToLogin();
|
|
const second = redirectToLogin();
|
|
|
|
assert.strictEqual(first, second);
|
|
assert.equal(replaceCount, 1);
|
|
finishNavigation();
|
|
assert.equal(await first, true);
|
|
assert.equal(router.currentRoute.value.query.redirect, "/list?tag=inbox");
|
|
|
|
assert.equal(await redirectToLogin(), false);
|
|
assert.equal(replaceCount, 1);
|
|
assert.equal(router.currentRoute.value.query.redirect, "/list?tag=inbox");
|
|
});
|