Files
dc/sas/mocks/test/locks.test.js
T
dc ca602082ff
Build / Build-and-ng-test (pull_request) Successful in 5m43s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m58s
Build / Build-and-test-development (pull_request) Successful in 29m42s
fix(mocks): abort like mp_abort, lock like mp_lockanytable, derive saslibs
Three more places where the mock answered differently from the SAS service.

mp_abort (dcMockUtils): every mock refusal is now a sasjsAbort table
(MSG + MAC) written where the macro writes it, so the client shows the
abort modal and rejects the promise. A thrown error had become an HTTP
500 "Job execution failed" - the browser host discards the webout when a
program throws - which is unreadable to the user and hides the message in
a stack. mpAbort also stops the program, so nothing is written after a
refusal; each service that can abort catches that stop at its top level.
stagedata, loadfile, getauditfile and makedata now go through it, and the
hand-rolled sasjsAbort blocks are gone.

mp_lockanytable (dcMockUtils): a lock is a row in MPE_LOCKANYTABLE marked
LOCKED carrying the holder's job id, user and reference, exactly as the
macro records it, and a lock held by another session refuses the write
with the macro's message. postdata and rejection now lock MPE_REVIEW and
MPE_SUBMIT around each write, which is where postdata.sas and
rejection.sas take them. The macro's retry loop is collapsed to a single
attempt: the runtime is single threaded, so nothing can release a lock
while the request waits.

startupservice: saslibs is the distinct LIBREF of the permitted datasets,
which is what "create table saslibs as select distinct libref from
&syslast" produces - it was a four-column row describing the control
library, with fields the client does not read.

Tests: abort.test.js and locks.test.js join the harness suites; 25 pass,
and 9 of the 13 relevant cases fail against the base services.
2026-11-04 15:37:31 +00:00

123 lines
3.6 KiB
JavaScript

/**
* mp_lockanytable, exercised through the service that uses it.
*
* The SAS rejection/postdata services lock MPE_REVIEW and MPE_SUBMIT through
* MPE_LOCKANYTABLE around each write, so two reviewers cannot append to the
* same control tables at once. The mock wrote them straight through, leaving
* the lock table empty and the guarantee unimplemented.
*
* Run with: node --test sas/mocks/test/
*/
const test = require('node:test')
const assert = require('node:assert')
const { seededEstate, runService, readTable, writeTable } = require('./harness')
const TABLE_ID = 'DC20261104-120000.000_000001_4242'
/** A submission waiting for review, and an empty lock table. */
function withSubmission(estate) {
writeTable(estate, 'MPE_SUBMIT', [
{
TABLE_ID: TABLE_ID,
BASE_LIB: 'TESTDATA',
BASE_DS: 'DEMO_01',
SUBMIT_STATUS_CD: 'SUBMITTED',
NUM_OF_APPROVALS_REQUIRED: 1,
NUM_OF_APPROVALS_REMAINING: 1
}
])
writeTable(estate, 'MPE_LOCKANYTABLE', [])
}
function reject(estate) {
return JSON.parse(
runService(estate, 'approvers/rejection', {
tableName: 'BrowserParams',
rows: [{ TABLE: TABLE_ID, STP_REASON: 'values look wrong' }]
})
)
}
test('a rejection locks and unlocks the tables it writes', () => {
const estate = seededEstate()
withSubmission(estate)
const body = reject(estate)
assert.equal(body.fromsas[0].RESPONSE, 'SUCCESS!')
assert.equal(readTable(estate, 'MPE_SUBMIT')[0].SUBMIT_STATUS_CD, 'REJECTED')
assert.equal(readTable(estate, 'MPE_REVIEW').length, 1)
const locks = readTable(estate, 'MPE_LOCKANYTABLE')
assert.deepEqual(
locks.map((lock) => lock.LOCK_LIB + '.' + lock.LOCK_DS).sort(),
['DC_JSLIB.MPE_REVIEW', 'DC_JSLIB.MPE_SUBMIT'],
'one lock row per table the service writes'
)
assert.ok(
locks.every((lock) => lock.LOCK_STATUS_CD === 'UNLOCKED'),
'left unlocked - the lock is released after each write'
)
assert.ok(
locks.every((lock) => lock.LOCK_REF && lock.LOCK_USER_NM && lock.LOCK_PID),
'the row says who took it and why, as the macro records it'
)
})
test('a lock held by another session refuses the write', () => {
const estate = seededEstate()
withSubmission(estate)
writeTable(estate, 'MPE_LOCKANYTABLE', [
{
LOCK_LIB: 'DC_JSLIB',
LOCK_DS: 'MPE_SUBMIT',
LOCK_STATUS_CD: 'LOCKED',
LOCK_PID: 'another-session',
LOCK_USER_NM: 'someone-else',
LOCK_REF: 'still approving'
}
])
const body = reject(estate)
assert.deepEqual(body.sasjsAbort, [
{
MSG:
'Unable to lock DC_JSLIB..MPE_SUBMIT via MPE_LOCKANYTABLE after 25 ' +
'attempts.\nPlease ask your administrator to investigate!',
MAC: 'mp_lockanytable'
}
])
assert.equal(
readTable(estate, 'MPE_SUBMIT')[0].SUBMIT_STATUS_CD,
'SUBMITTED',
'the submission is untouched when the lock cannot be taken'
)
assert.equal(readTable(estate, 'MPE_REVIEW').length, 0)
})
test('the lock is released only by the session that took it', () => {
const estate = seededEstate()
withSubmission(estate)
writeTable(estate, 'MPE_LOCKANYTABLE', [
{
LOCK_LIB: 'DC_JSLIB',
LOCK_DS: 'MPE_SUBMIT',
LOCK_STATUS_CD: 'LOCKED',
LOCK_PID: 'another-session',
LOCK_USER_NM: 'someone-else',
LOCK_REF: 'still approving'
}
])
// the refusal must not have taken the other session's lock away
reject(estate)
const held = readTable(estate, 'MPE_LOCKANYTABLE').find(
(lock) => lock.LOCK_DS === 'MPE_SUBMIT'
)
assert.equal(held.LOCK_STATUS_CD, 'LOCKED')
assert.equal(held.LOCK_PID, 'another-session')
})