workshop-publish.test.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. import test from 'node:test';
  2. import assert from 'node:assert/strict';
  3. import fs from 'node:fs';
  4. import path from 'node:path';
  5. import crypto from 'node:crypto';
  6. import {FILES,publishFiles} from '../scripts/publish-workshop-files.mjs';
  7. const sha=b=>crypto.createHash('sha256').update(b).digest('hex');
  8. function fixture(){
  9. fs.mkdirSync('work',{recursive:true});const dir=fs.mkdtempSync(path.resolve('work/delta-publish-test-'));
  10. const sourceDir=path.join(dir,'bundle'),targetDir=path.join(dir,'site'),manifestPath=path.join(dir,'manifest.json');
  11. fs.mkdirSync(sourceDir);fs.mkdirSync(targetDir);fs.writeFileSync(path.join(targetDir,'index.html'),'known public homepage');
  12. const files=FILES.map((relative,i)=>{
  13. const a=path.join(sourceDir,relative),b=path.join(targetDir,relative),bytes=Buffer.from('new-'+relative+'\n'),old=Buffer.from('old-'+relative+'\n');
  14. fs.mkdirSync(path.dirname(a),{recursive:true});fs.writeFileSync(a,bytes);
  15. if(i!==0){fs.mkdirSync(path.dirname(b),{recursive:true});fs.writeFileSync(b,old);}
  16. return{path:relative,sha256:sha(bytes),priorNormalizedSha256:[sha(old)],allowMissing:i===0};
  17. });
  18. const manifest={version:'ksf.workshop.publish.v1',commit:'d14d02fd358c6c5dedfabc96c53fb29add8bd021',homepageSha256:sha(Buffer.from('known public homepage')),files};
  19. fs.writeFileSync(manifestPath,JSON.stringify(manifest));
  20. return{dir,sourceDir,targetDir,manifestPath,manifest};
  21. }
  22. function snapshot(root){const rows=[];function walk(dir){for(const e of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,e.name);if(e.isDirectory())walk(p);else rows.push([path.relative(root,p),sha(fs.readFileSync(p))]);}}walk(root);return rows.sort();}
  23. test('默认预检不创建文件;实际更新九文件,首页保持,重复运行不写入',()=>{
  24. const f=fixture(),before=snapshot(f.targetDir),top=fs.readdirSync(f.dir);
  25. const plan=publishFiles(f);assert.equal(plan.readOnly,true);assert.equal(plan.filesChecked,9);assert.equal(plan.changes.length,9);assert.deepEqual(snapshot(f.targetDir),before);assert.deepEqual(fs.readdirSync(f.dir),top);
  26. const applied=publishFiles({...f,apply:true});assert.equal(applied.filesystemVerified,true);assert.equal(applied.publicSiteVerified,false);assert.ok(!applied.backup.startsWith(f.targetDir+path.sep));
  27. assert.equal(fs.readFileSync(path.join(f.targetDir,'index.html'),'utf8'),'known public homepage');
  28. for(const name of FILES)assert.equal(fs.readFileSync(path.join(f.targetDir,name),'utf8'),fs.readFileSync(path.join(f.sourceDir,name),'utf8'));
  29. const after=snapshot(f.targetDir),repeat=publishFiles({...f,apply:true});assert.deepEqual(repeat.changes,[]);assert.equal(repeat.backup,null);assert.deepEqual(snapshot(f.targetDir),after);
  30. });
  31. test('未知目标内容、首页不符、坏包及超出白名单均在写入之前拒绝',()=>{
  32. for(const kind of ['unknown','index','source','manifest']){
  33. const f=fixture();
  34. if(kind==='unknown')fs.writeFileSync(path.join(f.targetDir,FILES[2]),'another publisher changed this');
  35. if(kind==='index')fs.writeFileSync(path.join(f.targetDir,'index.html'),'different website');
  36. if(kind==='source')fs.appendFileSync(path.join(f.sourceDir,FILES[2]),'corrupt');
  37. if(kind==='manifest'){f.manifest.files[0].path='index.html';fs.writeFileSync(f.manifestPath,JSON.stringify(f.manifest));}
  38. const before=snapshot(f.targetDir),top=fs.readdirSync(f.dir);
  39. assert.throws(()=>publishFiles({...f,apply:true}),{code:{unknown:'UNKNOWN_TARGET_CONTENT',index:'HOMEPAGE_MISMATCH',source:'PAYLOAD_HASH_MISMATCH',manifest:'UNEXPECTED_FILE_LIST'}[kind]});
  40. assert.deepEqual(snapshot(f.targetDir),before);assert.deepEqual(fs.readdirSync(f.dir),top);
  41. }
  42. });
  43. test('中途写入失败恢复已改文件,只清理本次新建文件,保留完整备份',()=>{
  44. const f=fixture(),before=snapshot(f.targetDir);let attempts=0,error;
  45. try{publishFiles({...f,apply:true},{beforeWrite(){if(++attempts===4)throw Object.assign(Error('simulated write failure'),{code:'EACCES'});}});}catch(e){error=e;}
  46. assert.equal(attempts,4);assert.equal(error.rollbackComplete,true);assert.deepEqual(snapshot(f.targetDir),before);
  47. const journal=JSON.parse(fs.readFileSync(path.join(error.backup,'journal.json')));assert.equal(journal.status,'stopped');assert.equal(journal.files.length,9);
  48. });
  49. test('已发布文件被其他人并发改动时,回滚保留外部版本并报告未完全恢复',()=>{
  50. const f=fixture();let error;
  51. try{publishFiles({...f,apply:true},{beforeWrite(relative){if(relative===FILES[2]){fs.writeFileSync(path.join(f.targetDir,FILES[1]),'external concurrent edit');throw Error('stop');}}});}catch(e){error=e;}
  52. assert.equal(error.rollbackComplete,false);assert.equal(fs.readFileSync(path.join(f.targetDir,FILES[1]),'utf8'),'external concurrent edit');assert.equal(fs.existsSync(path.join(f.targetDir,FILES[0])),false);
  53. assert.equal(fs.readFileSync(path.join(f.targetDir,'index.html'),'utf8'),'known public homepage');
  54. });