The parity gate's Cypress job went red after the seed work landed, and the two
causes are the same change seen from the fixture side:
- makedata now carries SAS's demo validation rules for MPE_X_TEST, including the
HARDREGEX on SOME_CHAR that blocks any text but 'the'/'data'. The excel
fixtures upload arbitrary values into that table and expect the submission to
reach the approval screen, so ten excel specs timed out with no Approve
button. createdemodata drops MPE_X_TEST's rules, which is what editor.cy.ts
already documents ("MPE_X_TEST stays clean to match the excel fixtures").
- the LABELs the viewer-labels spec asserts went with the hand-written schema.
createdemodata restores them, because a SAS estate has none and the control
library should not claim otherwise.
Both are declared as fixture divergences in the parity README and asserted in
createdemodata.test.js, so the next change to either side fails a test instead
of a Cypress run.
Review findings:
- the SYSTIMEZONEOFFSET acceptance depended on the host's zone; both entry
points pin TZ=UTC before the harness loads, so the accepted list is the same
everywhere.
- the sas9 mock still published '+0100'; it now answers seconds east of UTC
like the standard estate.
- the README promised an eight-digit id scan that IDENTIFIER_CLASSES no longer
had. A DC-prefixed eight-digit class is back (a plain digit class would fire
on a yyyymmdd date), with a case proving a date is not refused.
188 lines
6.5 KiB
JavaScript
188 lines
6.5 KiB
JavaScript
/**
|
|
* The gate on the parity goldens.
|
|
*
|
|
* A golden is a projection of a SAS response, not a copy of one (see
|
|
* parity/project.js). This test is the second half of that promise: it scans
|
|
* every committed golden for identifier classes, and it proves the projection
|
|
* removes the things a recorder could otherwise carry - a hostname echoed in a
|
|
* payload value, a home path in an error message, a licence key in a globals
|
|
* row.
|
|
*
|
|
* The scan runs here, not only in the recorder, because a golden can also be
|
|
* edited by hand or produced by an older version of the recorder.
|
|
*
|
|
* Run with: node --test sas/mocks/test/
|
|
*/
|
|
const test = require('node:test')
|
|
const assert = require('node:assert')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const {
|
|
ENVELOPE_NEVER,
|
|
checkPublishable,
|
|
project,
|
|
scan,
|
|
secretViolations
|
|
} = require('./parity/project')
|
|
|
|
const GOLDEN = path.join(__dirname, 'parity', 'golden')
|
|
|
|
function goldenFiles(dir) {
|
|
const out = []
|
|
if (!fs.existsSync(dir)) return out
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const full = path.join(dir, entry.name)
|
|
if (entry.isDirectory()) out.push(...goldenFiles(full))
|
|
else if (entry.name.endsWith('.json')) out.push(full)
|
|
}
|
|
return out
|
|
}
|
|
|
|
/** A response shaped like the ones these services return. */
|
|
function syntheticResponse() {
|
|
return {
|
|
SYSDATE: '06NOV25',
|
|
SYSTIME: '14:21',
|
|
SYSVLONG: '9.04.01M9P060425',
|
|
SYSHOSTNAME: 'a-host-internal',
|
|
SYSTCPIPHOSTNAME: 'a-host-internal.example.io',
|
|
SYSSITE: '70189106',
|
|
SYSUSERID: 'someuser',
|
|
MF_GETUSER: 'sysadmin',
|
|
AUTOEXEC: '%2Fhome%2Fsomeuser%2Fsasjs_root%2Fsessions%2F1%2Fautoexec.sas',
|
|
SYSCC: '0',
|
|
SYSENCODING: 'utf-8',
|
|
_DEBUG: '131',
|
|
_PROGRAM: '/Public/app/dc/services/public/startupservice',
|
|
sasdatasets: [{ LIBREF: 'DC634301', DSN: 'MPE_TABLES' }],
|
|
globvars: [
|
|
{
|
|
DCLIB: 'DC634301',
|
|
LICENCE_KEY: 'a-real-licence-key-value',
|
|
ACTIVATION_KEY: 'an-activation-key',
|
|
DC_ADMIN_GROUP: 'all-users'
|
|
}
|
|
],
|
|
notes: [
|
|
{
|
|
MESSAGE:
|
|
'failed reading MPE_TABLES under /home/someuser/sasjs_root at 2026-11-06T14:21:32'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
const MEMBERS = ['sasdatasets', 'globvars', 'notes']
|
|
|
|
test('the projection keeps the contract and drops the estate', () => {
|
|
const raw = syntheticResponse()
|
|
const projected = project(raw, MEMBERS)
|
|
const text = JSON.stringify(projected)
|
|
|
|
assert.deepEqual(Object.keys(projected).sort(), [
|
|
'SYSCC',
|
|
'SYSENCODING',
|
|
'globvars',
|
|
'notes',
|
|
'sasdatasets'
|
|
])
|
|
assert.equal(projected.sasdatasets[0].LIBREF, '<LIBREF>')
|
|
assert.equal(projected.globvars[0].DCLIB, '<LIBREF>')
|
|
assert.equal(projected.globvars[0].DC_ADMIN_GROUP, 'all-users')
|
|
|
|
assert.equal(text.indexOf('a-host-internal'), -1, 'no hostname')
|
|
assert.equal(text.indexOf('someuser'), -1, 'no user or home path')
|
|
assert.equal(text.indexOf('70189106'), -1, 'no site id')
|
|
assert.equal(text.indexOf('DC634301'), -1, 'no real libref')
|
|
assert.match(projected.notes[0].MESSAGE, /<DATETIME>/)
|
|
|
|
assert.deepEqual(scan(projected), {}, 'the scan is clean')
|
|
assert.deepEqual(checkPublishable(projected, raw), [], 'the publish check passes')
|
|
})
|
|
|
|
test('a staging id is replaced whole, not just its libref-shaped head', () => {
|
|
// The staging id and the libref share a prefix. If the libref rule runs
|
|
// first the id comes out as <LIBREF>-1_2_3, which is neither a stable
|
|
// placeholder nor a real identifier - and it is the field that tells two
|
|
// staging calls apart.
|
|
const projected = project(
|
|
{
|
|
SYSCC: '0',
|
|
table_id: 'DC123456-1_2_3',
|
|
staged: 'DC20261106-142132.123_456789_1234',
|
|
libref: 'DC123456',
|
|
note: 'DC_JSLIB and DC634301 are librefs'
|
|
},
|
|
['table_id', 'staged', 'libref', 'note']
|
|
)
|
|
assert.equal(projected.table_id, '<STAGING_ID>')
|
|
assert.equal(projected.staged, '<STAGING_ID>')
|
|
assert.equal(projected.libref, '<LIBREF>')
|
|
assert.equal(projected.note, '<LIBREF> and <LIBREF> are librefs')
|
|
})
|
|
|
|
test('a secret is redacted by key name, not by what it holds', () => {
|
|
const projected = project(syntheticResponse(), MEMBERS)
|
|
assert.equal(projected.globvars[0].LICENCE_KEY, '<REDACTED>')
|
|
assert.equal(projected.globvars[0].ACTIVATION_KEY, '<REDACTED>')
|
|
})
|
|
|
|
test('an identifier echoed in a payload value stops the publish', () => {
|
|
// The case a regex cannot see: the value itself is the identifier, carried in
|
|
// a field that is meant to be published.
|
|
const raw = syntheticResponse()
|
|
raw.notes = [{ MESSAGE: 'error on a-host-internal while reading MPE_TABLES' }]
|
|
const projected = project(raw, MEMBERS)
|
|
// scrub does not know that literal, so it survives - and the deny check is
|
|
// what refuses it.
|
|
assert.match(projected.notes[0].MESSAGE, /a-host-internal/)
|
|
assert.notDeepEqual(
|
|
checkPublishable(projected, raw),
|
|
[],
|
|
'the deny list must reject a value the envelope unmasked'
|
|
)
|
|
})
|
|
|
|
test('a secret typed into a golden by hand is caught', () => {
|
|
// The gate runs over committed files too, not only over fresh projections.
|
|
const edited = {
|
|
_meta: { scenario: 'x' },
|
|
body: { globvars: [{ LICENCE_KEY: 'a-real-key', ACTIVATION_KEY: '<REDACTED>' }] }
|
|
}
|
|
assert.deepEqual(secretViolations(edited), ['body.globvars[0].LICENCE_KEY'])
|
|
})
|
|
|
|
test('the scan refuses a bare eight-digit DC id but not a date', () => {
|
|
// The claim in the README is that the publish scan catches an eight-digit id.
|
|
// It has to be DC-prefixed: a plain eight-digit class fires on a yyyymmdd
|
|
// date or an amount and refuses a clean golden.
|
|
assert.deepEqual(scan({ note: 'row DC12345678 was staged' }), { dcId: 1 })
|
|
assert.deepEqual(scan({ note: 'captured 20261106 at 14:21' }), {})
|
|
})
|
|
|
|
test('every committed golden is clean', () => {
|
|
const files = goldenFiles(GOLDEN)
|
|
assert.ok(files.length > 0, 'at least one golden is recorded')
|
|
for (const file of files) {
|
|
const golden = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
const where = path.relative(GOLDEN, file)
|
|
assert.deepEqual(scan(golden), {}, where + ' carries an identifier')
|
|
assert.deepEqual(
|
|
secretViolations(golden),
|
|
[],
|
|
where + ' carries a secret in a secret-named field'
|
|
)
|
|
const text = JSON.stringify(golden.body)
|
|
for (const key of ENVELOPE_NEVER) {
|
|
assert.equal(
|
|
text.indexOf('"' + key + '"'),
|
|
-1,
|
|
where + ' carries the envelope member ' + key
|
|
)
|
|
}
|
|
assert.equal(golden._meta.estate, 'synthetic', where + ' names its estate')
|
|
assert.ok(golden._meta.sas, where + ' records the SAS version family')
|
|
}
|
|
})
|