81 lines
6.1 KiB
TypeScript
81 lines
6.1 KiB
TypeScript
import { test, expect, chromium } from '@playwright/test'
|
|
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, resolve } from 'node:path'
|
|
import { spawn, execFileSync, type ChildProcess } from 'node:child_process'
|
|
import { createServer } from 'node:https'
|
|
import { request } from 'node:http'
|
|
|
|
test('LAN works without public DNS; public HTTP and HTTPS share files through a remote proxy', async () => {
|
|
test.setTimeout(60_000)
|
|
const root=mkdtempSync(join(tmpdir(),'bluebird-multiple-access-'))
|
|
const binary=resolve('../build/bluebird'), config=join(root,'config.toml')
|
|
const lan='http://127.0.0.1:19081', http='http://files.example.test:19081', https='https://files.example.test:19082'
|
|
writeFileSync(config,readFileSync('../deploy/config.example.toml','utf8')
|
|
.replace('listen = "127.0.0.1:8080"','listen = "127.0.0.1:19081"')
|
|
.replace('public_url = "http://localhost:8080"',`public_url = "${https}"`)
|
|
.replace('access_urls = []',`access_urls = ["${http}", "${lan}"]`)
|
|
.replace('secure_cookies = false','secure_cookies = true')
|
|
.replace('allow_lan_http = false','allow_lan_http = true')
|
|
.replace('trusted_proxies = []','trusted_proxies = ["127.0.0.1/32"]')
|
|
.replace('min_free_bytes = 1073741824','min_free_bytes = 0'))
|
|
execFileSync(binary,['init','--config',config],{env:{...process.env,BLUEBIRD_ADMIN_PASSWORD:'multiple-access-password'}})
|
|
execFileSync('openssl',['req','-x509','-newkey','rsa:2048','-nodes','-keyout',join(root,'key.pem'),'-out',join(root,'cert.pem'),'-days','1','-subj','/CN=files.example.test'],{stdio:'ignore'})
|
|
let worker:ChildProcess|undefined
|
|
const proxy=createServer({key:readFileSync(join(root,'key.pem')),cert:readFileSync(join(root,'cert.pem'))},(incoming,outgoing)=>{
|
|
const upstream=request(lan+incoming.url,{method:incoming.method,headers:{...incoming.headers,host:'127.0.0.1:19081','x-forwarded-host':incoming.headers.host,'x-forwarded-proto':'https','x-forwarded-for':'192.168.1.61'}},response=>{
|
|
outgoing.writeHead(response.statusCode!,response.headers);response.pipe(outgoing)
|
|
});upstream.on('error',()=>{outgoing.writeHead(502);outgoing.end()});incoming.pipe(upstream)
|
|
})
|
|
const browser=await chromium.launch({env:{...process.env,HTTP_PROXY:'',HTTPS_PROXY:'',ALL_PROXY:'',http_proxy:'',https_proxy:'',all_proxy:''},args:['--host-resolver-rules=MAP files.example.test 127.0.0.1','--no-proxy-server']})
|
|
try {
|
|
worker=spawn(binary,['serve','--config',config],{stdio:'ignore'})
|
|
await expect.poll(async()=>{try{return(await fetch(lan+'/api/v1/health')).status}catch{return 0}}).toBe(200)
|
|
await new Promise<void>(ready=>proxy.listen(19082,'127.0.0.1',ready))
|
|
const context=await browser.newContext({ignoreHTTPSErrors:true}), page=await context.newPage()
|
|
page.setDefaultTimeout(10_000)
|
|
const errors:string[]=[];page.on('pageerror',e=>errors.push(e.message))
|
|
// Abort every request to the public hostname: the LAN entrance must remain usable.
|
|
await page.route('**://files.example.test:*/**',route=>route.abort('namenotresolved'))
|
|
await page.goto(lan);await expect(page.getByRole('heading',{name:'准备寄件'})).toBeVisible()
|
|
await page.locator('input[type=file]').setInputFiles({name:'three-entrances.txt',mimeType:'text/plain',buffer:Buffer.from('same file in LAN, HTTP and HTTPS')})
|
|
await page.getByRole('button',{name:'上传并生成取件码'}).click()
|
|
await expect(page.getByRole('heading',{name:'文件已寄出'})).toBeVisible()
|
|
const lanLink=await page.getByRole('textbox',{name:'取件链接'}).inputValue()
|
|
expect(lanLink.startsWith(lan+'/#/pickup/')).toBeTruthy()
|
|
const qrBefore=await page.getByAltText('取件二维码').getAttribute('src')
|
|
await page.locator('.share-entry .n-base-selection').click()
|
|
await page.getByText('公网HTTPS · '+https,{exact:true}).click()
|
|
const httpsLink=await page.getByRole('textbox',{name:'取件链接'}).inputValue()
|
|
expect(httpsLink).toBe(lanLink.replace(lan,https))
|
|
await expect.poll(()=>page.getByAltText('取件二维码').getAttribute('src')).not.toBe(qrBefore)
|
|
await page.getByRole('button',{name:'完成',exact:true}).click()
|
|
await page.unroute('**://files.example.test:*/**')
|
|
for(const base of [lan,https,http]) {
|
|
if(base!==lan){
|
|
await page.goto(base);await expect(page.getByRole('heading',{name:'准备寄件'})).toBeVisible()
|
|
await page.locator('input[type=file]').setInputFiles({name:'public-upload.txt',mimeType:'text/plain',buffer:Buffer.from('public upload')})
|
|
await page.getByRole('button',{name:'上传并生成取件码'}).click();await expect(page.getByRole('heading',{name:'文件已寄出'})).toBeVisible()
|
|
expect((await page.getByRole('textbox',{name:'取件链接'}).inputValue()).startsWith(base+'/#/pickup/')).toBeTruthy()
|
|
await page.getByRole('button',{name:'完成',exact:true}).click()
|
|
}
|
|
await page.goto(lanLink.replace(lan,base))
|
|
await expect(page.getByRole('heading',{name:'你的文件已就位'})).toBeVisible()
|
|
const pending=page.waitForEvent('download');await page.getByRole('button',{name:'下载',exact:true}).click()
|
|
expect(readFileSync((await (await pending).path())!,'utf8')).toBe('same file in LAN, HTTP and HTTPS')
|
|
await page.goto(base+'/#/admin');await page.getByRole('textbox').first().fill('admin');await page.locator('input[type=password]').fill('multiple-access-password')
|
|
await page.getByRole('button',{name:'登录',exact:true}).click();await expect(page.getByRole('tab',{name:'服务策略'})).toBeVisible()
|
|
}
|
|
const cookies=await context.cookies(https+'/api/v1/admin/status')
|
|
expect(cookies.some(c=>c.name==='bb_admin'&&c.secure)).toBeTruthy()
|
|
expect(cookies.some(c=>c.name==='bb_admin_http'&&!c.secure)).toBeTruthy()
|
|
await page.goto(http+'/#/admin');await expect(page.getByRole('tab',{name:'服务策略'})).toBeVisible()
|
|
expect((await context.cookies(http+'/api/v1/admin/status')).some(c=>c.name==='bb_admin')).toBeFalsy()
|
|
expect(errors).toEqual([])
|
|
} finally {
|
|
await browser.close();proxy.closeAllConnections();await new Promise<void>(done=>proxy.close(()=>done()))
|
|
if(worker&&worker.exitCode===null){worker.kill('SIGTERM');await new Promise(done=>worker!.once('exit',done))}
|
|
rmSync(root,{recursive:true,force:true})
|
|
}
|
|
})
|