Files
auto-login/tests/utils.test.js
T
陈银军 1b0c3c012c feat: 按域名授权、iframe 多帧填充、自动提交限流修正,并补齐工程规范与文档
安全与权限:
- 站点访问权限改为 optional_host_permissions,保存配置时逐域名授权;移除 host_permissions 与 tabs,安装提示不再出现全站数据访问与浏览记录
- 主口令不再写入 storage.session,改用 IndexedDB 中不可导出的 CryptoKey 句柄恢复解锁,锁定即丢弃
- content script 注入范围只注册已授权域名,并随权限变化即时收敛

自动登录:
- 支持 iframe 内的登录表单:脚本注入所有帧,逐帧按自身地址匹配配置
- 手动填充改为逐帧探测,定向发送到真正含密码框的帧
- 限流修正:只在真正触发提交后计数;判定登录成功后立即清零;达到上限时页面给出可见提示;重置判断只看启用中的配置
- SPA 重试改为 DOM 变更门控 + 退避,并单独监听已发现的 Shadow Root

工程化与文档:
- 引入 ESLint(扁平配置)与 Prettier,CI 增加 lint 与 format:check
- 信息类日志改为 debugLog(默认静默,chrome.storage.local.debugLog 开关)
- 测试 123 → 128 用例(新增权限、iframe、限流相关用例)
- README / PRIVACY / CHANGELOG 同步
2026-09-18 00:05:20 +08:00

248 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// tests/utils.test.js
// 测试 utils.js 中的纯函数:isDomainMatch(URL 路径匹配)和 escapeHtml(HTML 转义)
// 运行:node --test tests/utils.test.js
const { describe, it } = require("node:test");
const assert = require("node:assert/strict");
const { isDomainMatch, escapeHtml, toMatchPatterns, pickFrameWithForm } = require("../dist/utils.js");
describe("isDomainMatch - 纯 hostname 匹配", () => {
it("精确域名匹配", () => {
assert.equal(isDomainMatch("https://example.com/login", "example.com"), true);
assert.equal(isDomainMatch("https://example.com/", "example.com"), true);
assert.equal(isDomainMatch("https://other.com/", "example.com"), false);
});
it("子域名通配符 *.example.com", () => {
assert.equal(isDomainMatch("https://oa.example.com/", "*.example.com"), true);
assert.equal(isDomainMatch("https://a.b.example.com/", "*.example.com"), true);
assert.equal(isDomainMatch("https://example.com/", "*.example.com"), true); // 根域也匹配
assert.equal(isDomainMatch("https://notexample.com/", "*.example.com"), false);
});
it("任意位置通配符", () => {
assert.equal(isDomainMatch("https://api.dev.example.com/", "api.*.example.com"), true);
assert.equal(isDomainMatch("https://api.prod.example.com/", "api.*.example.com"), true);
assert.equal(isDomainMatch("https://web.dev.example.com/", "api.*.example.com"), false);
});
it("端口和协议不影响匹配", () => {
assert.equal(isDomainMatch("http://example.com:8080/path", "example.com"), true);
assert.equal(isDomainMatch("https://example.com:443/", "example.com"), true);
});
it("非法 URL 返回 false", () => {
assert.equal(isDomainMatch("not-a-url", "example.com"), false);
assert.equal(isDomainMatch("", "example.com"), false);
assert.equal(isDomainMatch("https://example.com/", ""), false);
});
});
describe("isDomainMatch - hostname + path 匹配", () => {
it("精确路径匹配", () => {
assert.equal(isDomainMatch("https://oa.com/login", "oa.com/login"), true);
assert.equal(isDomainMatch("https://oa.com/admin/login", "oa.com/login"), false);
assert.equal(isDomainMatch("https://oa.com/", "oa.com/login"), false);
});
it("路径前缀通配 login/*", () => {
assert.equal(isDomainMatch("https://oa.com/login", "oa.com/login/*"), true);
assert.equal(isDomainMatch("https://oa.com/login/step2", "oa.com/login/*"), true);
assert.equal(isDomainMatch("https://oa.com/login/sub/deep", "oa.com/login/*"), true);
assert.equal(isDomainMatch("https://oa.com/logins", "oa.com/login/*"), false);
assert.equal(isDomainMatch("https://oa.com/admin", "oa.com/login/*"), false);
});
it("路径全通配 *", () => {
assert.equal(isDomainMatch("https://oa.com/anything", "oa.com/*"), true);
assert.equal(isDomainMatch("https://oa.com/", "oa.com/*"), true);
});
it("仅 hostname + 空路径匹配该域名所有页面", () => {
assert.equal(isDomainMatch("https://oa.com/any/path", "oa.com/"), true);
assert.equal(isDomainMatch("https://oa.com/", "oa.com/"), true);
});
it("路径内任意位置通配", () => {
assert.equal(isDomainMatch("https://site.com/a/123/b", "site.com/a/*/b"), true);
assert.equal(isDomainMatch("https://site.com/a/xyz/b", "site.com/a/*/b"), true);
assert.equal(isDomainMatch("https://site.com/a/123/c", "site.com/a/*/b"), false);
});
it("path 匹配与子域名通配组合", () => {
assert.equal(isDomainMatch("https://api.dev.com/login", "*.dev.com/login"), true);
assert.equal(isDomainMatch("https://web.dev.com/admin", "*.dev.com/login"), false);
assert.equal(isDomainMatch("https://api.dev.com/login/step2", "*.dev.com/login/*"), true);
});
it("带 query 和 hash 的 URL 不影响 path 匹配", () => {
assert.equal(isDomainMatch("https://oa.com/login?next=/home", "oa.com/login"), true);
assert.equal(isDomainMatch("https://oa.com/login#section", "oa.com/login"), true);
});
});
describe("toMatchPatterns - 配置域名转 match pattern", () => {
it("纯域名同时生成 http / https 两条", () => {
assert.deepEqual(toMatchPatterns("example.com"), ["http://example.com/*", "https://example.com/*"]);
});
it("带协议前缀的配置会被剥离", () => {
assert.deepEqual(toMatchPatterns("https://example.com"), [
"http://example.com/*",
"https://example.com/*",
]);
});
it("子域名通配符 *.example.com 保持原样", () => {
assert.deepEqual(toMatchPatterns("*.example.com"), ["http://*.example.com/*", "https://*.example.com/*"]);
});
it("路径规则按前缀截断到第一个 *", () => {
assert.deepEqual(toMatchPatterns("oa.com/login/*"), ["http://oa.com/login/*", "https://oa.com/login/*"]);
// 中间通配无法表达,截断成前缀(放宽注入范围,精度仍由 isDomainMatch 把关)
assert.deepEqual(toMatchPatterns("site.com/a/*/b"), ["http://site.com/a/*", "https://site.com/a/*"]);
});
it("无通配的路径补上结尾 *", () => {
assert.deepEqual(toMatchPatterns("oa.com/admin"), ["http://oa.com/admin*", "https://oa.com/admin*"]);
});
it("去掉端口(match pattern 不支持端口)", () => {
assert.deepEqual(toMatchPatterns("example.com:8080/app"), [
"http://example.com/app*",
"https://example.com/app*",
]);
});
it("中文域名转成 punycode", () => {
assert.deepEqual(toMatchPatterns("中文域名.中国"), [
"http://xn--fiq06l2rdsvs.xn--fiqs8s/*",
"https://xn--fiq06l2rdsvs.xn--fiqs8s/*",
]);
});
it("query / hash 会被忽略", () => {
assert.deepEqual(toMatchPatterns("oa.com/login?next=/home#x"), [
"http://oa.com/login*",
"https://oa.com/login*",
]);
});
it("无法表达的规则返回空数组", () => {
assert.deepEqual(toMatchPatterns(""), []);
assert.deepEqual(toMatchPatterns(null), []);
assert.deepEqual(toMatchPatterns("*"), []); // 裸通配
assert.deepEqual(toMatchPatterns("a.*.b.com"), []); // 中间通配的 host
});
});
describe("pickFrameWithForm - 从多帧结果里挑出有表单的帧", () => {
it("只有一个帧有表单时返回该帧", () => {
assert.equal(
pickFrameWithForm([
{ frameId: 0, result: 0 },
{ frameId: 5, result: 1 },
]),
5,
);
});
it("顶层帧有表单时优先顶层帧", () => {
assert.equal(
pickFrameWithForm([
{ frameId: 0, result: 1 },
{ frameId: 3, result: 2 },
]),
0,
);
});
it("多个子帧都有表单时取 frameId 最小的", () => {
assert.equal(
pickFrameWithForm([
{ frameId: 7, result: 1 },
{ frameId: 3, result: 1 },
]),
3,
);
});
it("都没有表单时返回 null", () => {
assert.equal(
pickFrameWithForm([
{ frameId: 0, result: 0 },
{ frameId: 2, result: 0 },
]),
null,
);
assert.equal(pickFrameWithForm([]), null);
assert.equal(pickFrameWithForm(undefined), null);
});
it("缺少 frameId 时按顶层帧处理", () => {
assert.equal(pickFrameWithForm([{ result: 1 }]), 0);
});
});
describe("debugLog - 调试日志开关", () => {
it("默认静默,打开开关后才输出", async () => {
const logs = [];
const originalLog = console.log;
console.log = (...args) => logs.push(args.join(" "));
const loadUtils = () => {
delete require.cache[require.resolve("../dist/utils.js")];
return require("../dist/utils.js");
};
try {
delete global.chrome;
let U = loadUtils();
assert.equal(await U.initDebugLog(), false);
U.debugLog("默认不该出现");
assert.equal(logs.filter((l) => l.includes("默认不该出现")).length, 0);
global.chrome = { storage: { local: { get: async () => ({ debugLog: true }) } } };
U = loadUtils();
assert.equal(await U.initDebugLog(), true);
U.debugLog("开启后应出现");
assert.equal(logs.filter((l) => l.includes("开启后应出现")).length, 1);
} finally {
console.log = originalLog;
delete global.chrome;
loadUtils();
}
});
});
describe("escapeHtml", () => {
it("转义 5 个特殊字符", () => {
assert.equal(escapeHtml("<"), "&lt;");
assert.equal(escapeHtml(">"), "&gt;");
assert.equal(escapeHtml('"'), "&quot;");
assert.equal(escapeHtml("'"), "&#39;");
assert.equal(escapeHtml("&"), "&amp;");
});
it("组合转义", () => {
assert.equal(
escapeHtml('<a href="x" onclick="alert(\'hi\')">text & more</a>'),
"&lt;a href=&quot;x&quot; onclick=&quot;alert(&#39;hi&#39;)&quot;&gt;text &amp; more&lt;/a&gt;",
);
});
it("null 和 undefined 返回空串", () => {
assert.equal(escapeHtml(null), "");
assert.equal(escapeHtml(undefined), "");
});
it("数字会被转成字符串", () => {
assert.equal(escapeHtml(123), "123");
});
it("普通文本不变", () => {
assert.equal(escapeHtml("hello world"), "hello world");
assert.equal(escapeHtml("中文测试123"), "中文测试123");
});
});