fix(mocks): refuse a library getcols does not have, as the estate does #512

Merged
allan merged 2 commits from fix/mock-getcols-libref into release/7.23 2026-11-07 20:14:52 +00:00
5 changed files with 125 additions and 23 deletions

No files matched your search

+18 -12
View File
@@ -29,20 +29,26 @@ if (libds) {
}
// ─── Load the table schema ────────────────────────────────────────────────────
// Same resolution as getdata.js: the requested libref first, then any libref
// (a LIBDS that doesn't match the data folder still resolves).
/*
getcols.sas validates LIBDS against the libraries it can assign before it
reads anything (%mpe_validatecol, then dc_assignlib), so a LIBDS naming a
library the estate does not have aborts "Invalid libds" - it does not go
looking for that member somewhere else. The mock used to search every libref,
which answered for a request the estate refuses, and a missing or malformed
LIBDS arrives at the same abort.
let tableData = table ? makeTableLoader(libDataDir(libref))(table) : null
if (!tableData) {
// Fall back to searching every libref if the table isn't in the requested
// libref (e.g. a LIBDS that doesn't match the actual data folder), and take
// the libref that actually held it for the registry lookup below.
const found = loadTableAnyLib(table)
if (found) {
tableData = found.data
libref = found.libref
}
A member missing from a library that does exist is a different path: the
estate fails inside its query, and there is no message there worth copying.
*/
const knownLib = libds
? allLibrefs().find(lib => lib.toUpperCase() === libref.toUpperCase())
: null
if (!libds || !table || !knownLib) {
mpAbort('Invalid libds', _program + '.sas')
}
libref = knownLib
const tableData = makeTableLoader(libDataDir(libref))(table) || null
// ─── Build the column list ────────────────────────────────────────────────────
// Every column, as getdata.sas emits them: the temporal columns are dropped from
+7 -11
View File
@@ -114,17 +114,13 @@ function runService(
*/
estate.sessionCount = (estate.sessionCount || 1) + 1
const sessionDir = path.join(estate.root, 'sessions', 's' + estate.sessionCount)
// a run's directory is only read back through estate.weboutPath, which moves
// to the next run, so the one before it is removed rather than left behind -
// a test file that runs many services would otherwise leave hundreds
const sessionsRoot = path.join(estate.root, 'sessions')
if (fs.existsSync(sessionsRoot)) {
for (const entry of fs.readdirSync(sessionsRoot)) {
if (entry !== 's' + estate.sessionCount) {
fs.rmSync(path.join(sessionsRoot, entry), { recursive: true, force: true })
}
}
}
/* Earlier sessions are left in place. Removing them pruned the estate, but a
service that writes its headers with the async fs.writeFile lands after the
program returns - so the prune could delete the directory under a pending
write and the callback's error took the test process down with it (an
intermittent ENOENT on stpsrv_header.txt). Each run still gets its own
directory, which is what keeps a stale body from being served; the whole
estate is removed when the process exits. */
fs.mkdirSync(sessionDir, { recursive: true })
const runWeboutPath = path.join(sessionDir, 'webout.txt')
const runHeadersPath = path.join(sessionDir, 'stpsrv_header.txt')
@@ -0,0 +1,24 @@
{
"_meta": {
"scenario": "wrong_libref",
"service": "public/getcols",
"estate": "synthetic",
"targetType": "SASJS",
"sas": "9.4M9",
"dc": "7.22.0",
"captured": "2026-11-07",
"tls": "verified",
"members": [
"sasjsAbort"
]
},
"body": {
"sasjsAbort": [
{
"MSG": "Invalid libds",
"MAC": "/Public/app/dc/services/public/getcols.sas"
}
],
"SYSCC": "0"
}
}
+31
View File
@@ -559,6 +559,37 @@
],
"note": "a temporal column formatted DATETIME. - no width in the name, so the format's own default (16) is the width, not the character fallback",
"accepted": []
},
{
"id": "wrong_libref",
"service": "public/getcols",
"params": {},
"input": {
"table": "iwant",
"rows": [
{
"LIBDS": "NO_SUCH_LIB.MPE_TABLES"
}
]
},
"mockInput": {
"table": "iwant",
"rows": [
{
"LIBDS": "NO_SUCH_LIB.MPE_TABLES"
}
]
},
"members": [
"sasjsAbort"
],
"note": "a column list for a library the estate does not have: the library is validated before the member is looked for, so this aborts instead of answering from another library's copy",
"accepted": [
{
"path": ".SYSCC",
"reason": "the SAS estate's wrapper adds SYSCC to an aborted response; a JS program's envelope is finalised at CLOSE, and mp_abort stops before it, so the mock's abort carries none. The message and the macro name - what the client renders - do match"
}
]
}
]
}
+45
View File
@@ -97,3 +97,48 @@ test('a download carries a header row, drops the validity columns and keeps a ne
'a value with a line break is quoted'
)
})
test('getcols refuses a library the estate does not have, instead of searching', () => {
const estate = seededEstate({ demo: false })
// getcols.sas validates LIBDS before it reads: a library it cannot assign is
// "Invalid libds", even when the member exists in another library - the mock
// used to find MPE_TABLES in DC_JSLIB and answer for it
const wrongLib = JSON.parse(
runService(estate, 'public/getcols', {
tableName: 'iwant',
rows: [{ LIBDS: 'NO_SUCH_LIB.MPE_TABLES' }]
})
)
assert.deepEqual(wrongLib.sasjsAbort, [
{
MSG: 'Invalid libds',
MAC: '/Public/app/dc/services/public/getcols.sas'
}
])
assert.equal(wrongLib.cols, undefined)
// a LIBDS with no library part arrives at the same abort
const malformed = JSON.parse(
runService(estate, 'public/getcols', {
tableName: 'iwant',
rows: [{ LIBDS: 'MPE_TABLES' }]
})
)
assert.deepEqual(malformed.sasjsAbort, [
{
MSG: 'Invalid libds',
MAC: '/Public/app/dc/services/public/getcols.sas'
}
])
// and the library the estate does have still answers
const found = JSON.parse(
runService(estate, 'public/getcols', {
tableName: 'iwant',
rows: [{ LIBDS: 'DC_JSLIB.MPE_TABLES' }]
})
)
assert.equal(found.sasjsAbort, undefined)
assert.ok(found.cols.length > 0)
})