303 lines
15 KiB
JavaScript
303 lines
15 KiB
JavaScript
// Runs actual submission code in a JS sandbox; no browser, network or database.
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {harness,response,scopedKey} = require('./helpers/arr_ui_harness.cjs');
|
|
const refusal=()=>response(409,null,'REPLAY_DATE_NOT_AVAILABLE');
|
|
|
|
test('definite unavailable source date clears only the refused intent and permits correction',async()=>{
|
|
const h=harness(()=>refusal());
|
|
await h.submit();
|
|
assert.equal(h.calls.length,1);
|
|
assert.equal(h.state.arrDownloadIntent,null);
|
|
assert.equal(h.storage.has('test'),false);
|
|
assert.equal(h.state.arrDownloadTask.status,'date_unavailable');
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-09-16');
|
|
assert.equal(h.element('#arr-download-button').disabled,true);
|
|
});
|
|
|
|
test('old not_received intent is released only after the explicit source-date refusal',async()=>{
|
|
const h=harness(()=>refusal());
|
|
const intent={request_id:'a'.repeat(32),report_date:'2026-09-16'};
|
|
h.rememberARRIntent(intent);
|
|
h.state.arrDownloadTask={...intent,status:'not_received',can_retry:false};
|
|
await h.submit();
|
|
assert.equal(JSON.parse(h.calls[0].body).request_id,intent.request_id);
|
|
assert.equal(h.state.arrDownloadIntent,null);
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
});
|
|
|
|
test('stale session token refreshes for the same user and retries the exact submission once',async()=>{
|
|
let posts=0;
|
|
const h=harness((url,options)=>{
|
|
if(url==='/api/session') return response(200,{username:'operator',csrf_token:'fresh-fixture'});
|
|
assert.equal(url,'/api/arr-downloads');
|
|
if(++posts===1) return response(403,null,'SESSION_INVALID');
|
|
return response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false});
|
|
});
|
|
await h.submit();
|
|
assert.equal(h.calls.length,3);
|
|
assert.equal(h.calls[1].url,'/api/session');
|
|
assert.equal(h.calls[1].method,'GET');
|
|
assert.equal(h.calls[0].body,h.calls[2].body);
|
|
assert.equal(h.calls[0].csrf,'fixture');
|
|
assert.equal(h.calls[2].csrf,'fresh-fixture');
|
|
assert.equal(h.state.arrDownloadTask.status,'queued');
|
|
assert.equal(h.state.arrDownloadSubmissionError,null);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,JSON.parse(h.calls[0].body).request_id);
|
|
});
|
|
|
|
test('a still-invalid session stops after one retry and keeps the rejection reason and identity',async()=>{
|
|
const h=harness((url,options)=>{
|
|
if(url==='/api/session') return response(200,{username:'operator',csrf_token:'fresh-fixture'});
|
|
return options.method==='POST' ? response(403,null,'SESSION_INVALID') : response(404,null,'ARR_DOWNLOAD_NOT_FOUND');
|
|
});
|
|
await h.submit();
|
|
const posts=h.calls.filter(c=>c.method==='POST');
|
|
assert.equal(posts.length,2);
|
|
assert.equal(posts[0].body,posts[1].body);
|
|
assert.equal(h.calls.filter(c=>c.url==='/api/session').length,1);
|
|
assert.equal(h.state.arrDownloadTask.status,'not_received');
|
|
assert.equal(h.state.arrDownloadTask.submission_error,'SESSION_INVALID');
|
|
assert.match(h.element('#arr-download-status').textContent,/arr_download.submission_rejected/);
|
|
assert.match(h.element('#arr-download-feedback').className,/is-error/);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,JSON.parse(posts[0].body).request_id);
|
|
});
|
|
|
|
for(const [description,sessionResponse] of [
|
|
['requires login',()=>response(401,null,'SESSION_INVALID')],
|
|
['switches user',()=>response(200,{username:'different-user',csrf_token:'fresh-fixture'})],
|
|
['has no valid token',()=>response(200,{username:'operator',csrf_token:''})],
|
|
]) {
|
|
test(`session refresh that ${description} cannot resend the mutation`,async()=>{
|
|
const h=harness((url,options)=>{
|
|
if(url==='/api/session') return sessionResponse();
|
|
return options.method==='POST' ? response(403,null,'SESSION_INVALID') : response(404,null,'ARR_DOWNLOAD_NOT_FOUND');
|
|
});
|
|
await h.submit();
|
|
assert.equal(h.calls.filter(c=>c.method==='POST').length,1);
|
|
assert.equal(h.state.csrf,'fixture');
|
|
assert.equal(h.state.arrDownloadTask.submission_error,'SESSION_INVALID');
|
|
assert.equal(h.state.arrDownloadIntent.request_id,JSON.parse(h.calls[0].body).request_id);
|
|
});
|
|
}
|
|
|
|
test('a different forbidden response is displayed without refreshing or resubmitting',async()=>{
|
|
const h=harness((url,options)=>options.method==='POST'
|
|
? response(403,null,'REPLAY_ORIGIN_REJECTED') : response(404,null,'ARR_DOWNLOAD_NOT_FOUND'));
|
|
await h.submit();
|
|
assert.equal(h.calls.length,2);
|
|
assert.equal(h.calls.some(c=>c.url==='/api/session'),false);
|
|
assert.equal(h.state.arrDownloadTask.submission_error,'REPLAY_ORIGIN_REJECTED');
|
|
assert.equal(h.state.arrDownloadTask.status,'not_received');
|
|
});
|
|
|
|
test('session recovery preserves a price decision body, extra headers and full response envelope',async()=>{
|
|
let posts=0;
|
|
const payload={case_id:'fixture-case',revision:7,real_price:'1500'};
|
|
const envelope={ok:true,data:{revision:8},trace:'fixture-trace'};
|
|
const h=harness((url,options)=>{
|
|
if(url==='/api/session') return response(200,{username:'operator',csrf_token:'fresh-fixture'});
|
|
assert.equal(url,'/api/jobs/fixture-job/review/items/1');
|
|
assert.equal(options.headers.get('X-Fixture'),'retained');
|
|
if(++posts===1) return response(403,null,'SESSION_INVALID');
|
|
return {status:200,ok:true,json:async()=>envelope};
|
|
});
|
|
const result=await h.api('/api/jobs/fixture-job/review/items/1',{
|
|
method:'POST',headers:{'Content-Type':'application/json','X-Fixture':'retained'},
|
|
body:JSON.stringify(payload),returnEnvelope:true,
|
|
});
|
|
assert.deepEqual(result,envelope);
|
|
assert.equal(h.calls[0].body,h.calls[2].body);
|
|
assert.deepEqual(JSON.parse(h.calls[2].body),payload);
|
|
assert.equal(posts,2);
|
|
});
|
|
|
|
test('a server failure remains uncertain without automatic session or mutation retry',async()=>{
|
|
const h=harness((url,options)=>options.method==='POST'
|
|
? response(500,null,'INTERNAL_ERROR') : response(404,null,'ARR_DOWNLOAD_NOT_FOUND'));
|
|
await h.submit();
|
|
assert.equal(h.calls.length,2);
|
|
assert.equal(h.state.arrDownloadSubmissionError,null);
|
|
assert.equal(h.state.arrDownloadTask.submission_error,undefined);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,JSON.parse(h.calls[0].body).request_id);
|
|
});
|
|
|
|
test('lost response and 404 keep original identity for explicit resubmission',async()=>{
|
|
let posts=0;
|
|
const h=harness((url,options)=>{
|
|
if(options.method==='POST') {
|
|
posts++;
|
|
if(posts===1) throw new TypeError('network lost');
|
|
return response(202,{...JSON.parse(options.body),status:'queued',job_id:null,can_retry:false});
|
|
}
|
|
return response(404,null,'ARR_DOWNLOAD_NOT_FOUND');
|
|
});
|
|
await h.submit();
|
|
const first=JSON.parse(h.calls[0].body);
|
|
assert.equal(h.state.arrDownloadTask.status,'not_received');
|
|
assert.equal(h.state.arrDownloadIntent.request_id,first.request_id);
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
h.element('#arr-download-date').value='2026-09-17';
|
|
h.handleARRDownloadDateChange();
|
|
await h.submit();
|
|
assert.deepEqual(JSON.parse(h.calls[2].body),first);
|
|
assert.equal(posts,2);
|
|
assert.equal(h.state.arrDownloadTask.status,'queued');
|
|
assert.equal(h.element('#arr-download-date').value,'2026-09-17');
|
|
});
|
|
|
|
test('other conflict codes never discard an uncertain request',async()=>{
|
|
const h=harness((url,options)=>options.method==='POST'
|
|
? response(409,null,'ARR_DOWNLOAD_DATE_CONFLICT'):response(404,null,'ARR_DOWNLOAD_NOT_FOUND'));
|
|
await h.submit();
|
|
const first=JSON.parse(h.calls[0].body);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,first.request_id);
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
h.element('#arr-download-date').value='2026-09-17';
|
|
h.handleARRDownloadDateChange();
|
|
assert.equal(h.state.arrDownloadIntent.request_id,first.request_id);
|
|
});
|
|
|
|
test('retry endpoint errors retain an existing interrupted task identity',async()=>{
|
|
const h=harness((url,options)=>options.method==='POST'?refusal():response(503,null,'ARR_DOWNLOAD_UNAVAILABLE'));
|
|
const intent={request_id:'b'.repeat(32),report_date:'2026-09-15'};
|
|
h.rememberARRIntent(intent);
|
|
h.state.arrDownloadTask={...intent,status:'interrupted',can_retry:true};
|
|
h.handleARRDownloadDateChange();
|
|
await h.submit();
|
|
assert.equal(h.calls[0].url,`/api/arr-downloads/${intent.request_id}/retry`);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,intent.request_id);
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-09-16');
|
|
});
|
|
|
|
test('selecting a next date during a running task survives polling without submitting or replacing that task',async()=>{
|
|
const h=harness(()=>{throw new Error('No request expected');});
|
|
const task={request_id:'c'.repeat(32),report_date:'2026-09-15',status:'downloading',can_retry:false};
|
|
await h.acceptARRDownloadTask(task,{sync:false});
|
|
assert.equal(h.element('#arr-download-date').disabled,false);
|
|
assert.equal(h.element('#arr-date-toggle').disabled,false);
|
|
h.element('#arr-download-date').value='2026-09-16';
|
|
h.handleARRDownloadDateChange();
|
|
await h.acceptARRDownloadTask({...task,status:'processing'},{sync:false});
|
|
await h.submit();
|
|
assert.equal(h.calls.length,0);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-09-16');
|
|
assert.equal(h.element('#arr-download-button').disabled,true);
|
|
assert.equal(h.state.arrDownloadIntent.request_id,task.request_id);
|
|
assert.equal(JSON.parse(h.storage.get('test')).report_date,'2026-09-15');
|
|
assert.match(h.element('#arr-download-status').textContent,/2026-09-15/);
|
|
});
|
|
|
|
for (const status of ['succeeded','failed']) {
|
|
test(`the next selected date is only submitted explicitly after ${status}`,async()=>{
|
|
const h=harness((url,options)=>response(202,{...JSON.parse(options.body),status:'queued',can_retry:false}));
|
|
const task={request_id:'d'.repeat(32),report_date:'2026-09-15',status:'downloading',can_retry:false};
|
|
await h.acceptARRDownloadTask(task,{sync:false});
|
|
h.element('#arr-download-date').value='2026-09-16';
|
|
h.handleARRDownloadDateChange();
|
|
await h.acceptARRDownloadTask({...task,status,can_retry:status==='failed'},{sync:false});
|
|
assert.equal(h.calls.length,0);
|
|
assert.equal(h.element('#arr-download-button').disabled,false);
|
|
assert.equal(h.element('#arr-download-button').textContent,'arr_download.start');
|
|
await h.submit();
|
|
assert.equal(h.calls.length,1);
|
|
assert.equal(h.calls[0].url,'/api/arr-downloads');
|
|
const submitted=JSON.parse(h.calls[0].body);
|
|
assert.equal(submitted.report_date,'2026-09-16');
|
|
assert.notEqual(submitted.request_id,task.request_id);
|
|
});
|
|
}
|
|
|
|
test('editing before status loads still restores the running task and retains the selected date',async()=>{
|
|
const task={request_id:'e'.repeat(32),report_date:'2026-09-15',status:'downloading',can_retry:false};
|
|
const h=harness(()=>response(200,{context_id:'production',ready:true,default_date:'2026-09-17',latest_task:task}));
|
|
h.handleARRDownloadDateChange();
|
|
await h.initARRDownload();
|
|
assert.equal(h.state.arrDownloadTask.request_id,task.request_id);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-09-16');
|
|
assert.equal(h.element('#arr-download-button').disabled,true);
|
|
assert.equal(h.calls.length,1);
|
|
assert.equal(h.calls[0].method,'GET');
|
|
});
|
|
|
|
test('config is fetched first and only this instance and user may restore an intent',async()=>{
|
|
const intent={request_id:'f'.repeat(32),report_date:'2026-10-07'};
|
|
const h=harness((url)=>{
|
|
if(url==='/api/arr-downloads') {
|
|
assert.equal(h.storageReads.length,0,'identity must be fetched before storage is read');
|
|
return response(200,{context_id:'production',ready:true,default_date:'2026-10-06'});
|
|
}
|
|
assert.equal(url,`/api/arr-downloads/${intent.request_id}`);
|
|
return response(200,{...intent,status:'downloading',can_retry:false});
|
|
});
|
|
h.storage.set(scopedKey(),JSON.stringify(intent));
|
|
h.storage.set(scopedKey('sandbox'),JSON.stringify({...intent,request_id:'a'.repeat(32)}));
|
|
h.storage.set(scopedKey('production','another-user'),JSON.stringify({...intent,request_id:'b'.repeat(32)}));
|
|
h.storage.set('arr:last-download:operator',JSON.stringify({...intent,report_date:'2026-09-15'}));
|
|
await h.initARRDownload();
|
|
assert.deepEqual(h.storageReads,[scopedKey()]);
|
|
assert.equal(h.state.arrDownloadTask.request_id,intent.request_id);
|
|
assert.equal(h.element('#arr-download-date').value,intent.report_date);
|
|
assert.equal(h.calls.every(call=>call.method==='GET'),true);
|
|
});
|
|
|
|
test('legacy or another context cannot replace default date with an old failed task',async()=>{
|
|
const old={request_id:'a'.repeat(32),report_date:'2026-09-15',status:'failed',can_retry:true};
|
|
const h=harness(()=>response(200,{context_id:'production',ready:true,default_date:'2026-10-07',latest_task:old}));
|
|
h.storage.set('arr:last-download:operator',JSON.stringify(old));
|
|
h.storage.set(scopedKey('sandbox'),JSON.stringify(old));
|
|
await h.initARRDownload();
|
|
assert.equal(h.state.arrDownloadTask,null);
|
|
assert.equal(h.state.arrDownloadIntent,null);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-10-07');
|
|
assert.deepEqual(h.storageReads,[scopedKey()]);
|
|
assert.equal(h.calls.length,1);
|
|
});
|
|
|
|
test('config failure does not read any saved request before identity is known',async()=>{
|
|
const h=harness(()=>{throw new TypeError('offline');});
|
|
h.storage.set('arr:last-download:operator',JSON.stringify({request_id:'a'.repeat(32),report_date:'2026-09-15'}));
|
|
await h.initARRDownload();
|
|
assert.equal(h.storageReads.length,0);
|
|
assert.equal(h.state.arrDownloadIntent,null);
|
|
assert.equal(h.state.arrDownloadRestored,false);
|
|
});
|
|
|
|
test('an unavailable instance without an identity stays unavailable without restoring storage',async()=>{
|
|
const h=harness(()=>response(200,{ready:false,context_id:null,default_date:'2026-10-07'}));
|
|
await h.initARRDownload();
|
|
assert.equal(h.storageReads.length,0);
|
|
assert.equal(h.state.arrDownloadReady,false);
|
|
assert.equal(h.state.arrDownloadConfigDisconnected,false);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-10-07');
|
|
assert.equal(h.element('#arr-download-button').disabled,true);
|
|
});
|
|
|
|
test('a running task from config preselects its date and only scoped terminal intent restores later',async()=>{
|
|
const task={request_id:'a'.repeat(32),report_date:'2026-10-07',status:'processing',can_retry:false};
|
|
const h=harness(url=>response(200,url==='/api/arr-downloads'
|
|
? {context_id:'production',ready:true,default_date:'2026-10-06',latest_task:task} : {...task,status:'failed',can_retry:true}));
|
|
await h.initARRDownload();
|
|
assert.equal(h.element('#arr-download-date').value,task.report_date);
|
|
assert.equal(h.element('#arr-download-button').disabled,true);
|
|
await h.initARRDownload();
|
|
assert.equal(h.state.arrDownloadTask.status,'failed');
|
|
assert.equal(h.state.arrDownloadIntent.request_id,task.request_id);
|
|
assert.equal(h.element('#arr-download-date').value,task.report_date);
|
|
});
|
|
|
|
test('changing source context clears the previous in-memory intent before restoring',async()=>{
|
|
let context='sandbox';
|
|
const h=harness(()=>response(200,{context_id:context,ready:true,default_date:'2026-10-07'}));
|
|
await h.initARRDownload();
|
|
h.rememberARRIntent({request_id:'a'.repeat(32),report_date:'2026-09-15'});
|
|
context='production';
|
|
await h.initARRDownload();
|
|
assert.equal(h.state.arrDownloadIntent,null);
|
|
assert.equal(h.state.arrDownloadTask,null);
|
|
assert.equal(h.element('#arr-download-date').value,'2026-10-07');
|
|
assert.equal(h.calls.length,2);
|
|
});
|