Files
peoplelib/src/_test/sources.test.js
T
lofyer eaf9939436
构建与发布 / 发布 GitHub Release (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
构建与发布 / 单测与集成测试 (push) Waiting to run
构建与发布 / 打包 ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
feat: 缺失文件扫描支持清理并发布 v2.1.1
2026-08-06 17:57:45 +08:00

706 lines
28 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert');
const h = require('./helpers');
h.installFetchStub();
const sources = require('../sources');
test('注册表:每个源都实现完整接口', () => {
const list = sources.listSources();
assert.ok(list.length >= 16);
assert.strictEqual(new Set(list.map((source) => source.id)).size, list.length, '数据源 ID 不能重复');
for (const s of list) {
const m = sources.getSource(s.id);
for (const fn of ['list', 'search', 'detail', 'download']) {
assert.strictEqual(typeof m[fn], 'function', `${s.id}.${fn} 缺失`);
}
assert.ok(s.name, `${s.id} 缺 name`);
}
});
test('注册表:开放教材与中英文维基文库已启用', () => {
const ids = sources.listSources().map((source) => source.id);
for (const id of ['openstax', 'opentextbook', 'wikisource-zh', 'wikisource-en']) {
assert.ok(ids.includes(id), `缺少新数据源: ${id}`);
}
});
test('注册表:未知 id 抛错', () => {
assert.throws(() => sources.getSource('nope'), /未知数据源/);
});
// --- PMC ---
test('pmc: esearch 响应异常时给出可读错误而不是 TypeError', async () => {
h.setHandler(h.routes([['esearch.fcgi', { body: { error: 'down' } }]]));
await assert.rejects(sources.getSource('pmc').search('x', 1), /无法识别的检索结果/);
});
test('pmc: postId 不重复拼 PMC 前缀', async () => {
const seen = [];
h.setHandler(h.routes([
['esummary.fcgi', (u) => { seen.push(u); return h.makeResponse({ body: { result: { 123: { uid: '123', title: 'T', authors: [] } } } }); }]
]));
const d = await sources.getSource('pmc').detail('PMC123');
assert.strictEqual(d.postId, '123');
assert.strictEqual(d.url, 'https://www.ncbi.nlm.nih.gov/pmc/articles/PMC123/');
assert.ok(!d.url.includes('PMCPMC'), 'URL 里出现了 PMCPMC');
assert.ok(seen[0].includes('id=123'), 'esummary 用了带前缀的 id');
});
test('pmc: 畸形 id 不会把正则搞崩', async () => {
h.setHandler(() => h.makeResponse({ body: '' }));
await assert.rejects(sources.getSource('pmc').download('12(3'), /无效的 PMC ID/);
await assert.rejects(sources.getSource('pmc').download('.*'), /无效的 PMC ID/);
});
test('pmc: 列表按 uid 归一化 postId', async () => {
h.setHandler(h.routes([
['esearch.fcgi', { body: { esearchresult: { count: '40', idlist: ['777'] } } }],
['esummary.fcgi', { body: { result: { 777: { uid: '777', title: 'A', authors: [{ name: 'X' }], pubdate: '2020 Jan' } } } }]
]));
const r = await sources.getSource('pmc').search('kw', 1);
assert.strictEqual(r.items[0].postId, '777');
assert.strictEqual(r.maxPage, 2);
});
// --- DOAJ ---
test('doaj: postId 不被二次编码', async () => {
const urls = [];
h.setHandler(h.routes([
['search/articles', { body: { total: 1, results: [{ id: '10.1234/abc', bibjson: { title: 'T', author: [], link: [] } }] } }],
['api/v2/articles/', (u) => { urls.push(u); return h.makeResponse({ body: { bibjson: { title: 'T', author: [], link: [] } } }); }]
]));
const doaj = sources.getSource('doaj');
const r = await doaj.search('kw', 1);
assert.strictEqual(r.items[0].postId, '10.1234/abc', 'postId 不该预先编码');
await doaj.detail(r.items[0].postId);
assert.ok(urls[0].includes('10.1234%2Fabc'), '详情 URL 编码错误: ' + urls[0]);
assert.ok(!urls[0].includes('%252F'), '出现二次编码: ' + urls[0]);
});
test('doaj: DOAJ 页链接正确编码', async () => {
h.setHandler(h.routes([['api/v2/articles/', { body: { bibjson: { link: [] } } }]]));
const d = await sources.getSource('doaj').download('10.1234/abc');
const page = d.links.find((l) => l.name === 'DOAJ 页');
assert.strictEqual(page.url, 'https://doaj.org/article/10.1234%2Fabc');
});
// --- Sci-Hub ---
test('scihub: 跳过广告 iframe 找到真正的 PDF', async () => {
h.setHandler(() => h.makeResponse({
body: '<iframe src="https://ads.example/b.html"></iframe><iframe src="/downloads/2020/x.pdf"></iframe>'
}));
const d = await sources.getSource('scihub').download('10.1038/nature12373');
assert.strictEqual(d.files[0].link, 'https://sci-hub.se/downloads/2020/x.pdf');
});
test('scihub: DOI 不存在时只请求一个镜像', async () => {
const hits = [];
h.setHandler((u) => { hits.push(u); return h.makeResponse({ body: 'article not found' }); });
await assert.rejects(sources.getSource('scihub').detail('10.1/x'), /不存在/);
assert.strictEqual(hits.length, 1, `不该轮询全部镜像,实际请求 ${hits.length} 次`);
});
test('scihub: 非 DOI 关键词返回空而不抛错', async () => {
const r = await sources.getSource('scihub').search('随便搜点什么', 1);
assert.deepStrictEqual(r.items, []);
assert.ok(r.note);
});
// --- LibGen ---
test('libgen: maxPage 只看分页控件,忽略页脚干扰链接', async () => {
const card = '<div class="resItemBox" data-book_id="1"><h3 itemprop="name"><a>Book One</a></h3></div>';
const footer = '<div class="footer"><a href="/x?page=999">junk</a></div>';
const pager = '<div class="paginator"><a href="?page=2">2</a><a href="?page=3">3</a></div>';
h.setHandler(() => h.makeResponse({ body: card + footer + pager }));
const r = await sources.getSource('libgen').search('godel escher', 1);
assert.strictEqual(r.items.length, 1);
assert.strictEqual(r.maxPage, 3, '页脚的 page=999 被误算进来了');
});
test('libgen: 无分页控件时不虚报页数', async () => {
const card = '<div class="resItemBox" data-book_id="9"><h3 itemprop="name"><a>Solo</a></h3></div>';
h.setHandler(() => h.makeResponse({ body: card + '<a href="/y?page=42">junk</a>' }));
const r = await sources.getSource('libgen').search('solo book', 1);
assert.strictEqual(r.maxPage, 1);
});
test('libgen: JSON-LD image 为对象时详情不崩溃', async () => {
const ld = JSON.stringify({ '@type': 'Book', name: 'B', image: { '@type': 'ImageObject', url: '/c.jpg' } });
h.setHandler(() => h.makeResponse({
body: `<script type="application/ld+json">${ld}</script><h1 itemprop="name">B</h1>`
}));
const d = await sources.getSource('libgen').detail('web:5');
assert.strictEqual(d.title, 'B');
assert.ok(/\/c\.jpg$/.test(d.cover), 'cover 解析失败: ' + d.cover);
});
test('libgen: 关键词过短直接返回提示', async () => {
const r = await sources.getSource('libgen').search('ab', 1);
assert.deepStrictEqual(r.items, []);
assert.ok(r.note);
});
// --- Standard Ebooks ---
test('standardebooks: author 为字符串时不丢作者', async () => {
h.setHandler(h.routes([['feeds/opds/all', {
body: {
publications: [{
metadata: { identifier: 'https://standardebooks.org/ebooks/jane-austen/emma', title: 'Emma', author: 'Jane Austen' },
images: []
}]
}
}]]));
const r = await sources.getSource('standardebooks').search('emma', 1);
assert.strictEqual(r.items[0].subtitle, 'Jane Austen');
});
test('standardebooks: author 混排对象与字符串', async () => {
h.setHandler(h.routes([['feeds/opds/all', {
body: {
publications: [{
metadata: { identifier: 'https://standardebooks.org/ebooks/a/b', title: 'T', author: [{ name: 'A' }, 'B'] },
images: []
}]
}
}]]));
const r = await sources.getSource('standardebooks').search('t', 1);
assert.strictEqual(r.items[0].subtitle, 'A, B');
});
test('standardebooks: 非法 slug 被拒绝', async () => {
await assert.rejects(sources.getSource('standardebooks').detail('../../etc/passwd'), /无效的/);
});
// --- Open Library ---
test('openlibrary: 详情解析作者姓名', async () => {
h.setHandler(h.routes([
[/works\/OL1W\.json/, { body: { title: 'W', authors: [{ author: { key: '/authors/OL1A' } }], subjects: [] } }],
[/authors\/OL1A\.json/, { body: { name: 'Ursula Le Guin' } }]
]));
const d = await sources.getSource('openlibrary').detail('OL1W');
assert.deepStrictEqual(d.authors, ['Ursula Le Guin']);
});
test('openlibrary: 单个作者取不到不影响整体', async () => {
h.setHandler(h.routes([
[/works\/OL2W\.json/, { body: { title: 'W', authors: [{ author: { key: '/authors/BAD' } }, { author: { key: '/authors/OK' } }], subjects: [] } }],
// 404 不触发重试,避免这条用例白等两轮退避
[/authors\/BAD\.json/, { status: 404 }],
[/authors\/OK\.json/, { body: { name: 'Good' } }]
]));
const d = await sources.getSource('openlibrary').detail('OL2W');
assert.deepStrictEqual(d.authors, ['Good']);
});
test('openlibrary: 下载只给 archive.org 真实存在的文件', async () => {
h.setHandler(h.routes([
['editions.json', { body: { entries: [{ ocaid: 'someitem' }] } }],
['archive.org/metadata/', {
body: { files: [{ name: 'someitem.pdf', format: 'Text PDF' }, { name: 'thumb.jpg', format: 'JPEG' }] }
}]
]));
const d = await sources.getSource('openlibrary').download('OL3W');
assert.strictEqual(d.files.length, 1, '推了不存在的格式: ' + JSON.stringify(d.files));
assert.strictEqual(d.files[0].format, 'PDF');
assert.ok(d.files[0].link.includes('someitem.pdf'));
});
test('openlibrary: 借阅制条目被跳过', async () => {
h.setHandler(h.routes([
['editions.json', { body: { entries: [{ ocaid: 'lend', access_restricted: 'borrow' }] } }]
]));
const d = await sources.getSource('openlibrary').download('OL4W');
assert.deepStrictEqual(d.files, []);
});
// --- bioRxiv ---
test('biorxiv: 瞬时故障会重试而不是直接失败', async () => {
let n = 0;
h.setHandler(() => {
n++;
// 502 与超时走的是同一条 isRetryable 分支,用 502 避免真的等满超时
if (n <= 2) return h.makeResponse({ status: 502 });
return h.makeResponse({ body: { messages: [{ total: 100 }], collection: [] } });
});
const r = await sources.getSource('biorxiv').list(1);
assert.ok(n >= 3, `没有重试,只请求了 ${n} 次`);
assert.ok(r.maxPage >= 1);
});
test('biorxiv: 超时被判定为可重试(回归 504|502|503 正则漏判)', () => {
const { isRetryable } = require('../sources/http');
assert.strictEqual(isRetryable(new Error('请求超时,站点无响应')), true);
assert.strictEqual(isRetryable(new Error('网络连接失败,请检查网络或代理设置')), true);
const src = require('fs').readFileSync(require.resolve('../sources/biorxiv.js'), 'utf8');
assert.ok(!/504\|502\|503/.test(src), '旧的字符串匹配门仍在');
});
test('biorxiv: 不支持搜索时明确报错', async () => {
await assert.rejects(sources.getSource('biorxiv').search('x', 1), /不支持搜索/);
});
// --- MOTW ---
test('motw: 分页用 offset/limit 且随页码递增', async () => {
const urls = [];
h.setHandler((u) => {
urls.push(u);
return h.makeResponse({ body: { _items: [], _meta: { total: 1000, max_results: 48 } } });
});
const motw = sources.getSource('motw');
await motw.list(1);
await motw.list(3);
assert.ok(urls[0].includes('offset=0&limit=48'), urls[0]);
assert.ok(urls[1].includes('offset=96&limit=48'), urls[1]);
});
test('motw: 未缓存的详情给出可操作提示', async () => {
await assert.rejects(sources.getSource('motw').detail('unknown-id'), /重新进入/);
});
// --- arXiv ---
test('arxiv: 解析 atom feed 并取 pdf 链接', async () => {
const xml = `<feed><opensearch:totalResults>40</opensearch:totalResults>
<entry><id>http://arxiv.org/abs/2201.00978v1</id><title>Paper T</title>
<summary>S</summary><published>2022-01-03T00:00:00Z</published>
<author><name>A One</name></author>
<link title="pdf" href="https://arxiv.org/pdf/2201.00978v1"/>
<category term="cs.CV"/></entry></feed>`;
h.setHandler(() => h.makeResponse({ body: xml }));
const r = await sources.getSource('arxiv').search('transformer', 1);
assert.strictEqual(r.items[0].postId, '2201.00978v1');
assert.strictEqual(r.maxPage, 2);
const d = await sources.getSource('arxiv').download('2201.00978v1');
assert.strictEqual(d.files[0].link, 'https://arxiv.org/pdf/2201.00978v1');
});
// --- OpenStax ---
test('openstax: 只展示 live 教材并支持本地关键词分页', async () => {
const openstax = h.freshRequire('sources/openstax.js');
const books = Array.from({ length: 21 }, (_, i) => ({
id: i + 1,
slug: `books/book-${i + 1}`,
book_state: 'live',
title: i === 20 ? 'Advanced Calculus' : `Biology ${i + 1}`,
subjects: i === 20 ? ['Math'] : ['Science'],
subject_categories: []
}));
books.push({ id: 99, slug: 'books/draft', book_state: 'draft', title: 'Draft Calculus' });
h.setHandler(h.routes([['/apps/cms/api/books', { body: { books } }]]));
const page2 = await openstax.list(2);
assert.strictEqual(page2.items.length, 1);
assert.strictEqual(page2.maxPage, 2);
const found = await openstax.search('advanced math', 1);
assert.deepStrictEqual(found.items.map((item) => item.postId), ['21']);
assert.strictEqual(found.items[0].url, 'https://openstax.org/details/books/book-21');
});
test('openstax: 详情解析作者、许可和日期', async () => {
const openstax = h.freshRequire('sources/openstax.js');
h.setHandler(h.routes([['/apps/cms/api/v2/pages/76/', {
body: {
id: 76,
meta: { slug: 'calculus-volume-3', html_url: 'https://openstax.org/details/books/calculus-volume-3' },
title: 'Calculus Volume 3',
publish_date: '2016-03-30',
authors: [{ value: { name: 'Gilbert Strang' } }, { name: 'Second Author' }],
book_subjects: { subject_name: 'Math' },
book_categories: [{ subject_name: 'Calculus' }],
description: '<p>Open <b>calculus</b> textbook.</p>',
license_name: 'Creative Commons Attribution-NonCommercial-ShareAlike License',
license_version: '4.0',
digital_isbn_13: '978-1-947172-16-6'
}
}]]));
const detail = await openstax.detail('76');
assert.deepStrictEqual(detail.authors, ['Gilbert Strang', 'Second Author']);
assert.strictEqual(detail.date, '2016-03-30');
assert.strictEqual(detail.brief, 'Open calculus textbook.');
assert.ok(detail.tags.includes('主题:Math'));
assert.ok(detail.tags.some((tag) => tag.includes('4.0')));
});
test('openstax: PDF 去重且非法 id 不发请求', async () => {
const openstax = h.freshRequire('sources/openstax.js');
h.resetCalls();
h.setHandler(h.routes([['/apps/cms/api/v2/pages/76/', {
body: {
id: 76,
meta: { slug: 'calculus-volume-3' },
title: 'Calculus: Volume 3',
pdf_url: 'https://assets.openstax.org/calculus.pdf',
high_resolution_pdf_url: 'https://assets.openstax.org/calculus.pdf',
license_url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/'
}
}]]));
const download = await openstax.download('76');
assert.strictEqual(download.files.length, 1);
assert.strictEqual(download.files[0].name, 'Calculus_ Volume 3.pdf');
await assert.rejects(openstax.detail('../76'), /无效的 OpenStax ID/);
assert.strictEqual(h.getCalls().length, 1);
});
// --- Open Textbook Library ---
test('opentextbook: 搜索结果解析作者和服务端分页', async () => {
const opentextbook = h.freshRequire('sources/opentextbook.js');
h.setHandler(h.routes([['textbooks.json?q=calculus&page=2', {
body: {
data: [{
id: 10,
title: 'Calculus',
copyright_year: 2023,
contributors: [
{ first_name: 'Gilbert', last_name: 'Strang' },
{ corporate: true, title: 'Open Education Team' }
],
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus'
}],
links: { total_pages: 10, total_count: 98 }
}
}]]));
const result = await opentextbook.search('calculus', 2);
assert.strictEqual(result.page, 2);
assert.strictEqual(result.maxPage, 10);
assert.strictEqual(result.items[0].subtitle, 'Gilbert Strang, Open Education Team');
assert.strictEqual(result.items[0].date, '2023');
});
test('opentextbook: 详情展开 data 并保留单书许可', async () => {
const opentextbook = h.freshRequire('sources/opentextbook.js');
h.setHandler(h.routes([['textbooks/10.json', {
body: {
data: {
id: 10,
title: 'Calculus',
edition_statement: 'Third Edition',
copyright_year: 1991,
license: 'Attribution-NonCommercial-ShareAlike',
language: 'eng',
description: '<p>Free <b>calculus</b> textbook.</p>',
contributors: [{ first_name: 'Gilbert', last_name: 'Strang' }],
subjects: [{ name: 'Mathematics' }],
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus'
}
}
}]]));
const detail = await opentextbook.detail('10');
assert.deepStrictEqual(detail.authors, ['Gilbert Strang']);
assert.strictEqual(detail.brief, 'Free calculus textbook.');
assert.ok(detail.tags.includes('版本:Third Edition'));
assert.ok(detail.tags.includes('许可:Attribution-NonCommercial-ShareAlike'));
});
test('opentextbook: 只有真实文件 URL 才进入下载列表', async () => {
const opentextbook = h.freshRequire('sources/opentextbook.js');
h.resetCalls();
h.setHandler(h.routes([['textbooks/10.json', {
body: {
data: {
id: 10,
title: 'Calculus: Third Edition',
url: 'https://open.umn.edu/opentextbooks/textbooks/calculus',
formats: [
{ type: 'PDF', url: 'https://ocw.mit.edu/courses/calculus/open-textbook/' },
{ type: 'PDF', url: 'https://cdn.example/calculus.pdf?download=1' },
{ type: 'EPUB', url: 'https://cdn.example/calculus.epub' },
{ type: 'EPUB', url: 'https://cdn.example/calculus.epub' }
]
}
}
}]]));
const download = await opentextbook.download('10');
assert.deepStrictEqual(download.files.map((file) => file.format), ['PDF', 'EPUB']);
assert.strictEqual(download.links[1].name, 'PDF 获取页');
assert.strictEqual(download.files[0].name, 'Calculus_ Third Edition.pdf');
await assert.rejects(opentextbook.download('10/../../x'), /无效的开放教材 ID/);
assert.strictEqual(h.getCalls().length, 1);
});
// --- Wikisource ---
test('wikisource: 搜索使用整数偏移并携带可识别 User-Agent', async () => {
const wikisource = h.freshRequire('sources/wikisource-zh.js');
let request = null;
h.setHandler((url, options) => {
request = { url, options };
return h.makeResponse({
body: {
query: {
searchinfo: { totalhits: 24753 },
search: [{ pageid: 6, title: '論語' }]
}
}
});
});
const result = await wikisource.search('論語', 2);
assert.strictEqual(result.items[0].postId, '6');
assert.strictEqual(result.items[0].subtitle, '中文');
assert.strictEqual(result.maxPage, 500, 'MediaWiki 搜索最多允许偏移到 10000 条');
assert.ok(request.url.includes('sroffset=20'), request.url);
assert.match(request.options.headers['User-Agent'], /PeopleLib\/2\.1\.1/);
});
test('wikisource: 浏览按 continuation 令牌翻页', async () => {
const wikisource = h.freshRequire('sources/wikisource-en.js');
const urls = [];
h.setHandler((url) => {
urls.push(url);
if (url.includes('apcontinue=')) {
return h.makeResponse({ body: { query: { allpages: [{ pageid: 2, title: 'Second Book' }] } } });
}
return h.makeResponse({
body: {
continue: { apcontinue: 'Second Book', continue: '-||' },
query: { allpages: [{ pageid: 1, title: 'First Book' }] }
}
});
});
const first = await wikisource.list(1);
const second = await wikisource.list(2);
assert.strictEqual(first.maxPage, 2);
assert.strictEqual(second.items[0].postId, '2');
assert.ok(urls[1].includes('apcontinue=Second+Book'), urls[1]);
});
test('wikisource: 详情和导出链接由服务端标题生成', async () => {
const wikisource = h.freshRequire('sources/wikisource-zh.js');
h.resetCalls();
h.setHandler(h.routes([['pageids=6', {
body: {
query: {
pages: [{
pageid: 6,
title: '論語/學而第一',
extract: '<p>學而時習之。</p>',
fullurl: 'https://zh.wikisource.org/wiki/%E8%AB%96%E8%AA%9E',
thumbnail: { source: 'https://upload.wikimedia.org/cover.jpg' }
}]
}
}
}]]));
const detail = await wikisource.detail('6');
assert.strictEqual(detail.brief, '學而時習之。');
assert.strictEqual(detail.cover, 'https://upload.wikimedia.org/cover.jpg');
const download = await wikisource.download('6');
assert.deepStrictEqual(download.files.map((file) => file.format), ['EPUB', 'PDF']);
assert.ok(download.files[0].link.includes('lang=zh'));
assert.ok(download.files[0].link.includes('page=%E8%AB%96%E8%AA%9E%2F%E5%AD%B8%E8%80%8C%E7%AC%AC%E4%B8%80'));
assert.strictEqual(download.files[0].name, '論語_學而第一.epub');
await assert.rejects(wikisource.detail('../6'), /无效的中文维基文库 ID/);
assert.strictEqual(h.getCalls().length, 2);
});
// --- Z-Library ---
test('zlib: postId 缺 hash 时详情仍可用', async () => {
const zlib = h.freshRequire('sources/zlib.js');
const auth = require('../sources/zlib-auth');
const origSession = auth.getSession;
const origRead = auth.read;
auth.getSession = () => ({ userId: '1', userKey: 'k', mirror: 'https://z-lib.fm' });
auth.read = () => ({ email: 'e', password: 'p', userId: '1', userKey: 'k', mirror: 'https://z-lib.fm' });
try {
const urls = [];
h.setHandler((u) => {
urls.push(u);
return h.makeResponse({ body: { success: 1, book: { title: 'B', author: 'X' } } });
});
const d = await zlib.detail('123/');
assert.strictEqual(d.title, 'B');
assert.ok(urls[0].includes('/eapi/book/123'), urls[0]);
assert.ok(!urls[0].includes('/eapi/book/123/?'), '缺 hash 时不该留下尾斜杠: ' + urls[0]);
} finally {
auth.getSession = origSession;
auth.read = origRead;
}
});
test('zlib: 完全无效的 id 仍然拒绝', async () => {
const zlib = h.freshRequire('sources/zlib.js');
await assert.rejects(zlib.detail('not-an-id'), /无效的 Z-Library ID/);
});
// 实测:会话过期时 /file 返回 400 + {"success":0,"error":"Please login"}。
// 若按 HTTP 状态码短路,真实原因会被吞掉,自动重登也不会触发。
test('zlib: 4xx+JSON 的会话过期能被识别并自动重新登录', async () => {
const zlib = h.freshRequire('sources/zlib.js');
const auth = require('../sources/zlib-auth');
const orig = { read: auth.read, getSession: auth.getSession, setSession: auth.setSession, clearSession: auth.clearSession };
let session = { userId: 'old', userKey: 'stale', mirror: 'https://z-lib.fm' };
auth.read = () => ({ email: 'e@x.com', password: 'p', ...session });
auth.getSession = () => (session.userKey ? session : null);
auth.setSession = (userId, userKey, mirror) => { session = { userId, userKey, mirror }; };
auth.clearSession = () => { session = { userId: '', userKey: '', mirror: '' }; };
try {
let loggedIn = false;
h.setHandler((url) => {
if (url.includes('/rpc.php')) {
loggedIn = true;
return h.makeResponse({
headers: {
'set-cookie': [
'remix_userid=42; Path=/; Secure; HttpOnly',
'remix_userkey=fresh; Path=/; Secure; HttpOnly'
]
},
body: { errors: [], response: { redirect: '/' } }
});
}
if (url.includes('userKey=fresh')) {
return h.makeResponse({ body: { success: 1, file: { downloadLink: 'https://cdn/x.pdf', extension: 'pdf' } } });
}
return h.makeResponse({ status: 400, body: { success: 0, error: 'Please login' } });
});
const d = await zlib.download('123/abc');
assert.ok(loggedIn, '过期会话没有触发重新登录');
assert.strictEqual(d.files[0].link, 'https://cdn/x.pdf');
} finally {
Object.assign(auth, orig);
}
});
test('zlib: 凭据错误时报出服务端原因而不是 HTTP 状态码', async () => {
const zlib = h.freshRequire('sources/zlib.js');
const auth = require('../sources/zlib-auth');
const orig = { read: auth.read, getSession: auth.getSession, write: auth.write, clear: auth.clear };
// login() 先写盘,doLogin() 再读回来,所以 stub 要如实模拟这个往返
let stored = null;
auth.read = () => stored;
auth.getSession = () => null;
auth.write = (c) => { stored = { ...c }; };
auth.clear = () => { stored = null; };
try {
let request = null;
h.setHandler((url, options) => {
request = { url, body: options.body };
return h.makeResponse({
body: {
errors: [],
response: {
validationError: true,
fields: ['email', 'password'],
message: 'Incorrect email or password'
}
}
});
});
const r = await zlib.login('e@x.com', 'wrong');
assert.strictEqual(r.ok, false);
assert.match(r.error, /Incorrect email or password/, '真实原因被 HTTP 状态码盖掉了');
assert.ok(request.url.endsWith('/rpc.php'));
assert.match(request.body, /action=login/);
assert.match(request.body, /gg_json_mode=1/);
} finally {
Object.assign(auth, orig);
}
});
test('zlib: RPC 登录从安全 Cookie 建立会话', async () => {
const zlib = h.freshRequire('sources/zlib.js');
const auth = require('../sources/zlib-auth');
const orig = {
read: auth.read,
getSession: auth.getSession,
write: auth.write,
setSession: auth.setSession,
clear: auth.clear
};
let stored = null;
let session = null;
auth.read = () => stored;
auth.getSession = () => session;
auth.write = (c) => { stored = { ...c }; };
auth.setSession = (userId, userKey, mirror) => { session = { userId, userKey, mirror }; };
auth.clear = () => { stored = null; session = null; };
try {
h.setHandler(() => h.makeResponse({
headers: {
'set-cookie': [
'remix_userid=42; Path=/; Secure; HttpOnly',
'remix_userkey=key%2Bvalue; Path=/; Secure; HttpOnly'
]
},
body: { errors: [], response: { redirect: '/' } }
}));
const r = await zlib.login('e@x.com', 'correct');
assert.strictEqual(r.ok, true);
assert.strictEqual(session.userId, '42');
assert.strictEqual(session.userKey, 'key+value');
assert.match(session.mirror, /^https:\/\//);
} finally {
Object.assign(auth, orig);
}
});
test('zlib: 可注入同源浏览器登录传输并持久化会话', async () => {
const zlib = h.freshRequire('sources/zlib.js');
const auth = require('../sources/zlib-auth');
const orig = {
read: auth.read,
getSession: auth.getSession,
write: auth.write,
setSession: auth.setSession,
clear: auth.clear
};
let stored = null;
let session = null;
auth.read = () => stored;
auth.getSession = () => session;
auth.write = (c) => { stored = { ...c }; };
auth.setSession = (userId, userKey, mirror) => { session = { userId, userKey, mirror }; };
auth.clear = () => { stored = null; session = null; };
zlib.setLoginTransport(async (mirror, email, password) => {
assert.match(mirror, /^https:\/\//);
assert.strictEqual(email, 'e@x.com');
assert.strictEqual(password, 'correct');
return { userId: 'browser-user', userKey: 'browser-key' };
});
try {
const result = await zlib.login('e@x.com', 'correct');
assert.strictEqual(result.ok, true);
assert.strictEqual(session.userId, 'browser-user');
assert.strictEqual(session.userKey, 'browser-key');
} finally {
zlib.setLoginTransport(null);
Object.assign(auth, orig);
}
});
// --- Gutenberg ---
test('gutenberg: 解析格式与封面', async () => {
h.setHandler(h.routes([['gutendex.com/books', {
body: {
count: 64,
results: [{
id: 11, title: 'Alice', authors: [{ name: 'Carroll' }],
formats: { 'application/epub+zip': 'https://x/a.epub', 'image/jpeg': 'https://x/c.jpg' }
}]
}
}]]));
const r = await sources.getSource('gutenberg').search('alice', 1);
assert.strictEqual(r.items[0].postId, '11');
assert.strictEqual(r.items[0].cover, 'https://x/c.jpg');
assert.strictEqual(r.maxPage, 2);
});