76 lines
7.4 KiB
TypeScript
76 lines
7.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import {test,mock} from 'node:test';
|
||
import {createServer} from 'node:http';
|
||
import {createSocket} from 'node:dgram';
|
||
import {chromium} from 'playwright';
|
||
import {useTestDatabase} from './test-database.ts';
|
||
useTestDatabase('browser');
|
||
const launch=chromium.launch.bind(chromium);
|
||
mock.method(chromium,'launch',async(options:Parameters<typeof chromium.launch>[0])=>{
|
||
try{return await launch(options);}catch(error){console.error('Chromium launch diagnostic:',(error as Error).message);throw error;}
|
||
});
|
||
const original=await import('../lib/http.ts');
|
||
let fixture='',proxied:string[]=[],cookieSeen=false;
|
||
mock.module(new URL('../lib/http.ts',import.meta.url).href,{namedExports:{...original,requestPublic:async(url:string,options:{headers?:Record<string,string>})=>{
|
||
proxied.push(url);
|
||
if(url.endsWith('/go-book'))return {code:302,headers:{location:'/book/111431'},body:Buffer.alloc(0),type:'text/html'};
|
||
if(url.endsWith('/session')){cookieSeen=options.headers?.cookie?.includes('anonymous_session=fixture')??false;return {code:200,headers:{'content-type':'text/plain'},body:Buffer.from('ok'),type:'text/plain'};}
|
||
if(url.includes('/probe/'))return {code:200,headers:{'content-type':'application/javascript'},body:Buffer.from('window.loadedChunks=(window.loadedChunks||0)+1;'),type:'application/javascript'};
|
||
if(url.endsWith('/redirect'))return {code:302,headers:{location:target},body:Buffer.alloc(0),type:'text/html'};
|
||
if(!/^https:\/\/(e\.lanbook\.com|book\.ru)\//.test(url))throw new Error('Unexpected transport destination');
|
||
return {code:200,headers:{'content-type':'text/html; charset=utf-8','set-cookie':['anonymous_session=fixture; Path=/; Secure; SameSite=Lax']},body:Buffer.from(fixture),type:'text/html'};
|
||
}}});
|
||
const {checkBrowser}=await import('../lib/browser.ts');
|
||
let target='';
|
||
test('sandboxed Chromium blocks private HTTP, redirects, frames, popups, workers, WebSockets and WebRTC UDP',async()=>{
|
||
let httpHits=0,udpHits=0;
|
||
const server=createServer((_req,res)=>{httpHits++;res.end('private');}),udp=createSocket('udp4');udp.on('message',()=>udpHits++);
|
||
await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve));await new Promise<void>(resolve=>udp.bind(0,'127.0.0.1',resolve));
|
||
const address=server.address();assert.ok(address&&typeof address!=='string');target='http://127.0.0.1:'+address.port+'/private';
|
||
const udpAddress=udp.address();
|
||
fixture=`<!doctype html><html><body><h1>Book</h1><div id="message">Loading</div><script>
|
||
fetch(${JSON.stringify(target)}).catch(()=>{});
|
||
fetch('https://e.lanbook.com/session').catch(()=>{});
|
||
fetch('https://e.lanbook.com/redirect').catch(()=>{});
|
||
const iframe=document.createElement('iframe');iframe.src=${JSON.stringify(target)};document.body.appendChild(iframe);
|
||
window.open(${JSON.stringify(target)});
|
||
new Worker(URL.createObjectURL(new Blob(['fetch('+JSON.stringify(${JSON.stringify(target)})+').catch(()=>{})'])));
|
||
try{new WebSocket(${JSON.stringify(target.replace('http:','ws:'))})}catch{}
|
||
try{const rtc=new RTCPeerConnection({iceServers:[{urls:'stun:127.0.0.1:${udpAddress.port}'}]});rtc.createDataChannel('test');rtc.createOffer().then(offer=>rtc.setLocalDescription(offer));}catch{}
|
||
setTimeout(()=>document.getElementById('message').textContent='Для доступа к чтению книги необходимо авторизоваться.',2000);
|
||
</script></body></html>`;
|
||
try{
|
||
const check=await checkBrowser('https://e.lanbook.com/book/111431');
|
||
assert.equal(check.status,'auth',check.reason);assert.equal(httpHits,0);assert.equal(udpHits,0);assert.equal(cookieSeen,true,'The fresh anonymous cookie must survive the pinned transport');
|
||
assert.ok(proxied.every(url=>url.startsWith('https://e.lanbook.com/')));assert.ok(proxied.includes('https://e.lanbook.com/redirect'));
|
||
}finally{await new Promise<void>(resolve=>server.close(()=>resolve()));udp.close();}
|
||
});
|
||
test('a CSS-hidden login template does not make a known library book unavailable',async()=>{
|
||
fixture='<html><head><style>.reader-login-template {display:none}</style></head><body><h1>Book</h1><div class="reader-login-template">Для доступа к чтению книги необходимо авторизоваться.</div><p>Only the catalogue card has loaded.</p></body></html>';
|
||
const check=await checkBrowser('https://e.lanbook.com/book/2');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200,'The browser must actually render the fixture');
|
||
});
|
||
test('a burst of essential chunks is queued instead of dropping requests over the parallelism cap',async()=>{
|
||
fixture='<html><body><h1>Book</h1><div id="message">Loading</div><script>for(let i=0;i<16;i++){let s=document.createElement("script");s.src="/probe/"+i+".js";document.head.appendChild(s)}setInterval(()=>{if(window.loadedChunks===16)document.getElementById("message").textContent="Для доступа к чтению книги необходимо авторизоваться."},50)</script></body></html>';
|
||
const check=await checkBrowser('https://e.lanbook.com/book/3');assert.equal(check.status,'auth',check.reason);
|
||
});
|
||
test('a public document redirect is explicitly revalidated and rendered at its final URL',async()=>{
|
||
fixture='<html><body><h1>Book</h1>Для доступа к чтению книги необходимо авторизоваться.</body></html>';
|
||
const check=await checkBrowser('https://e.lanbook.com/go-book');assert.equal(check.status,'auth',check.reason);assert.equal(check.finalUrl,'https://e.lanbook.com/book/111431');
|
||
});
|
||
test('BOOK.ru CSS pseudo-element tooltip proves that only a guest fragment is available',async()=>{
|
||
fixture='<html><head><style>a::after{content:none}a:hover::after{content:"В гостевом режиме доступны только 20 страниц книги.";display:block}</style></head><body><h1>Book</h1><a href="https://reader.new.book.ru/">Ознакомиться</a></body></html>';
|
||
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.match(check.reason,/до 20 страниц/);
|
||
});
|
||
|
||
test('BOOK.ru locked orange preview is unavailable even without a loaded tooltip',async()=>{
|
||
fixture='<html><body><h1>Book</h1><a class="book_mode_orange__fixture" href="https://reader.new.book.ru/"><span class="book_marker2__fixture">Lock</span>Ознакомиться</a></body></html>';
|
||
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'restricted',check.reason);assert.equal(check.evidence?.rule,'bookru-locked-preview');assert.doesNotMatch(check.reason,/20/);
|
||
fixture=fixture.replace('</body>','<a href="https://reader.new.book.ru/">Читать полностью</a></body>');
|
||
assert.equal((await checkBrowser('https://book.ru/book/940586')).status,'unknown','A full-reading control contradicts the locked-preview-only signal');
|
||
});
|
||
|
||
test('a hidden BOOK.ru preview cannot prove a restriction through its pseudo-element',async()=>{
|
||
fixture='<html><head><style>a::after{content:"В гостевом режиме доступны только 20 страниц книги.";display:block}</style></head><body><h1>Book</h1><div style="display:none"><a href="https://reader.new.book.ru/">Ознакомиться</a></div></body></html>';
|
||
const check=await checkBrowser('https://book.ru/book/940586');assert.equal(check.status,'unknown',check.reason);assert.equal(check.code,200);
|
||
});
|