conductor/scripts/test-setup-conductor.mjs

81 lines
6.3 KiB
JavaScript

// Run only against the disposable nested-Docker host created by test-setup-conductor.py.
import {chromium,expect} from '@playwright/test';
import {execFileSync} from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
const [host,port]=process.argv.slice(2);
if(!/^conductor-slice12-[a-f0-9]{10}$/.test(host)||!/^[0-9]+$/.test(port))throw new Error('Disposable test host required.');
const artifacts=path.resolve('test-results/slice12');
const hostname='conductor.customer.test',base='https://'+hostname+':'+port;
const run=(args,input)=>execFileSync('docker',['exec','-i',host,...args],{input,encoding:'utf8',maxBuffer:4e6});
const result=JSON.parse(run(['cat','/opt/skeletonworks/conductor/'+hostname+'/setup-result.json']));
const inner=result.container;
const cli=(...args)=>JSON.parse(run(['docker','exec',inner,'node','dist/scripts/installedApps.js',...args]));
let browser,page;
try{
browser=await chromium.launch({headless:true,args:['--host-resolver-rules=MAP '+hostname+' 127.0.0.1','--no-proxy-server']});
const context=await browser.newContext({baseURL:base,ignoreHTTPSErrors:true,viewport:{width:1400,height:1000}});
page=await context.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message));
await page.goto('/');
await expect(page).toHaveTitle('Set up · Conductor');
// Browser-based setup proves HTTPS cookies work through the generated Caddy route.
await page.getByLabel('Username',{exact:true}).fill('customer-admin');
await page.getByLabel('Password',{exact:true}).fill('disposable-setup-password');
await page.getByLabel('Confirm password',{exact:true}).fill('disposable-setup-password');
await page.getByRole('button',{name:'Create administrator',exact:true}).click();
await expect(page.getByRole('heading',{name:'Welcome to Conductor'})).toBeVisible();
await expect(page).toHaveTitle('Conductor');
await expect(page.locator('#conductor-favicon')).toHaveAttribute('href','/favicon.svg');
await expect(page.getByText(/Backend scaffold|coming in a later step/)).toHaveCount(0);
const nav=name=>page.getByRole('navigation').getByRole('button',{name,exact:true}).click();
await nav('Projects');
await expect(page.getByText('No saved projects yet.')).toBeVisible();
await page.getByRole('button',{name:'New project',exact:true}).click();
await page.getByLabel('Project name',{exact:true}).fill('Fresh VM project');
await page.getByRole('button',{name:'Create project',exact:true}).click();
await expect(page.getByTitle('Click to rename project')).toHaveText('Fresh VM project');
await nav('Publishing');
await expect(page.getByText('No included applications are installed yet.',{exact:false})).toBeVisible();
expect(cli('list')).toEqual([]);
await page.screenshot({path:path.join(artifacts,'fresh-publishing.png'),fullPage:true});
// Install a verified, independently packaged app, without any core image/config change.
const originalImage=run(['docker','inspect','--format','{{.Image}}',inner]);
run(['docker','exec',inner,'mkdir','-m','700','/tmp/example-package']);
run(['bash','-c','tar -C /opt/conductor-test/example-package -cf - . | docker exec -i "$1" tar -xf - -C /tmp/example-package','fixture',inner]);
const installed=cli('install','/tmp/example-package');
await page.getByRole('button',{name:'Refresh applications',exact:true}).click();
await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toBeVisible();
await expect(page.getByText('Social Scheduler',{exact:true})).toHaveCount(0);
await page.screenshot({path:path.join(artifacts,'installed-publishing.png'),fullPage:true});
await page.getByRole('link',{name:'Open Example Application',exact:true}).click();
await expect(page).toHaveTitle('Example Application · Conductor');
await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toBeVisible();
expect(run(['docker','inspect','--format','{{.Image}}',inner])).toBe(originalImage);
const backup=JSON.parse(run(['runuser','-u','admin','--','/opt/skeletonworks/scripts/backup-conductor.sh','--fqdn',hostname]));
expect(backup.coverage.installedApplications).toBe(true);
// Reject corrupt/unsafe backups before touching the live fixture.
run(['python3','/opt/conductor-test/check-backup-rejection.py',backup.backup.file,hostname]);
// Lose only the known disposable application's assets, then prove recovery from the archive.
run(['docker','exec',inner,'node','-e','require("fs").rmSync("/data/applications/example-application",{recursive:true,force:true})']);
run(['/opt/skeletonworks/scripts/restore-conductor.sh','--fqdn',hostname,'--backup-file',backup.backup.file,'--force','--skip-safety-backup']);
expect(cli('list')[0].digest).toBe(installed.digest);
run(['docker','exec',inner,'node','-e','if(!require("fs").existsSync(process.argv[1]))process.exit(1)','/data/applications/example-application/'+installed.digest+'/app.js']);
run(['docker','compose','-f','/opt/skeletonworks/conductor/'+hostname+'/docker-compose.yml','up','-d','--force-recreate']);
for(let i=0;i<50;i++){
if(run(['docker','inspect','--format','{{.State.Health.Status}}',inner]).trim()==='healthy')break;
await new Promise(r=>setTimeout(r,500));
}
expect(run(['docker','inspect','--format','{{.State.Health.Status}}',inner]).trim()).toBe('healthy');
await page.goto('/');await nav('Projects');
await expect(page.getByRole('article',{name:'Project Fresh VM project',exact:true})).toBeVisible();
await page.goto('/example-application');
await expect(page.getByRole('heading',{name:'Example Application',exact:true})).toBeVisible();
expect(cli('list')[0].digest).toBe(installed.digest);
expect(errors).toEqual([]);
await fs.writeFile(path.join(artifacts,'browser-result.json'),JSON.stringify({passed:true,checks:['HTTPS browser first-run administrator','default icon and browser title','finished Projects','empty initial Publishing','independent app registration and launch','unchanged core image','admin-run scheduled backup','corrupt and unsafe archive refusal','app asset restore','account/project/app persistence after recreation']},null,2));
console.log('PASS: generated HTTPS route, browser administration, Projects, empty/populated Publishing, app restore and recreation.');
}catch(error){
if(page)await page.screenshot({path:path.join(artifacts,'failure.png'),fullPage:true}).catch(()=>{});
throw error;
}finally{await browser?.close();}