fix(mocks): refuse a library getcols does not have, as the estate does #512
No files matched your search
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
Reference in new issue
Block a user