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: '' })); 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 = '

Book One

'; const footer = ''; const pager = '
23
'; 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 = '

Solo

'; h.setHandler(() => h.makeResponse({ body: card + 'junk' })); 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: `

B

` })); 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 = `40 http://arxiv.org/abs/2201.00978v1Paper T S2022-01-03T00:00:00Z A One `; 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: '

Open calculus textbook.

', 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: '

Free calculus textbook.

', 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: '

學而時習之。

', 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); });