Ergänze Auftragsbearbeitung, Disposition, Stammdaten, Kunden- und Transporteurportale, Lagerverwaltung, Abrechnung, Exporte und Statistik. Füge Kontoeinstellungen, Mitteilungen, Automatikdienste und Betriebsstatus sowie die zugehörigen Tests und PHP-Abgleichsdokumentation hinzu. Vergrößere die persönlichen Auftragslisten auf einheitliche Höhen und richte Statusanzeigen sowie die Ranking-Vorschau-Steuerelemente bündig aus. Validierung: 44 Frontendtests und Produktionsbuild erfolgreich. Backend funktional unverändert gegenüber dem vollständigen Lauf mit 1.624 erfolgreichen und drei übersprungenen Tests. Layout im Browser und lokale Bereitstellung geprüft.
15 lines
4.9 KiB
JavaScript
15 lines
4.9 KiB
JavaScript
import test from 'node:test';import assert from 'node:assert/strict';import vm from 'node:vm';import {readFileSync} from 'node:fs';import ts from 'typescript';
|
|
const code=ts.transpileModule(readFileSync(new URL('../src/api.ts',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022}}).outputText;
|
|
const home={userId:11,username:'synthetic',headquartersId:7,originalHeadquartersId:7,roles:[]};
|
|
const json=(data,status=200)=>new Response(JSON.stringify(data),{status,headers:{'Content-Type':'application/json'}});
|
|
function deferred(){let resolve;const promise=new Promise(r=>resolve=r);return {promise,resolve};}
|
|
function client(stored){const entries=new Map(stored?[['votian.workspace',JSON.stringify(stored)]]:[]),storage={getItem:k=>entries.get(k)||null,setItem:(k,v)=>entries.set(k,v),removeItem:k=>entries.delete(k)};const calls=[];let intercept;
|
|
const context={exports:{},Headers,URLSearchParams,Response,URL,setTimeout,sessionStorage:storage,window:{sessionStorage:storage,localStorage:{removeItem(){}}},fetch:async(path,options)=>{calls.push({path,options});if(intercept){const v=intercept(path,options);if(v)return await v;}if(path==='/api/me'||path==='/api/login')return json(home);if(path==='/api/csrf')return json({token:'isolated',headerName:'X-CSRF-TOKEN'});if(path.startsWith('/api/account/headquarters/'))return json({...home,headquartersId:Number(path.split('/').pop())});if(path==='/api/logout')return new Response(null,{status:204});return json({ok:true});}};
|
|
vm.runInNewContext(code,context,{filename:'api.js'});return {api:context.exports,calls,entries,intercept:fn=>intercept=fn};}
|
|
const header=call=>call.options.headers.get('X-Workspace-Headquarters');
|
|
test('restores only a preference bound to the same user and home; recovery endpoints stay unscoped',async()=>{const c=client({userId:11,homeId:7,target:8});assert.equal((await c.api.getSession()).headquartersId,8);assert(c.calls.every(x=>header(x)===null));await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),'8');await c.api.request('/account/headquarters');assert.equal(header(c.calls.at(-1)),null);await c.api.logout();assert.equal(header(c.calls.find(x=>x.path==='/api/logout')),null);assert(!c.entries.has('votian.workspace'));const other=client({userId:99,homeId:7,target:8});assert.equal((await other.api.getSession()).headquartersId,7);assert(!other.calls.some(x=>x.path.endsWith('/headquarters/8')));});
|
|
test('failed selection retains the previous scope; denied saved selection recovers to home',async()=>{const c=client();await c.api.getSession();await c.api.changeWorkspace(8);c.intercept(p=>p.endsWith('/headquarters/9')?json({message:'denied'},403):null);await assert.rejects(c.api.changeWorkspace(9),/denied/);await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),'8');const restored=client({userId:11,homeId:7,target:8});restored.intercept(p=>p.endsWith('/headquarters/8')?json({message:'revoked'},403):null);const u=await restored.api.getSession();assert.equal(u.headquartersId,7);assert.match(u.workspaceNotice,/revoked/);assert(!restored.entries.has('votian.workspace'));});
|
|
test('a response from an old headquarters cannot enter the next workspace',async()=>{const c=client();await c.api.getSession();await c.api.changeWorkspace(8);const read=deferred();c.intercept(p=>p==='/api/orders'?read.promise:null);const pending=c.api.request('/orders');const rejection=assert.rejects(pending,/beendeten Sitzung/);await c.api.changeWorkspace(7);read.resolve(json({old:true}));await rejection;assert.equal(header(c.calls.find(x=>x.path==='/api/orders')),'8');await c.api.request('/customers');assert.equal(header(c.calls.at(-1)),null);});
|
|
test('switching waits for the full write response and blocks new writes during preflight',async()=>{const c=client();await c.api.getSession();const sent=deferred(),write=deferred();c.intercept(p=>{if(p==='/api/change'){sent.resolve();return write.promise;}});const saving=c.api.put('/change',{value:'synthetic'});await sent.promise;await assert.rejects(c.api.changeWorkspace(8),/Änderung läuft/);write.resolve(json({completed:true}));await saving;const selecting=deferred();c.intercept(p=>p.endsWith('/headquarters/8')?selecting.promise:null);const switching=c.api.changeWorkspace(8);await assert.rejects(c.api.put('/change',{value:'second'}),/Niederlassung wird gewechselt/);selecting.resolve(json({...home,headquartersId:8}));await switching;});
|
|
test('two tabs keep separate contexts and caller headers cannot override the selection',async()=>{const a=client(),b=client();await a.api.getSession();await b.api.getSession();await a.api.changeWorkspace(8);await a.api.request('/customers',{headers:{'X-Workspace-Headquarters':'99','X-Order-Headquarters':'18'}});assert.equal(header(a.calls.at(-1)),'8');assert.equal(a.calls.at(-1).options.headers.get('X-Order-Headquarters'),'18');await b.api.request('/customers');assert.equal(header(b.calls.at(-1)),null);});
|