feat: 支持自定义 Z-Library 镜像并发布 v2.1.5
在 Z-Library 账户设置内增加镜像站点编辑入口,一行一个 HTTPS 地址, 自定义地址经 origin 规范化后优先于内置列表。镜像配置与凭据分开处理, 退出登录后仍保留;移除当前会话依赖的镜像时清理旧会话和 Cookie。 版本更新到 2.1.5,并补充存储、校验、IPC、界面与真实 Electron 保存测试。
This commit is contained in:
@@ -135,6 +135,36 @@ test('customMirrors 往返不丢失', () => {
|
||||
assert.deepStrictEqual(auth.read().customMirrors, ['https://m1', 'https://m2']);
|
||||
});
|
||||
|
||||
test('setCustomMirrors 不重写密文,凭据与会话都保留', () => {
|
||||
const d = tmp();
|
||||
const auth = freshAuth();
|
||||
auth.init(d, fakeStorage());
|
||||
auth.write({ email: 'a@b.c', password: 'Keep', userId: '1', userKey: 'k', mirror: 'https://m1' });
|
||||
|
||||
auth.setCustomMirrors(['https://m1', 'https://m2']);
|
||||
assert.deepStrictEqual(auth.getCustomMirrors(), ['https://m1', 'https://m2']);
|
||||
assert.strictEqual(auth.read().password, 'Keep', '改镜像不该动凭据');
|
||||
assert.deepStrictEqual(auth.getSession(), { userId: '1', userKey: 'k', mirror: 'https://m1' });
|
||||
|
||||
auth.setCustomMirrors([]);
|
||||
assert.deepStrictEqual(auth.getCustomMirrors(), []);
|
||||
assert.strictEqual(auth.hasCreds(), true);
|
||||
});
|
||||
|
||||
test('退出登录清掉凭据但保留自定义镜像', () => {
|
||||
const d = tmp();
|
||||
const auth = freshAuth();
|
||||
auth.init(d, fakeStorage());
|
||||
auth.write({ email: 'a@b.c', password: 'p', userId: '1', userKey: 'k' });
|
||||
auth.setCustomMirrors(['https://mine.example']);
|
||||
|
||||
auth.clear();
|
||||
assert.strictEqual(auth.hasCreds(), false, '凭据未清除');
|
||||
assert.strictEqual(auth.getSession(), null, '会话未清除');
|
||||
// 默认域名可能已全部失效,退出登录顺带清掉镜像会让用户无从登录
|
||||
assert.deepStrictEqual(auth.getCustomMirrors(), ['https://mine.example']);
|
||||
});
|
||||
|
||||
test('损坏的密文不影响会话字段读取', () => {
|
||||
const d = tmp();
|
||||
const auth = freshAuth();
|
||||
|
||||
@@ -95,6 +95,29 @@ app.whenReady().then(async () => {
|
||||
proxyRoute);
|
||||
await win.webContents.executeJavaScript(`window.api.proxy.set('')`);
|
||||
|
||||
await win.webContents.executeJavaScript(`document.getElementById('zlibMirrorBtn').click()`);
|
||||
await waitUntil(() => win.webContents.executeJavaScript(
|
||||
`!!document.getElementById('zlibMirrorList')`
|
||||
));
|
||||
await win.webContents.executeJavaScript(`(() => {
|
||||
document.getElementById('zlibMirrorList').value =
|
||||
'https://mirror.example/path\\nhttps://second.example/';
|
||||
document.getElementById('modalOk').click();
|
||||
})()`);
|
||||
const mirrorConfig = await waitUntil(() => win.webContents.executeJavaScript(`(async () => {
|
||||
const result = await window.api.zlib.getMirrors();
|
||||
const status = document.getElementById('zlibMirrorStatus').textContent;
|
||||
return result.ok && result.data.custom.length === 2 && /已自定义 2 个镜像/.test(status)
|
||||
? { custom: result.data.custom, status }
|
||||
: null;
|
||||
})()`));
|
||||
check('Z-Library 账户设置可保存并优先使用自定义镜像',
|
||||
JSON.stringify(mirrorConfig.custom) === JSON.stringify([
|
||||
'https://mirror.example',
|
||||
'https://second.example'
|
||||
]),
|
||||
JSON.stringify(mirrorConfig));
|
||||
|
||||
const sourceRows = await waitUntil(() => win.webContents.executeJavaScript(`(() => {
|
||||
const rows = Array.from(document.querySelectorAll('#sourceList input[data-id]')).map((input) => ({
|
||||
id: input.dataset.id,
|
||||
|
||||
@@ -560,6 +560,70 @@ test('zlib: 完全无效的 id 仍然拒绝', async () => {
|
||||
await assert.rejects(zlib.detail('not-an-id'), /无效的 Z-Library ID/);
|
||||
});
|
||||
|
||||
test('zlib: 自定义镜像规范化、去重并优先于内置列表', () => {
|
||||
const zlib = h.freshRequire('sources/zlib.js');
|
||||
const auth = require('../sources/zlib-auth');
|
||||
const orig = {
|
||||
getCustomMirrors: auth.getCustomMirrors,
|
||||
setCustomMirrors: auth.setCustomMirrors,
|
||||
getSession: auth.getSession,
|
||||
clearSession: auth.clearSession
|
||||
};
|
||||
let custom = [];
|
||||
auth.getCustomMirrors = () => custom.slice();
|
||||
auth.setCustomMirrors = (list) => { custom = list.slice(); };
|
||||
auth.getSession = () => null;
|
||||
auth.clearSession = () => {};
|
||||
try {
|
||||
const result = zlib.setMirrors([
|
||||
'https://mirror.example/path?ignored=1',
|
||||
'https://mirror.example/',
|
||||
'https://second.example'
|
||||
]);
|
||||
assert.deepStrictEqual(result.custom, [
|
||||
'https://mirror.example',
|
||||
'https://second.example'
|
||||
]);
|
||||
assert.deepStrictEqual(zlib.getMirrorConfig().custom, result.custom);
|
||||
assert.ok(result.defaults.length >= 1, '内置镜像列表丢失');
|
||||
} finally {
|
||||
Object.assign(auth, orig);
|
||||
}
|
||||
});
|
||||
|
||||
test('zlib: 自定义镜像拒绝非 HTTPS、凭据与超量输入', () => {
|
||||
const zlib = h.freshRequire('sources/zlib.js');
|
||||
assert.throws(() => zlib.setMirrors(['http://mirror.example']), /必须使用 HTTPS/);
|
||||
assert.throws(() => zlib.setMirrors(['https://user:pass@mirror.example']), /不能包含账号信息/);
|
||||
assert.throws(
|
||||
() => zlib.setMirrors(Array.from({ length: 21 }, (_, i) => `https://m${i}.example`)),
|
||||
/最多只能保存 20 个镜像/
|
||||
);
|
||||
});
|
||||
|
||||
test('zlib: 移除当前会话的非内置镜像会清除旧会话', () => {
|
||||
const zlib = h.freshRequire('sources/zlib.js');
|
||||
const auth = require('../sources/zlib-auth');
|
||||
const orig = {
|
||||
getCustomMirrors: auth.getCustomMirrors,
|
||||
setCustomMirrors: auth.setCustomMirrors,
|
||||
getSession: auth.getSession,
|
||||
clearSession: auth.clearSession
|
||||
};
|
||||
let custom = ['https://old.example'];
|
||||
let cleared = 0;
|
||||
auth.getCustomMirrors = () => custom.slice();
|
||||
auth.setCustomMirrors = (list) => { custom = list.slice(); };
|
||||
auth.getSession = () => ({ userId: '1', userKey: 'k', mirror: 'https://old.example' });
|
||||
auth.clearSession = () => { cleared += 1; };
|
||||
try {
|
||||
zlib.setMirrors(['https://new.example']);
|
||||
assert.strictEqual(cleared, 1, '被移除镜像上的会话仍被保留');
|
||||
} finally {
|
||||
Object.assign(auth, orig);
|
||||
}
|
||||
});
|
||||
|
||||
// 实测:会话过期时 /file 返回 400 + {"success":0,"error":"Please login"}。
|
||||
// 若按 HTTP 状态码短路,真实原因会被吞掉,自动重登也不会触发。
|
||||
test('zlib: 4xx+JSON 的会话过期能被识别并自动重新登录', async () => {
|
||||
|
||||
@@ -240,6 +240,28 @@ test('Z-Library 进入详情不消耗下载额度,点击下载后才解析并
|
||||
assert.match(onDemand, /每日免费下载额度有限,仅在点击后获取下载地址/);
|
||||
});
|
||||
|
||||
test('Z-Library 账户设置提供可编辑的镜像站点列表', () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||
const app = fs.readFileSync(path.join(__dirname, '..', 'ui', 'app.js'), 'utf8');
|
||||
const preload = fs.readFileSync(path.join(__dirname, '..', '..', 'preload.js'), 'utf8');
|
||||
const main = fs.readFileSync(path.join(__dirname, '..', '..', 'main.js'), 'utf8');
|
||||
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');
|
||||
|
||||
const accountAt = html.indexOf('Z-Library 账号');
|
||||
const mirrorsAt = html.indexOf('id="zlibMirrorBtn"');
|
||||
const nextGroupAt = html.indexOf('<div class="settings-group">', accountAt + 30);
|
||||
assert.ok(accountAt >= 0 && mirrorsAt > accountAt && mirrorsAt < nextGroupAt,
|
||||
'镜像设置没有放在 Z-Library 账户设置组内');
|
||||
assert.match(app, /window\.api\.zlib\.getMirrors\(\)/);
|
||||
assert.match(app, /window\.api\.zlib\.setMirrors\(lines\)/);
|
||||
assert.match(app, /一行一个地址,必须是 HTTPS/);
|
||||
assert.match(preload, /getMirrors:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('zlib:getMirrors'\)/);
|
||||
assert.match(preload, /setMirrors:\s*\(list\)\s*=>\s*ipcRenderer\.invoke\('zlib:setMirrors', list\)/);
|
||||
assert.match(main, /ipcMain\.handle\('zlib:getMirrors',[^\n]*wrap\(/);
|
||||
assert.match(main, /ipcMain\.handle\('zlib:setMirrors',[^\n]*wrap\(/);
|
||||
assert.match(css, /textarea\.modal-input\s*\{[^}]*height:\s*auto/);
|
||||
});
|
||||
|
||||
test('漫画源按分类显示,MangaDex 可筛选中文章节并设置图片画质', () => {
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||
const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8');
|
||||
|
||||
Reference in New Issue
Block a user