board-local-deploy.test.mjs 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. import fs from 'node:fs';
  2. import os from 'node:os';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. import test from 'node:test';
  7. import {publishFiles} from '../scripts/publish-board-local.mjs';
  8. import {deployMemberRoster} from '../scripts/deploy-board-member-roster.mjs';
  9. const sha=b=>crypto.createHash('sha256').update(b).digest('hex');
  10. const root=path.resolve('.');
  11. function fixture(){
  12. const base=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-board-deploy-test-')),targetDir=path.join(base,'site');fs.mkdirSync(targetDir);
  13. const manifest=JSON.parse(fs.readFileSync('deploy/board-local-20261111.json'));manifest.homepageSha256=sha('fixture homepage');
  14. fs.writeFileSync(path.join(targetDir,'index.html'),'fixture homepage');
  15. for(const f of manifest.files){const to=path.join(targetDir,f.path);fs.mkdirSync(path.dirname(to),{recursive:true});fs.copyFileSync(f.path,to);const bytes=fs.readFileSync(f.path);f.sha256=sha(bytes.includes(0)?bytes:Buffer.from(bytes.toString('utf8').replaceAll('\r\n','\n')));}
  16. const manifestPath=path.join(base,'manifest.json');fs.writeFileSync(manifestPath,JSON.stringify(manifest));
  17. return{base,manifest,targetDir,manifestPath,sourceDir:root};
  18. }
  19. test('完整前端发布预检只读,apply可重复且不替换首页',()=>{
  20. const f=fixture();try{
  21. const added=f.manifest.files.find(p=>p.allowMissing);fs.unlinkSync(path.join(f.targetDir,added.path));
  22. const plan=publishFiles(f);assert.equal(plan.readOnly,true);assert.deepEqual(plan.changes,[added.path]);assert.equal(fs.existsSync(path.join(f.targetDir,added.path)),false);
  23. const result=publishFiles({...f,apply:true});assert.equal(result.filesystemVerified,true);assert.equal(result.homepageModified,false);
  24. assert.deepEqual(publishFiles(f).changes,[]);assert.equal(fs.readFileSync(path.join(f.targetDir,'index.html'),'utf8'),'fixture homepage');
  25. }finally{fs.rmSync(f.base,{recursive:true,force:true});}
  26. });
  27. test('未知现场拒绝,写入失败恢复,新静态文件保持0644',()=>{
  28. const f=fixture();try{
  29. const added=f.manifest.files.filter(p=>p.allowMissing).slice(0,2);for(const p of added)fs.unlinkSync(path.join(f.targetDir,p.path));
  30. assert.throws(()=>publishFiles({...f,apply:true},{beforeWrite:p=>{if(p===added[1].path)throw Error('fixture');}}),e=>e.rollbackComplete===true);
  31. for(const p of added)assert.equal(fs.existsSync(path.join(f.targetDir,p.path)),false);
  32. const existing=f.manifest.files.find(p=>!p.allowMissing);fs.writeFileSync(path.join(f.targetDir,existing.path),'unknown concurrent content');
  33. assert.throws(()=>publishFiles({...f,apply:true}),{code:'UNKNOWN_TARGET_CONTENT'});
  34. fs.copyFileSync(existing.path,path.join(f.targetDir,existing.path));publishFiles({...f,apply:true});
  35. if(process.platform!=='win32')for(const p of added)assert.equal(fs.statSync(path.join(f.targetDir,p.path)).mode&0o777,0o644);
  36. }finally{fs.rmSync(f.base,{recursive:true,force:true});}
  37. });
  38. test('成员接口未知版本在写入前停止',async()=>{
  39. let writes=0;await assert.rejects(deployMemberRoster({manifest:{functions:[{objectId:'a',name:'n',path:'/n',beforeSha256:sha('old'),afterSha256:sha('new')}]},request:async(method)=>{if(method!=='GET')writes++;return{name:'n',path:'/n',code:'unexpected'};}}),/UNKNOWN_MEMBER_FUNCTION_BASELINE/);assert.equal(writes,0);
  40. });
  41. test('成员接口已发布版本只读和apply均不重复写入',async()=>{
  42. let writes=0;const cfg={manifest:{functions:[{objectId:'a',name:'n',path:'/n',beforeSha256:sha('old'),afterSha256:sha('new')}]},request:async(method)=>{if(method!=='GET')writes++;return{name:'n',path:'/n',code:'new'};}};
  43. assert.equal((await deployMemberRoster(cfg)).readOnly,true);assert.equal((await deployMemberRoster({...cfg,apply:true})).functions[0].action,'unchanged');assert.equal(writes,0);
  44. });