init
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
// tests/crypto-store.test.js
|
||||
// 测试 crypto-store.js:主口令派生、AES-GCM 加解密、设置/解锁/锁定
|
||||
// 运行:node --test tests/crypto-store.test.js
|
||||
|
||||
const { describe, it, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
function createMemoryStorage() {
|
||||
const local = {};
|
||||
const session = {};
|
||||
const makeArea = (store) => ({
|
||||
async get(keys) {
|
||||
const result = {};
|
||||
if (Array.isArray(keys)) keys.forEach((k) => { if (k in store) result[k] = store[k]; });
|
||||
else if (typeof keys === "string") { if (keys in store) result[keys] = store[keys]; }
|
||||
else Object.assign(result, store);
|
||||
return result;
|
||||
},
|
||||
async set(items) { Object.assign(store, items); },
|
||||
async remove(keys) {
|
||||
if (Array.isArray(keys)) keys.forEach((k) => delete store[k]);
|
||||
else delete store[keys];
|
||||
},
|
||||
});
|
||||
return { local: makeArea(local), session: makeArea(session), localStore: local, sessionStore: session };
|
||||
}
|
||||
|
||||
function loadCryptoStore(storage) {
|
||||
global.chrome = { storage };
|
||||
delete require.cache[require.resolve("../crypto-store.js")];
|
||||
return require("../crypto-store.js");
|
||||
}
|
||||
|
||||
describe("crypto-store - 基础加解密", () => {
|
||||
it("encryptJson + decryptJson 往返一致", async () => {
|
||||
const storage = createMemoryStorage();
|
||||
const C = loadCryptoStore(storage);
|
||||
const salt = C.randomBytes(16);
|
||||
const key = await C.deriveKey("mypassword", salt, C.PBKDF2_ITERATIONS);
|
||||
const data = [{ id: "e1", name: "个人", configs: [{ domain: "a.com" }] }];
|
||||
const cipher = await C.encryptJson(data, key);
|
||||
const decrypted = await C.decryptJson(cipher, key);
|
||||
assert.deepEqual(decrypted, data);
|
||||
});
|
||||
|
||||
it("不同口令派生的密钥无法解密对方数据", async () => {
|
||||
const storage = createMemoryStorage();
|
||||
const C = loadCryptoStore(storage);
|
||||
const salt = C.randomBytes(16);
|
||||
const key1 = await C.deriveKey("password1", salt, C.PBKDF2_ITERATIONS);
|
||||
const key2 = await C.deriveKey("password2", salt, C.PBKDF2_ITERATIONS);
|
||||
const cipher = await C.encryptJson({ secret: "hello" }, key1);
|
||||
await assert.rejects(() => C.decryptJson(cipher, key2));
|
||||
});
|
||||
|
||||
it("b64encode / b64decode 往返一致", () => {
|
||||
const C = loadCryptoStore(createMemoryStorage());
|
||||
const buf = new Uint8Array([0, 1, 2, 253, 254, 255]);
|
||||
const encoded = C.b64encode(buf);
|
||||
const decoded = C.b64decode(encoded);
|
||||
assert.deepEqual(Array.from(decoded), Array.from(buf));
|
||||
});
|
||||
});
|
||||
|
||||
describe("crypto-store - 设置/解锁/锁定", () => {
|
||||
let C, storage;
|
||||
|
||||
beforeEach(() => {
|
||||
storage = createMemoryStorage();
|
||||
C = loadCryptoStore(storage);
|
||||
});
|
||||
|
||||
it("hasVault 初始为 false", async () => {
|
||||
assert.equal(await C.hasVault(), false);
|
||||
assert.equal(await C.isUnlocked(), false);
|
||||
});
|
||||
|
||||
it("setupVault 加密并解锁", async () => {
|
||||
const envs = [{ id: "e1", name: "个人", configs: [] }];
|
||||
const key = await C.setupVault("mypass123", envs);
|
||||
assert.ok(key);
|
||||
assert.equal(await C.hasVault(), true);
|
||||
assert.equal(await C.isUnlocked(), true);
|
||||
// local 中是密文,不是明文
|
||||
assert.ok(storage.localStore.vaultMeta);
|
||||
assert.ok(storage.localStore.encryptedVault);
|
||||
assert.equal(storage.localStore.environments, undefined);
|
||||
// session 中是明文
|
||||
assert.ok(storage.sessionStore.environments);
|
||||
});
|
||||
|
||||
it("setupVault 口令不能为空", async () => {
|
||||
await assert.rejects(() => C.setupVault("", []), /口令不能为空/);
|
||||
});
|
||||
|
||||
it("unlock 正确口令可解密", async () => {
|
||||
await C.setupVault("correct horse", [{ id: "e1", name: "x", configs: [] }]);
|
||||
// 先锁定
|
||||
await C.lock();
|
||||
assert.equal(await C.isUnlocked(), false);
|
||||
|
||||
const { environments, key } = await C.unlock("correct horse");
|
||||
assert.ok(key);
|
||||
assert.equal(environments.length, 1);
|
||||
assert.equal(environments[0].name, "x");
|
||||
assert.equal(await C.isUnlocked(), true);
|
||||
});
|
||||
|
||||
it("unlock 错误口令抛错", async () => {
|
||||
await C.setupVault("correct", []);
|
||||
await C.lock();
|
||||
await assert.rejects(() => C.unlock("wrong"), /口令错误/);
|
||||
// 错误口令后仍未解锁
|
||||
assert.equal(await C.isUnlocked(), false);
|
||||
});
|
||||
|
||||
it("lock 清除 session 明文但保留 local 密文", async () => {
|
||||
await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]);
|
||||
assert.ok(storage.sessionStore.environments);
|
||||
await C.lock();
|
||||
assert.equal(storage.sessionStore.environments, undefined);
|
||||
// 密文还在
|
||||
assert.ok(storage.localStore.encryptedVault);
|
||||
});
|
||||
|
||||
it("unlock 未设置口令时抛错", async () => {
|
||||
await assert.rejects(() => C.unlock("anything"), /尚未设置口令/);
|
||||
});
|
||||
|
||||
it("lock 后可再次 unlock", async () => {
|
||||
await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]);
|
||||
await C.lock();
|
||||
await C.unlock("pw");
|
||||
assert.equal(await C.isUnlocked(), true);
|
||||
const envs = (await C.unlock("pw")).environments;
|
||||
assert.equal(envs[0].name, "x");
|
||||
});
|
||||
});
|
||||
|
||||
describe("crypto-store - persistEncrypted", () => {
|
||||
it("用密钥重新加密 environments 写回 local", async () => {
|
||||
const storage = createMemoryStorage();
|
||||
const C = loadCryptoStore(storage);
|
||||
const key = await C.setupVault("pw", [{ id: "e1", name: "x", configs: [] }]);
|
||||
// 修改环境
|
||||
const newEnvs = [{ id: "e1", name: "x", configs: [{ domain: "a.com" }] }];
|
||||
await C.persistEncrypted(newEnvs, key);
|
||||
// 锁定后重新解锁,验证持久化生效
|
||||
await C.lock();
|
||||
const { environments } = await C.unlock("pw");
|
||||
assert.equal(environments[0].configs.length, 1);
|
||||
assert.equal(environments[0].configs[0].domain, "a.com");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
// tests/detect-captcha.test.js
|
||||
// 测试 content.js 中的 detectCaptcha:各类验证码场景识别
|
||||
// 通过 vm 在沙箱中加载 content.js,注入 mock document
|
||||
// 运行:node --test tests/detect-captcha.test.js
|
||||
|
||||
const { describe, it, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const vm = require("node:vm");
|
||||
|
||||
// 创建一个最小化的 mock document,支持 querySelector / querySelectorAll
|
||||
// elements 是一个选择器 -> 元素对象的映射;querySelector 返回第一个匹配
|
||||
function createMockDoc(elements) {
|
||||
return {
|
||||
querySelector(selector) {
|
||||
// 简化:把逗号分隔的选择器拆开,返回第一个命中的
|
||||
const parts = selector.split(",").map((s) => s.trim());
|
||||
for (const sel of parts) {
|
||||
if (elements[sel]) return elements[sel];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
const parts = selector.split(",").map((s) => s.trim());
|
||||
const result = [];
|
||||
for (const sel of parts) {
|
||||
if (elements[sel]) result.push(elements[sel]);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
body: {},
|
||||
};
|
||||
}
|
||||
|
||||
// 加载 content.js 到 vm 沙箱,返回沙箱上下文
|
||||
function loadContentJs(mockDoc) {
|
||||
const code = fs.readFileSync(path.join(__dirname, "..", "content.js"), "utf8");
|
||||
const sandbox = {
|
||||
URL: globalThis.URL,
|
||||
window: { location: { href: "https://example.com/login" } },
|
||||
document: mockDoc,
|
||||
chrome: {
|
||||
runtime: { onMessage: { addListener() {} } },
|
||||
storage: {
|
||||
local: { get: async () => ({}), set: async () => {}, remove: async () => {} },
|
||||
session: { get: async () => ({}), set: async () => {}, remove: async () => {} },
|
||||
},
|
||||
},
|
||||
MutationObserver: class {
|
||||
observe() {}
|
||||
},
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
console,
|
||||
};
|
||||
// 让 utils.js 提供的 isDomainMatch / escapeHtml 在沙箱中可用
|
||||
sandbox.globalThis = sandbox;
|
||||
vm.createContext(sandbox);
|
||||
// 先加载 utils.js
|
||||
const utilsCode = fs.readFileSync(path.join(__dirname, "..", "utils.js"), "utf8");
|
||||
vm.runInContext(utilsCode, sandbox);
|
||||
vm.runInContext(code, sandbox);
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
describe("detectCaptcha", () => {
|
||||
let sandbox;
|
||||
|
||||
beforeEach(() => {
|
||||
// 默认 mock:无任何验证码元素
|
||||
const doc = createMockDoc({});
|
||||
sandbox = loadContentJs(doc);
|
||||
});
|
||||
|
||||
it("无验证码元素时返回 null", () => {
|
||||
assert.equal(sandbox.detectCaptcha(), null);
|
||||
});
|
||||
|
||||
it("检测到 reCAPTCHA(.g-recaptcha)", () => {
|
||||
const doc = createMockDoc({
|
||||
".g-recaptcha": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.ok(r);
|
||||
assert.equal(r.type, "reCAPTCHA");
|
||||
assert.match(r.reason, /reCAPTCHA/);
|
||||
});
|
||||
|
||||
it("检测到 reCAPTCHA iframe", () => {
|
||||
const doc = createMockDoc({
|
||||
"iframe[src*='recaptcha']": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "reCAPTCHA");
|
||||
});
|
||||
|
||||
it("检测到 hCaptcha", () => {
|
||||
const doc = createMockDoc({
|
||||
".h-captcha": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "hCaptcha");
|
||||
});
|
||||
|
||||
it("检测到滑块验证码(#slider)", () => {
|
||||
const doc = createMockDoc({
|
||||
"#slider": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "slider/captcha");
|
||||
});
|
||||
|
||||
it("检测到图形验证码容器(.captcha)", () => {
|
||||
const doc = createMockDoc({
|
||||
".captcha": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "slider/captcha");
|
||||
});
|
||||
|
||||
it("检测到 geetest 极验验证码", () => {
|
||||
const doc = createMockDoc({
|
||||
".geetest": { offsetParent: {} },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "slider/captcha");
|
||||
});
|
||||
|
||||
it("检测到验证码输入框(name=captcha)", () => {
|
||||
const doc = createMockDoc({
|
||||
"input[name*='captcha' i]": { offsetParent: {}, type: "text" },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "captcha-input");
|
||||
});
|
||||
|
||||
it("检测到 placeholder 含「验证码」的输入框", () => {
|
||||
const doc = createMockDoc({
|
||||
"input[placeholder*='验证码' i]": { offsetParent: {}, type: "text" },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "captcha-input");
|
||||
});
|
||||
|
||||
it("hidden 类型的验证码输入框不被识别(忽略隐藏域)", () => {
|
||||
const doc = createMockDoc({
|
||||
"input[name*='captcha' i]": { offsetParent: {}, type: "hidden" },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("offsetParent 为 null 的滑块元素不可见,不被识别", () => {
|
||||
// 滑块/输入框分支会检查 offsetParent !== null,不可见元素不触发
|
||||
const doc = createMockDoc({
|
||||
"#slider": { offsetParent: null },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r, null);
|
||||
});
|
||||
|
||||
it("reCAPTCHA 优先级高于滑块/输入框", () => {
|
||||
const doc = createMockDoc({
|
||||
".g-recaptcha": { offsetParent: {} },
|
||||
"#slider": { offsetParent: {} },
|
||||
"input[name*='captcha' i]": { offsetParent: {}, type: "text" },
|
||||
});
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.equal(r.type, "reCAPTCHA");
|
||||
});
|
||||
|
||||
it("返回对象包含 reason 字段说明原因", () => {
|
||||
const doc = createMockDoc({ "#captcha": { offsetParent: {} } });
|
||||
const sb = loadContentJs(doc);
|
||||
const r = sb.detectCaptcha();
|
||||
assert.ok(r && typeof r.reason === "string" && r.reason.length > 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
// tests/env-store.test.js
|
||||
// 测试 env-store.js:环境 CRUD(数据存于 chrome.storage.session)
|
||||
// 运行:node --test tests/env-store.test.js
|
||||
|
||||
const { describe, it, beforeEach } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
// 内存版 storage,同时模拟 local 和 session
|
||||
function createMemoryStorage() {
|
||||
const local = {};
|
||||
const session = {};
|
||||
const makeArea = (store) => ({
|
||||
async get(keys) {
|
||||
const result = {};
|
||||
if (Array.isArray(keys)) {
|
||||
keys.forEach((k) => { if (k in store) result[k] = store[k]; });
|
||||
} else if (typeof keys === "string") {
|
||||
if (keys in store) result[keys] = store[keys];
|
||||
} else {
|
||||
Object.assign(result, store);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
async set(items) { Object.assign(store, items); },
|
||||
async remove(keys) {
|
||||
if (Array.isArray(keys)) keys.forEach((k) => delete store[k]);
|
||||
else delete store[keys];
|
||||
},
|
||||
_dump() { return store; },
|
||||
});
|
||||
return { local: makeArea(local), session: makeArea(session), localStore: local, sessionStore: session };
|
||||
}
|
||||
|
||||
function loadEnvStore(storage) {
|
||||
global.chrome = { storage };
|
||||
delete require.cache[require.resolve("../env-store.js")];
|
||||
return require("../env-store.js");
|
||||
}
|
||||
|
||||
describe("env-store - 读写 session", () => {
|
||||
it("未解锁(session 空)时返回空环境", async () => {
|
||||
const storage = createMemoryStorage();
|
||||
const EnvStore = loadEnvStore(storage);
|
||||
const { environments, activeEnvId } = await EnvStore.loadEnvironments();
|
||||
assert.deepEqual(environments, []);
|
||||
assert.equal(activeEnvId, null);
|
||||
});
|
||||
|
||||
it("saveEnvironments / loadEnvironments 读写 session", async () => {
|
||||
const storage = createMemoryStorage();
|
||||
const EnvStore = loadEnvStore(storage);
|
||||
const envs = [{ id: "e1", name: "个人", configs: [] }];
|
||||
await EnvStore.saveEnvironments(envs, "e1");
|
||||
const loaded = await EnvStore.loadEnvironments();
|
||||
assert.equal(loaded.environments.length, 1);
|
||||
assert.equal(loaded.environments[0].name, "个人");
|
||||
assert.equal(loaded.activeEnvId, "e1");
|
||||
// 数据落在 session,不在 local
|
||||
assert.ok(storage.sessionStore.environments);
|
||||
assert.equal(storage.localStore.environments, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("env-store - 环境 CRUD", () => {
|
||||
let EnvStore, storage;
|
||||
|
||||
beforeEach(async () => {
|
||||
storage = createMemoryStorage();
|
||||
EnvStore = loadEnvStore(storage);
|
||||
await EnvStore.saveEnvironments(
|
||||
[
|
||||
{ id: "e1", name: "个人", configs: [] },
|
||||
{ id: "e2", name: "测试", configs: [] },
|
||||
{ id: "e3", name: "生产", configs: [] },
|
||||
],
|
||||
"e1"
|
||||
);
|
||||
});
|
||||
|
||||
it("createEnvironment 新增并激活", async () => {
|
||||
const env = await EnvStore.createEnvironment("客户A");
|
||||
assert.equal(env.name, "客户A");
|
||||
const { environments, activeEnvId } = await EnvStore.loadEnvironments();
|
||||
assert.equal(environments.length, 4);
|
||||
assert.equal(activeEnvId, env.id);
|
||||
});
|
||||
|
||||
it("renameEnvironment", async () => {
|
||||
await EnvStore.renameEnvironment("e1", "个人-改");
|
||||
const { environments } = await EnvStore.loadEnvironments();
|
||||
assert.equal(environments[0].name, "个人-改");
|
||||
});
|
||||
|
||||
it("deleteEnvironment 非当前环境,active 不变", async () => {
|
||||
await EnvStore.deleteEnvironment("e2");
|
||||
const { environments, activeEnvId } = await EnvStore.loadEnvironments();
|
||||
assert.equal(environments.length, 2);
|
||||
assert.equal(activeEnvId, "e1");
|
||||
});
|
||||
|
||||
it("deleteEnvironment 当前环境,自动切到第一个", async () => {
|
||||
await EnvStore.deleteEnvironment("e1");
|
||||
const { environments, activeEnvId } = await EnvStore.loadEnvironments();
|
||||
assert.equal(environments.length, 2);
|
||||
assert.equal(activeEnvId, environments[0].id);
|
||||
});
|
||||
|
||||
it("deleteEnvironment 只剩一个时抛错", async () => {
|
||||
await EnvStore.deleteEnvironment("e1");
|
||||
await EnvStore.deleteEnvironment("e2");
|
||||
const { environments } = await EnvStore.loadEnvironments();
|
||||
assert.equal(environments.length, 1);
|
||||
await assert.rejects(() => EnvStore.deleteEnvironment(environments[0].id), /至少保留一个环境/);
|
||||
});
|
||||
|
||||
it("setActiveEnv + getActiveEnv", async () => {
|
||||
await EnvStore.setActiveEnv("e3");
|
||||
const env = await EnvStore.getActiveEnv();
|
||||
assert.equal(env.id, "e3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("env-store - 配置 CRUD", () => {
|
||||
let EnvStore, storage;
|
||||
|
||||
beforeEach(async () => {
|
||||
storage = createMemoryStorage();
|
||||
EnvStore = loadEnvStore(storage);
|
||||
await EnvStore.saveEnvironments(
|
||||
[
|
||||
{ id: "e1", name: "个人", configs: [] },
|
||||
{ id: "e2", name: "测试", configs: [] },
|
||||
],
|
||||
"e1"
|
||||
);
|
||||
});
|
||||
|
||||
it("saveConfigs / loadConfigs 读写当前环境", async () => {
|
||||
const cfg = { id: "c1", domain: "a.com", username: "u", password: "p" };
|
||||
await EnvStore.saveConfigs([cfg]);
|
||||
const configs = await EnvStore.loadConfigs();
|
||||
assert.equal(configs.length, 1);
|
||||
assert.equal(configs[0].domain, "a.com");
|
||||
});
|
||||
|
||||
it("loadAllConfigs 扁平化并附带 envId/envName", async () => {
|
||||
await EnvStore.saveConfigs([{ id: "c1", domain: "a.com", username: "u1", password: "p1" }]);
|
||||
await EnvStore.setActiveEnv("e2");
|
||||
await EnvStore.saveConfigs([{ id: "c2", domain: "b.com", username: "u2", password: "p2" }]);
|
||||
|
||||
const all = await EnvStore.loadAllConfigs();
|
||||
assert.equal(all.length, 2);
|
||||
assert.ok(all.every((c) => c.envId && c.envName));
|
||||
assert.deepEqual(all.map((c) => c.domain).sort(), ["a.com", "b.com"]);
|
||||
});
|
||||
|
||||
it("deleteEnvironment 清理 local 中该环境的失败计数", async () => {
|
||||
// 失败计数存于 local
|
||||
await storage.local.set({
|
||||
loginAttempts: {
|
||||
"e2::a.com": { count: 3, lastAttempt: Date.now() },
|
||||
"other::b.com": { count: 1, lastAttempt: Date.now() },
|
||||
},
|
||||
});
|
||||
await EnvStore.deleteEnvironment("e2");
|
||||
const attempts = (await storage.local.get("loginAttempts")).loginAttempts;
|
||||
assert.equal(attempts["e2::a.com"], undefined);
|
||||
assert.ok(attempts["other::b.com"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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 } = require("../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("escapeHtml", () => {
|
||||
it("转义 5 个特殊字符", () => {
|
||||
assert.equal(escapeHtml("<"), "<");
|
||||
assert.equal(escapeHtml(">"), ">");
|
||||
assert.equal(escapeHtml('"'), """);
|
||||
assert.equal(escapeHtml("'"), "'");
|
||||
assert.equal(escapeHtml("&"), "&");
|
||||
});
|
||||
|
||||
it("组合转义", () => {
|
||||
assert.equal(
|
||||
escapeHtml('<a href="x" onclick="alert(\'hi\')">text & more</a>'),
|
||||
"<a href="x" onclick="alert('hi')">text & more</a>"
|
||||
);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user