安全与权限: - 站点访问权限改为 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 同步
150 lines
5.0 KiB
JavaScript
150 lines
5.0 KiB
JavaScript
// tests/permissions.test.js
|
||
// 测试 permissions.js:可选主机权限的授权状态判断与申请逻辑
|
||
// 运行:node --test tests/permissions.test.js
|
||
|
||
const { describe, it, beforeEach } = require("node:test");
|
||
const assert = require("node:assert/strict");
|
||
|
||
const { toMatchPatterns } = require("../dist/utils.js");
|
||
|
||
/** 构造一个假的 chrome.permissions,granted 集合可观察 */
|
||
function createFakePermissions(granted = []) {
|
||
const state = {
|
||
granted: new Set(granted),
|
||
requestCalls: [],
|
||
containsCalls: 0,
|
||
throwOnContains: false,
|
||
};
|
||
return {
|
||
state,
|
||
api: {
|
||
contains: async ({ origins }) => {
|
||
state.containsCalls++;
|
||
if (state.throwOnContains) throw new Error("boom");
|
||
return origins.every((o) => state.granted.has(o));
|
||
},
|
||
request: async ({ origins }) => {
|
||
state.requestCalls.push(origins);
|
||
origins.forEach((o) => state.granted.add(o));
|
||
return true;
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function loadPerms(permissionsApi) {
|
||
global.toMatchPatterns = toMatchPatterns; // permissions.js 会延迟读取该全局函数
|
||
global.chrome = permissionsApi === null ? {} : { permissions: permissionsApi };
|
||
delete require.cache[require.resolve("../dist/permissions.js")];
|
||
return require("../dist/permissions.js");
|
||
}
|
||
|
||
describe("permissions - patternsForDomain", () => {
|
||
beforeEach(() => {
|
||
global.toMatchPatterns = toMatchPatterns;
|
||
});
|
||
|
||
it("返回 http / https 两条模式", () => {
|
||
const P = loadPerms(createFakePermissions().api);
|
||
assert.deepEqual(P.patternsForDomain("example.com"), ["http://example.com/*", "https://example.com/*"]);
|
||
});
|
||
|
||
it("通配符域名同样可转成可申请的 origin", () => {
|
||
const P = loadPerms(createFakePermissions().api);
|
||
assert.deepEqual(P.patternsForDomain("*.example.com"), [
|
||
"http://*.example.com/*",
|
||
"https://*.example.com/*",
|
||
]);
|
||
});
|
||
|
||
it("无法表达的规则返回空数组", () => {
|
||
const P = loadPerms(createFakePermissions().api);
|
||
assert.deepEqual(P.patternsForDomain("*"), []);
|
||
assert.deepEqual(P.patternsForDomain(""), []);
|
||
});
|
||
});
|
||
|
||
describe("permissions - partitionPatterns", () => {
|
||
it("按已授权拆分成 granted / missing", async () => {
|
||
const fake = createFakePermissions(["https://a.com/*"]);
|
||
const P = loadPerms(fake.api);
|
||
|
||
const result = await P.partitionPatterns(["https://a.com/*", "http://b.com/*", "https://b.com/*"]);
|
||
|
||
assert.deepEqual(result.granted, ["https://a.com/*"]);
|
||
assert.deepEqual(result.missing, ["http://b.com/*", "https://b.com/*"]);
|
||
});
|
||
|
||
it("contains 抛错时按未授权处理", async () => {
|
||
const fake = createFakePermissions(["https://a.com/*"]);
|
||
fake.state.throwOnContains = true;
|
||
const P = loadPerms(fake.api);
|
||
|
||
const result = await P.partitionPatterns(["https://a.com/*"]);
|
||
assert.deepEqual(result.granted, []);
|
||
assert.deepEqual(result.missing, ["https://a.com/*"]);
|
||
});
|
||
});
|
||
|
||
describe("permissions - partitionDomains", () => {
|
||
it("任一 scheme 已授权即算该域名已授权", async () => {
|
||
const fake = createFakePermissions(["https://a.com/*"]);
|
||
const P = loadPerms(fake.api);
|
||
|
||
const result = await P.partitionDomains(["a.com", "b.com"]);
|
||
assert.deepEqual(result.granted, ["a.com"]);
|
||
assert.deepEqual(result.missing, ["b.com"]);
|
||
});
|
||
|
||
it("完全未授权的域名进入 missing", async () => {
|
||
const P = loadPerms(createFakePermissions().api);
|
||
const result = await P.partitionDomains(["a.com"]);
|
||
assert.deepEqual(result.missing, ["a.com"]);
|
||
});
|
||
|
||
it("无法转成模式的域名视为未授权", async () => {
|
||
const P = loadPerms(createFakePermissions().api);
|
||
const result = await P.partitionDomains(["*"]);
|
||
assert.deepEqual(result.missing, ["*"]);
|
||
});
|
||
});
|
||
|
||
describe("permissions - requestForDomain", () => {
|
||
it("已授权时直接返回 true 且不重复申请", async () => {
|
||
const fake = createFakePermissions(["http://a.com/*", "https://a.com/*"]);
|
||
const P = loadPerms(fake.api);
|
||
|
||
assert.equal(await P.requestForDomain("a.com"), true);
|
||
assert.equal(fake.state.requestCalls.length, 0, "已授权不应再次弹框");
|
||
});
|
||
|
||
it("未授权时申请全部模式", async () => {
|
||
const fake = createFakePermissions();
|
||
const P = loadPerms(fake.api);
|
||
|
||
assert.equal(await P.requestForDomain("a.com"), true);
|
||
assert.deepEqual(fake.state.requestCalls, [["http://a.com/*", "https://a.com/*"]]);
|
||
});
|
||
|
||
it("无法转换的域名不会发起申请", async () => {
|
||
const fake = createFakePermissions();
|
||
const P = loadPerms(fake.api);
|
||
|
||
assert.equal(await P.requestForDomain("*"), false);
|
||
assert.equal(fake.state.requestCalls.length, 0);
|
||
});
|
||
|
||
it("环境没有 chrome.permissions 时返回 false", async () => {
|
||
const P = loadPerms(null);
|
||
assert.equal(await P.requestForDomain("a.com"), false);
|
||
});
|
||
|
||
it("申请被拒绝时返回 false(不抛错)", async () => {
|
||
const fake = createFakePermissions();
|
||
fake.api.request = async () => false;
|
||
const P = loadPerms(fake.api);
|
||
|
||
assert.equal(await P.requestForDomain("a.com"), false);
|
||
});
|
||
});
|