mega-plant/scripts/verify-clock.mjs

64 lines
4.9 KiB
JavaScript
Raw Normal View History

// Reversible integration check against the running local app and configured PB.
// Creates one temporary mechanic, only writes clock events for that fixture,
// and deletes all fixture events/locks/employee in finally. Never clocks real staff.
// --prepare-browser leaves the fixture until --cleanup-browser, and writes a
// short-lived user cookie to a private temp file for browser checks.
import PocketBase from 'pocketbase';
import assert from 'node:assert/strict';
import { writeFileSync, readFileSync, rmSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const stateFile='/tmp/plant-clock-browser-fixture.json';
const cookieFile='/tmp/plant-clock-browser-cookie.json';
const base=process.env.MEGA_AUTH_BASE_URL||'http://localhost:6007';
const pb=new PocketBase(process.env.NEXT_PUBLIC_PB_URL);pb.autoCancellation(false);
pb.beforeSend=(url,options)=>({url,options:{...options,signal:AbortSignal.timeout(15000)}});
let fixture;
async function cleanup() {
if(!fixture)return;
const key=`mechanics:${fixture.id}`;
for(const collection of ['clock_events','clock_scan_locks']) {
const records=await pb.collection(collection).getFullList({filter:pb.filter('employee = {:key}',{key})});
for(const record of records)await pb.collection(collection).delete(record.id);
}
await pb.collection('mechanics').delete(fixture.id);fixture=null;
}
try {
await pb.collection('_superusers').authWithPassword(process.env.PB_ADMIN,process.env.PB_PASS);
if(process.argv.includes('--cleanup-browser')) {
fixture=JSON.parse(readFileSync(stateFile,'utf8'));await cleanup();
rmSync(stateFile,{force:true});rmSync(cookieFile,{force:true});console.log('Browser fixture and temporary cookie removed.');
} else {
const state=await pb.collection('user_onboarding').getFirstListItem('password_changed = true && disabled = false');
const user=await pb.collection('users').impersonate(state.user,3600);
const cookie=`mega_session=${user.authStore.token}`;
fixture=await pb.collection('mechanics').create({name:'CLOCK TEST — temporary worker',employee_code:`TEST${Date.now()}`,role:'Mechanic',source:'Temporary clock integration test; removed after verification'});
if(process.argv.includes('--prepare-browser')) {
writeFileSync(stateFile,JSON.stringify(fixture),{mode:0o600});
writeFileSync(cookieFile,JSON.stringify([{name:'mega_session',value:user.authStore.token,domain:'localhost',path:'/',httpOnly:true,sameSite:'Lax'}]),{mode:0o600});
console.log(JSON.stringify({employeeCode:fixture.employee_code,fixtureId:fixture.id,cookieFile}));fixture=null;
} else {
const call=async(path,body)=>{
const response=await fetch(base+path,{method:body?'POST':'GET',headers:{Cookie:cookie,...(body?{'Content-Type':'application/json',Origin:base}:{})},...(body?{body:JSON.stringify(body)}:{})});
return {status:response.status,body:await response.json()};
};
assert.equal((await fetch(base+'/api/clock/roster')).status,401);
const scan=(employee_no,request_id=randomUUID())=>call('/api/clock/scan',{employee_no,request_id});
const key=`mechanics:${fixture.id}`;
const payload={employee_no:fixture.employee_code,request_id:randomUUID()};
const first=await call('/api/clock/scan',payload);assert.equal(first.status,200,JSON.stringify(first.body));assert.equal(first.body.direction,'in');
const retry=await call('/api/clock/scan',payload);assert.equal(retry.status,200);assert.equal(retry.body.id,first.body.id);
const repeat=await scan(`OLD BADGE NAME | ${fixture.employee_code}`);assert.equal(repeat.status,409);assert.equal(repeat.body.repeat.direction,'in');
assert.equal((await scan('NO-SUCH-BADGE-XYZ')).status,404);
// Age the IN past the repeat window instead of waiting a minute.
await pb.collection('clock_events').update(first.body.id,{scanned_at:new Date(Date.now()-2*60_000).toISOString()});
const out=await scan(fixture.employee_code);assert.equal(out.status,200,JSON.stringify(out.body));assert.equal(out.body.direction,'out');assert.equal(out.body.sessionMinutes,2);
const roster=await call('/api/clock/roster');assert.equal(roster.status,200);assert.equal(roster.body.events.filter(e=>e.employee===key).length,2);
const anonymous=new PocketBase(process.env.NEXT_PUBLIC_PB_URL);
try {assert.equal((await anonymous.collection('clock_events').getFullList()).length,0);} catch(e) {assert.equal(e.status,403);}
const locks=await pb.collection('clock_scan_locks').getFullList({filter:pb.filter('employee = {:id}',{id:key})});assert.equal(locks.length,0);
console.log('PASS: API auth, scan IN without a site, retry, repeat refused, unknown badge, OUT, audit history, private collection and lock release.');
}
}
} catch(e) {console.error('Clock integration failed:',e.status??'',e.message);process.exitCode=1;}
finally {try{await cleanup();}catch(e){console.error('Fixture cleanup failed:',e.message);process.exitCode=1;}}