Files
dc/sas/mocks/test/fault-paths.test.js
T
dc bda5dea33e
Build / Build-and-ng-test (pull_request) Successful in 3m10s
Build / lighthouse (pull_request) Successful in 6m50s
Build / Build-and-test-development (pull_request) Successful in 29m28s
fix(mocks): catalogs are not datasets, and getcolvals validates shape before resolution
Reviews 893 and 894 on #518.

894-1 and 894-2 - a -FC request named a format catalog, and the mock answered
it with the sibling dataset. Stripping the suffix for validation made the name
canonical but also changed which object was resolved, so LIBREF.NAME-FC returned
NAME's rows for a name that is not a catalog at all. Now the MPE_TABLES
registration decides: if it carries a row for NAME-FC the member is a catalog
and is streamed as LIBREF.NAME.ext; if it does not, the request is not a catalog
and the estate's own answer is reproduced - syscc=1012 from %mp_cntlout,
recorded as public/getrawdata/catalog_suffix_on_dataset. The catalog the fixture
does hold, MPE_X_CATALOG, has no format entries, so its listing is empty; that
is recorded in the parity README beside the other fixture divergences rather
than left as an unexplained empty body. The test that pinned the object swap now
asserts both halves.

894-3 - getcolvals answered a well-formed libref it does not have with "Invalid
libds", and a malformed one with "Target table not found": the two rules were
the wrong way round. getcolvals.sas validates the SHAPE first with
%mpe_validatecol(...,LIBDS,...) and that is the only "Invalid libds"; the table
is then left to %mp_derivedvars, which aborts "Target table not found: <LIBDS>"
with its own MAC. Both are now recorded: public/getcolvals/wrong_libref uses a
valid-length NOLIB and pins the resolution abort, and the new
public/getcolvals/malformed_libds pins the shape abort with an 11-character
libref. fault-paths.test.js went with them.

893-2 - loadTableFrom duplicated loadTableData, so the two could drift: one
loadTableFile(libref, tableName) now serves both, with the control library as
the default.

893-3 - an unreadable or malformed table file was swallowed into null and
surfaced as "Target table not found". It now throws naming the file, because a
broken fixture should not send the next reader looking for a missing table; an
absent member still returns null and keeps the recorded abort.

Tests: 149 pass. Each of the three behaviour changes was run against the
previous code first and fails there - swallowing parse errors, answering the
catalog with the dataset, and restoring the inverted getcolvals rule.
parity/check.js ok on all 25 scenarios.
2026-11-08 00:48:14 +00:00

165 lines
5.6 KiB
JavaScript

/**
* Two fault paths the mock answered as if nothing was wrong: a DDL download for
* a dataset that is not there, and a dropdown for a table or a library that is
* not there. All are recorded against the estate in `parity/scenarios.json`;
* these are the same facts as unit tests, so a regression names the rule.
*/
const test = require('node:test')
const assert = require('node:assert')
const fs = require('node:fs')
const { seededEstate, runService } = require('./harness')
/**
* The header file is written with fs.writeFile, so it lands a tick after the
* program returns. Poll for the line rather than sleep a guessed interval: a
* loaded runner can take longer than any number a test would pick.
*/
async function headersWhen(estate, pattern) {
const deadline = Date.now() + 2000
let headers = ''
while (Date.now() < deadline) {
headers = fs.existsSync(estate.headersPath)
? fs.readFileSync(estate.headersPath, 'utf8')
: ''
if (pattern.test(headers)) return headers
await new Promise((resolve) => setImmediate(resolve))
}
return headers
}
test('getddl aborts when the dataset is not there', () => {
const estate = seededEstate({ demo: false })
const body = JSON.parse(
runService(estate, 'public/getddl', {
params: { libref: 'DC_JSLIB', ds: 'NO_SUCH_DS', flavour: 'SAS' }
})
)
// getddl.sas names both parts, and %mp_abort(mac=&_program) reports the
// service rather than a macro
assert.deepEqual(body.sasjsAbort, [
{
MSG: 'Dataset DC_JSLIB.NO_SUCH_DS was not found',
MAC: '/Public/app/dc/services/public/getddl'
}
])
// an abort streams no file: the body is the abort, not DDL text
assert.equal(body.text, undefined)
})
test('getddl names the file after the member it is for', async () => {
const estate = seededEstate({ demo: false })
runService(estate, 'public/getddl', {
params: { libref: 'DC_JSLIB', ds: 'MPE_TABLES', flavour: 'SAS' }
})
// mp_streamfile(contenttype=TEXT) sends an application/text attachment, and
// names it without quoting - the estate's own response headers are
// `content-type: application/text` and
// `content-disposition: attachment; filename=DC634301_MPE_TABLES.ddl`
const pattern = /content-disposition: attachment; filename=DC_JSLIB_MPE_TABLES\.ddl/i
const headers = await headersWhen(estate, pattern)
assert.match(headers, pattern)
assert.match(headers, /content-type: application\/text/i)
assert.doesNotMatch(headers, /application\/octet-stream/i)
})
test('getddl answers an unknown flavour the way the estate does', async () => {
const estate = seededEstate({ demo: false })
const params = { libref: 'DC_JSLIB', ds: 'MPE_TABLES', flavour: 'XXYY' }
const body = runService(estate, 'public/getddl', { params })
/* getddl.sas passes the flavour straight to mp_ds2ddl, which has branches for
SAS, TSQL and PGSQL only: anything else leaves the file holding its opening
comment, and mp_streamfile sends it. So the answer is a download, not an
abort - and the estate's own response for an unknown flavour is a 200 whose
body is that one line. */
assert.doesNotMatch(body, /sasjsAbort/)
assert.match(body, /^\/\* DDL generated by \S+ on +\S+ \*\/\n$/)
// the headers are still those of a download, written after the body
const headers = await headersWhen(
estate,
/content-disposition: attachment; filename=DC_JSLIB_MPE_TABLES\.ddl/i
)
assert.match(headers, /content-type: application\/text/i)
})
test('getcolvals aborts when the table is not there', () => {
const estate = seededEstate({ demo: false })
const body = JSON.parse(
runService(estate, 'public/getcolvals', {
tableName: 'iwant',
rows: [
{
libds: 'DC_JSLIB.NO_SUCH_TABLE',
col: 'RULE_TYPE',
STARTROW: 1,
ROWS: 50
}
]
})
)
// the abort comes from mp_derivedvars, so the MAC is the macro's default;
// a dropdown for a table that has gone is an error, not one blank value
assert.deepEqual(body.sasjsAbort, [
{ MSG: 'Target table not found: DC_JSLIB.NO_SUCH_TABLE', MAC: 'MP_ABORT' }
])
assert.equal(body.vals, undefined)
})
test('getcolvals refuses a malformed libds before it looks anything up', () => {
const estate = seededEstate({ demo: false })
const body = JSON.parse(
runService(estate, 'public/getcolvals', {
tableName: 'iwant',
rows: [
{
// an 11-character libref: %mpe_validatecol's LIBDS rule is shape only
libds: 'NO_SUCH_LIB.MPE_TABLES',
col: 'RULE_TYPE',
STARTROW: 1,
ROWS: 5
}
]
})
)
// the shape abort is the service's own, from getcolvals.sas:124-127
assert.deepEqual(body.sasjsAbort, [
{
MSG: 'Invalid libds',
MAC: '/Public/app/dc/services/public/getcolvals.sas'
}
])
assert.equal(body.vals, undefined)
})
test('getcolvals aborts a well-formed library it does not have as a missing table', () => {
const estate = seededEstate({ demo: false })
const body = JSON.parse(
runService(estate, 'public/getcolvals', {
tableName: 'iwant',
rows: [
{
// NOLIB passes the shape rule, so this is a resolution failure
libds: 'NOLIB.MPE_TABLES',
col: 'RULE_TYPE',
STARTROW: 1,
ROWS: 5
}
]
})
)
/* The estate assigns the library, then %mp_derivedvars aborts, so the MAC is
the macro's default rather than the service - recorded as the golden for
public/getcolvals/wrong_libref. */
assert.deepEqual(body.sasjsAbort, [
{ MSG: 'Target table not found: NOLIB.MPE_TABLES', MAC: 'MP_ABORT' }
])
assert.equal(body.vals, undefined)
})