66 lines
6.2 KiB
TypeScript
66 lines
6.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {chromium} from 'playwright-core';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {newToken,tokenDigest,ACCOUNT_COOKIE} from '../src/lib/account-crypto';
|
|
import {guidanceXml,type Guidance} from '../src/content/guides/catalog';
|
|
import {db} from '../src/lib/db';
|
|
import {publishDocument,resolveDocument,withdrawDocument,discoverDocuments} from '../src/lib/guidance-store';
|
|
export async function verifyGuidancePublishing(base:string){
|
|
const owner=randomUUID(),other=randomUUID(),id='user-'+owner+'-document';
|
|
for(const uid of [owner,other])await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,'test',now())",[uid,uid+'@example.com']);
|
|
const doc:Guidance={id,revision:1,kind:'context',title:'实际数据库文档',summary:'摘要',applies:'条件',body:'正文',steps:[],checks:[],links:[],tags:[]};
|
|
await assert.rejects(()=>publishDocument(other,doc),/INVALID_ID/);
|
|
await publishDocument(other,{...doc,id:'user-'+other+'-document'});
|
|
await publishDocument(owner,doc);assert.equal((await resolveDocument(id,1))?.title,doc.title);
|
|
await assert.rejects(()=>publishDocument(other,{...doc,revision:2}),/INVALID_ID/);
|
|
const concurrent=await Promise.allSettled([publishDocument(owner,{...doc,revision:2,body:'新正文'}),publishDocument(owner,{...doc,revision:2})]);assert.equal(concurrent.filter(r=>r.status==='fulfilled').length,1);
|
|
assert.equal((await resolveDocument(id,1))?.body,'正文');assert.equal(await resolveDocument(id,999),null);
|
|
await assert.rejects(()=>db().query('UPDATE guidance_revisions SET content=$2 WHERE document_id=$1',[id,{}]),/immutable/);
|
|
assert.equal(await withdrawDocument(other,id),false);assert.equal(await withdrawDocument(owner,id),true);assert.equal(await resolveDocument(id),null);assert.ok(await resolveDocument(id,1));assert.ok(!(await discoverDocuments()).some(d=>d.id===id));
|
|
const token=newToken();await db().query("INSERT INTO account_tokens(digest,user_id,kind,expires_at) VALUES($1,$2,'session',now()+interval '1 hour')",[tokenDigest(token),owner]);
|
|
const headers={'Content-Type':'application/json',origin:process.env.ACCOUNT_PUBLIC_ORIGIN||base,cookie:ACCOUNT_COOKIE+'='+token};
|
|
const httpDoc={...doc,id:'user-'+owner+'-http',tags:['往返标签']};
|
|
const request=(path:string,method:string,body?:unknown,extra:Record<string,string>={})=>fetch(base+'/api/documents'+path,{method,headers:{...headers,...extra},...(body?{body:typeof body==='string'?body:JSON.stringify(body)}:{})});
|
|
assert.equal((await request('','POST',httpDoc,{cookie:''})).status,401);
|
|
assert.equal((await request('','POST',httpDoc,{origin:'https://untrusted.example'})).status,403);
|
|
const preview=await request('/preview','POST',httpDoc);assert.equal(preview.status,200,await preview.clone().text());assert.equal(await resolveDocument(httpDoc.id,1),null);
|
|
const created=await request('','POST',guidanceXml(httpDoc),{'Content-Type':'application/xml'});assert.equal(created.status,201,await created.clone().text());
|
|
assert.equal((await request('','POST',httpDoc)).status,409);
|
|
assert.equal((await request('','POST',{...httpDoc,id:'user-'+other+'-http'})).status,400);
|
|
const own=await fetch(base+'/api/documents?author='+owner).then(r=>r.json());assert.ok(own.documents.some((d:{id:string})=>d.id===httpDoc.id));assert.ok(own.documents.every((d:{author:string})=>d.author===owner));assert.equal((await fetch(base+'/api/documents?author=invalid')).status,400);
|
|
const xml=await fetch(base+'/api/documents/'+httpDoc.id+'?revision=1&format=xml');assert.equal(xml.status,200);assert.match(await xml.text(),/<tag>往返标签<\/tag>/);
|
|
const page=await fetch(base+'/documents/'+httpDoc.id+'?revision=1');assert.equal(page.status,200);assert.ok((await page.text()).includes('实际数据库文档'));
|
|
// The homepage and discovery list hydrate from the real API, so verify them in Chromium on the CI runner.
|
|
const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});
|
|
try{
|
|
const page=await browser.newPage();
|
|
const link=`a[href="/documents/${httpDoc.id}?revision=1"]`;
|
|
await page.goto(base+'/',{waitUntil:'domcontentloaded'});
|
|
await page.locator(link).waitFor({state:'visible'});assert.equal(await page.locator(link).getByRole('heading').textContent(),httpDoc.title);
|
|
// Published matches must not inherit the examples' empty state; submitted filters survive reload.
|
|
const search=page.getByRole('textbox',{name:'搜索文档'});
|
|
await search.fill(httpDoc.title);await search.press('Enter');
|
|
await page.waitForURL(url=>url.searchParams.get('q')===httpDoc.title);
|
|
await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor();
|
|
assert.equal(await page.getByText('没有匹配文档',{exact:false}).count(),0);
|
|
await page.reload({waitUntil:'domcontentloaded'});
|
|
await page.getByRole('status').filter({hasText:'找到 2 篇文档'}).waitFor();
|
|
assert.equal(await search.inputValue(),httpDoc.title);
|
|
await page.getByRole('tab',{name:/事实依据/}).click();
|
|
await page.waitForURL(url=>url.searchParams.get('kind')==='evidence');
|
|
await page.getByRole('heading',{name:'没有匹配文档',exact:true}).waitFor();
|
|
await page.getByRole('button',{name:'清除所有筛选'}).click();
|
|
await page.waitForURL(url=>url.search==='');
|
|
await page.locator(link).waitFor({state:'visible'});
|
|
assert.equal(await search.inputValue(),'');
|
|
await page.getByRole('navigation',{name:'主导航',exact:true}).getByRole('link',{name:'社区文档',exact:true}).click();await page.locator(link).waitFor({state:'visible'});
|
|
await page.locator(link).click();await page.locator('h1').waitFor();assert.equal(await page.locator('h1').textContent(),httpDoc.title);
|
|
await page.goto(base+'/write',{waitUntil:'domcontentloaded'});assert.equal(await page.locator('h1').textContent(),'发布独立文档');
|
|
}finally{await browser.close();}
|
|
assert.equal((await request('/'+httpDoc.id,'DELETE',undefined,{cookie:''})).status,401);
|
|
assert.equal((await request('/'+httpDoc.id,'DELETE')).status,200);
|
|
assert.equal((await fetch(base+'/api/documents/'+httpDoc.id)).status,404);
|
|
assert.equal((await fetch(base+'/api/documents/'+httpDoc.id+'?revision=1')).status,200);
|
|
|
|
}
|