81 lines
6.3 KiB
JavaScript
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();}
|