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.
123 lines
3.6 KiB
JavaScript
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')
|
|
})
|