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.
165 lines
5.6 KiB
JavaScript
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)
|
|
})
|