67 lines
5.6 KiB
JavaScript
67 lines
5.6 KiB
JavaScript
// 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}`;
|
|
const sites=await pb.collection('sites').getList(1,1,{sort:'name'});
|
|
const site=sites.items[0].id;
|
|
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,site}),{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,site,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 lookupPath=`/api/clock/employee-lookup?employee_no=${fixture.employee_code}&site=${site}`;
|
|
const lookup=await call(lookupPath);assert.equal(lookup.status,200);assert.equal(lookup.body.nextDirection,'in');
|
|
const printedBadge=await call(`/api/clock/employee-lookup?employee_no=${encodeURIComponent(`OLD BADGE NAME | ${fixture.employee_code}`)}&site=${site}`);assert.equal(printedBadge.status,200);assert.equal(printedBadge.body.employee.id,lookup.body.employee.id);
|
|
const payload={employee_no:fixture.employee_code,site,expected_employee_id:lookup.body.employee.id,expected_event_id:null,expected_direction:'in',expected_work_date:lookup.body.workDate,request_id:randomUUID(),allow_recent:false,location:{latitude:0,longitude:0,accuracy:8}};
|
|
const first=await call('/api/clock/scan',payload);assert.equal(first.status,200,JSON.stringify(first.body));assert.equal(first.body.direction,'in');assert.equal(first.body.hasLocation,true);
|
|
const retry=await call('/api/clock/scan',payload);assert.equal(retry.status,200);assert.equal(retry.body.id,first.body.id);
|
|
assert.equal((await call('/api/clock/scan',{...payload,request_id:randomUUID()})).status,409);
|
|
const next=await call(lookupPath);assert.equal(next.body.nextDirection,'out');
|
|
await new Promise(resolve=>setTimeout(resolve,1100));
|
|
const outPayload={...payload,expected_event_id:first.body.id,expected_direction:'out',request_id:randomUUID(),location:null};
|
|
assert.equal((await call('/api/clock/scan',outPayload)).status,409);
|
|
const out=await call('/api/clock/scan',{...outPayload,allow_recent:true});assert.equal(out.status,200,JSON.stringify(out.body));assert.equal(out.body.direction,'out');
|
|
const roster=await call('/api/clock/roster');assert.equal(roster.status,200);assert.equal(roster.body.events.filter(e=>e.employee===lookup.body.employee.id).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:lookup.body.employee.id})});assert.equal(locks.length,0);
|
|
console.log('PASS: API auth, lookup, IN, retry, stale scan, recent warning, 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;}}
|