fix(mocks): emit the sasjs lineage capture through webout()
Build / Build-and-ng-test (pull_request) Successful in 3m42s
Lighthouse Checks / lighthouse (pull_request) Successful in 8m16s
Build / Build-and-test-development (pull_request) Successful in 29m9s

The capture assigned its body to _webout in one template literal, and that
literal was not JSON: raw line breaks inside the graphviz label and an
unescaped quote in labelloc = "t". fromsas, clickableids, info and flatdata
are members now, written as string literals that keep the label's quotes and
line breaks, with the envelope supplied by the runtime.

sas/mocks/test/sasjs-webout.test.js holds the six services that set their own
content headers, and pins this capture's members. CONTRIBUTING.md lists the
same six.
This commit is contained in:
dc committed 2026-11-06 09:03:01 +00:00
1 parent 67631133f9
commit caa58771bf
3 files changed
+145 -66

No files matched your search

+1 -1
View File
@@ -147,7 +147,7 @@ The app will be at `http://localhost:4200`. It calls the JS mock services on por
### How the mocks work
Each `.js` file in `sas/mocks/sasjs/services/` corresponds to a real SAS service (e.g. `public/startupservice.js` mimics `services/public/startupservice.sas`). The JS runtime predeclares `fs`, `_program` (the program's deployed path), `weboutPath` (the response body it writes) and `_webout`, and the services emit their members through the runtime's `webout()` - `webout('OPEN')`, one `webout('OBJ', name, rows)` (or `webout('ARR', name, rows)` for positional rows) per member, `webout('CLOSE')` - so the response envelope comes from the host and the file carries only what the SAS service returns. A few services still write the body themselves through `_webout`: the ones that emit plain text, CSV, DDL or HTML and set their own content headers (`admin/exportdb`, `admin/makedata`, `public/getrawdata`, `public/getddl`), and the lineage captures. Some mocks are stateful: they persist data to a `mock-storage/` folder on the SASjs Drive (e.g. `licence.json`, `users.json`, `filter.txt`) using the predeclared `fs` module.
Each `.js` file in `sas/mocks/sasjs/services/` corresponds to a real SAS service (e.g. `public/startupservice.js` mimics `services/public/startupservice.sas`). The JS runtime predeclares `fs`, `_program` (the program's deployed path), `weboutPath` (the response body it writes) and `_webout`, and the services emit their members through the runtime's `webout()` - `webout('OPEN')`, one `webout('OBJ', name, rows)` (or `webout('ARR', name, rows)` for positional rows) per member, `webout('CLOSE')` - so the response envelope comes from the host and the file carries only what the SAS service returns. A few services still write the body themselves through `_webout`: the ones that emit plain text, CSV, DDL, HTML or a file download and set their own content headers (`admin/exportconfig`, `admin/exportdb`, `admin/makedata`, `auditors/getauditfile`, `public/getddl`, `public/getrawdata`). `sas/mocks/test/sasjs-webout.test.js` holds that list, so a service that goes back to writing its own body fails the suite. Some mocks are stateful: they persist data to a `mock-storage/` folder on the SASjs Drive (e.g. `licence.json`, `users.json`, `filter.txt`) using the predeclared `fs` module.
The mock data includes sample tables (`DC996664.MPE_X_TEST`, `MPE_X_NEW`, `MPE_X_FORMULA_TEST`, etc.) with realistic columns, validation rules (HARDREGEX, SOFTREGEX, HARDSELECT, READONLY, HIDDEN, ROUND, NUMBER_FORMAT), and dropdown values. This is sufficient to exercise the full editor grid, approval workflow, viewer, and admin screens.
@@ -1,73 +1,30 @@
_webout = `{"SYSDATE" : "26SEP22"
,"SYSTIME" : "08:54"
, "fromsas":
[
/* A fixed sample of what the SAS service returns for this screen.
The members are the same shape; the envelope now comes from the runtime. */
webout('OPEN')
webout('OBJ', 'fromsas', [
{
"STRING": "digraph G { concentrate=true; node [style=filled,shape=plain]; labelloc = "t";"
STRING:
'digraph G { concentrate=true; node [style=filled,shape=plain]; labelloc = "t";'
},
{
"STRING": "label=<<table>
<tr>
<td align='text' colspan='4'>REVERSE Lineage for <b>changed_records</b></td>
</tr>
<tr>
<td align='right'>Library:<br /></td>
<td align='left'>DC_JSLIB</td>
<td align='right'>Generated by:</td>
<td align='left'>sasdemo</td>
</tr>
<tr>
<td align='right'>Table:</td>
<td align='left'>MPE_DATALOADS</td>
<td align='right'>Generated on:</td>
<td align='left'> 26SEP2022:08:54:38</td>
</tr>
</table>>"
STRING:
"label=<<table>\n <tr>\n <td align='text' colspan='4'>REVERSE Lineage for <b>changed_records</b></td>\n </tr>\n <tr>\n <td align='right'>Library:<br /></td>\n <td align='left'>DC_JSLIB</td>\n <td align='right'>Generated by:</td>\n <td align='left'>sasdemo</td>\n </tr>\n <tr>\n <td align='right'>Table:</td>\n <td align='left'>MPE_DATALOADS</td>\n <td align='right'>Generated on:</td>\n <td align='left'> 26SEP2022:08:54:38</td>\n </tr>\n </table>>"
},
{
"STRING": "x [label="No lineage found" shape=Mdiamond]}
STRING: 'x [label="No lineage found" shape=Mdiamond]}'
}
]
, "clickableids":
[
]
, "info":
[
])
webout('OBJ', 'clickableids', [])
webout('OBJ', 'info', [
{
"COLURI": "",
"COLNAME": "changed_records",
"TABURI": "OMSOBJ:PhysicalTable\A59LNVZG.BR00002J",
"TABNAME": "MPE_DATALOADS",
"LIBURI": "OMSOBJ:SASLibrary\A59LNVZG.B500000K",
"LIBREF": "DC_JSLIB",
"RC": 0
COLURI: '',
COLNAME: 'changed_records',
TABURI: 'OMSOBJ:PhysicalTable\\A59LNVZG.BR00002J',
TABNAME: 'MPE_DATALOADS',
LIBURI: 'OMSOBJ:SASLibrary\\A59LNVZG.B500000K',
LIBREF: 'DC_JSLIB',
RC: 0
}
]
, "flatdata":
[
]
,"_DEBUG" : ""
,"_METAUSER": "sasdemo@SAS"
,"_METAPERSON": "sasdemo"
,"_PROGRAM" : "/Projects/app/dc/services/lineage/fetchcollineage"
,"AUTOEXEC" : "D%3A%5Copt%5Csasinside%5CConfig%5CLev1%5CSASApp%5CStoredProcessServer%5Cautoexec.sas"
,"MF_GETUSER" : "sasdemo"
,"SYSCC" : "0"
,"SYSENCODING" : "wlatin1"
,"SYSERRORTEXT" : ""
,"SYSHOSTNAME" : "SAS"
,"SYSPROCESSID" : "41DD8058137FCED940C5250000000000"
,"SYSPROCESSMODE" : "SAS Stored Process Server"
,"SYSPROCESSNAME" : ""
,"SYSJOBID" : "27448"
,"SYSSCPL" : "Linunx"
,"SYSSITE" : "123"
,"SYSUSERID" : "sassrv"
,"SYSVLONG" : "9.04.01M7P080520"
,"SYSWARNINGTEXT" : "ENCODING option ignored for files opened with RECFM=N."
,"END_DTTM" : "2022-09-26T08:54:39.013000"
,"MEMSIZE" : "46GB"
}
`
])
webout('OBJ', 'flatdata', [])
webout('CLOSE')
+122
View File
@@ -0,0 +1,122 @@
/**
* The sasjs mock tree's response shape.
*
* Every service under sas/mocks/sasjs/services emits its members through
* `webout()`, so the response envelope comes from the runtime. A few write the
* whole body themselves with content headers of their own - text, CSV, DDL,
* HTML, file downloads - and are listed here. A service that assigns `_webout`
* and is not on that list has gone back to freezing the envelope at the
* recording, which is what `CONTRIBUTING.md` describes.
*
* 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 vm = require('node:vm')
const SERVICES = path.join(__dirname, '..', 'sasjs', 'services')
/** The services whose response is not a JSON body of members. */
const PLAIN_BODY = [
'admin/exportconfig.js',
'admin/exportdb.js',
'admin/makedata.js',
'auditors/getauditfile.js',
'public/getddl.js',
'public/getrawdata.js'
]
function walk(dir) {
return fs
.readdirSync(dir, { withFileTypes: true })
.flatMap((entry) =>
entry.isDirectory()
? walk(path.join(dir, entry.name))
: [path.join(dir, entry.name)]
)
}
function serviceFiles() {
return walk(SERVICES)
.filter((file) => file.endsWith('.js'))
.sort()
}
/** Runs a service the way the SASjs Server runtime does, keeping its members. */
function runService(file) {
const members = {}
const sandbox = {
webout: (kind, name, rows) => {
if (typeof name === 'string' && rows !== undefined) members[name] = rows
},
require: (request) => {
const spec = String(request)
return spec.startsWith('.')
? require(path.resolve(path.dirname(file), spec))
: require(spec)
},
fs,
_program: path.resolve(file),
weboutPath: path.join(path.dirname(file), 'body.json')
}
try {
vm.runInNewContext(fs.readFileSync(file, 'utf8'), sandbox, {
filename: file
})
} catch (error) {
error.message = `${path.relative(SERVICES, file)}: ${error.message}`
throw error
}
return members
}
test('only the plain-body services assign the response themselves', () => {
const offenders = serviceFiles()
.filter((file) => fs.readFileSync(file, 'utf8').includes('_webout'))
.map((file) => path.relative(SERVICES, file))
assert.deepStrictEqual(offenders, PLAIN_BODY)
})
test('the lineage capture emits the recorded members', () => {
const file = path.join(SERVICES, 'lineage', 'fetchlineage.js')
const members = runService(file)
assert.deepStrictEqual(Object.keys(members), [
'fromsas',
'clickableids',
'info',
'flatdata'
])
assert.strictEqual(members.fromsas.length, 3)
assert.match(members.fromsas[0].STRING, /^digraph G \{/)
assert.match(
members.fromsas[1].STRING,
/REVERSE Lineage for <b>changed_records<\/b>/
)
assert.ok(
members.fromsas[1].STRING.includes("<td align='left'>DC_JSLIB</td>")
)
assert.strictEqual(
members.fromsas[2].STRING,
'x [label="No lineage found" shape=Mdiamond]}'
)
// members come from the sandbox realm, so compare their JSON shape
const emitted = JSON.parse(JSON.stringify(members))
assert.deepStrictEqual(emitted.info, [
{
COLURI: '',
COLNAME: 'changed_records',
TABURI: 'OMSOBJ:PhysicalTable\\A59LNVZG.BR00002J',
TABNAME: 'MPE_DATALOADS',
LIBURI: 'OMSOBJ:SASLibrary\\A59LNVZG.B500000K',
LIBREF: 'DC_JSLIB',
RC: 0
}
])
assert.deepStrictEqual(emitted.clickableids, [])
assert.deepStrictEqual(emitted.flatdata, [])
})