File size: 3,561 Bytes
b2210de
 
 
 
 
2c1d035
 
b2210de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const root = process.argv[2] ? path.resolve(process.argv[2]) : path.resolve(__dirname, '..');
const contract = fs.readFileSync(path.join(root, 'static/reader/core/reader-contract.js'), 'utf8');
const security = fs.readFileSync(path.join(root, 'static/reader/core/reader-security.js'), 'utf8');
const source = 'https://voiceofml-search.hf.space/api/reader-bucket-resource?path=' + encodeURIComponent(
  'chapters/ebook/epub/' + 'a'.repeat(64) + '/' + 'b'.repeat(16) + '/chapter-manifest.json');

function fixture(url = source, pathname = '/static/reader.html', pendingBody = false) {
  const timers = new Map(), listeners = new Map(), calls = [];
  let body;
  const sandbox = {
    URL, URLSearchParams, AbortController, Response, TextEncoder, TextDecoder,
    location: {origin:'https://site.test', href:'https://site.test' + pathname, pathname,
      search:'?url=' + encodeURIComponent(url)},
    sessionStorage: {getItem:()=>null},
    setTimeout: fn => {const id=timers.size+1;timers.set(id,fn);return id;},
    clearTimeout: id => timers.delete(id),
    addEventListener: (name, fn) => listeners.set(name, fn),
    removeEventListener: name => listeners.delete(name),
    fetch: async (url, options) => {
      calls.push({url, options});
      if (!pendingBody) return new Response('{"version":1}');
      const stream = new ReadableStream({start:controller=>{body=controller;}});
      options.signal.addEventListener('abort',()=>body.error(options.signal.reason),{once:true});
      return new Response(stream);
    }
  };
  sandbox.self = sandbox;
  vm.createContext(sandbox);
  vm.runInContext(security, sandbox);
  vm.runInContext(contract, sandbox);
  return {sandbox, timers, listeners, calls};
}

async function main() {
  for (const pathname of ['/static/reader.html', '/search/static/reader.html']) {
    const f=fixture(source,pathname);
    vm.runInContext(contract,f.sandbox);
    assert.strictEqual(f.calls.length,1,'classic/module loads share the preload');
    assert.strictEqual(f.calls[0].options.credentials,'omit');
    const result=await f.sandbox.__VOICE_READER_CHAPTER_PRELOAD__.manifest;
    assert.strictEqual(await result.text(),'{"version":1}');
    assert.strictEqual(f.timers.size,0);
    f.sandbox.__VOICE_READER_CHAPTER_PRELOAD__.dispose();
    assert.strictEqual(f.listeners.size,0);
  }
  for (const url of [source.replace('voiceofml-search.hf.space','evil.test'), source+'&path=extra',
    source+'&other=1',source+'#fragment',source.replace('chapter-manifest.json','chapter-0001.xhtml')]) {
    assert.strictEqual(fixture(url).calls.length,0,'invalid or non-manifest URL must not preload');
  }
  assert.strictEqual(fixture(source,'/search/').calls.length,0,'search page cannot preload a document');
  for (const event of ['voice-reader-dispose','pagehide','timeout']) {
    const f=fixture(source,'/static/reader.html',true), pending=f.sandbox.__VOICE_READER_CHAPTER_PRELOAD__.manifest;
    await Promise.resolve();
    if (event==='timeout') [...f.timers.values()][0]();
    else f.listeners.get(event)({persisted:false});
    await assert.rejects(pending,error=>error.name==='AbortError');
    assert.strictEqual(f.timers.size,0);
    f.sandbox.__VOICE_READER_CHAPTER_PRELOAD__.dispose();
    assert.strictEqual(f.listeners.size,0);
  }
  console.log('chapter manifest preload validation, reuse, body deadline and disposal passed');
}
main().catch(error=>{console.error(error);process.exitCode=1;});