fix(mocks): emit the sasjs lineage capture through webout()
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:
1 parent
67631133f9
commit
caa58771bf
3 files changed
+148
-69
No files matched your search
+1
-1
@@ -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":
|
||||
[
|
||||
{
|
||||
"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>>"
|
||||
/* 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": "x [label="No lineage found" shape=Mdiamond]}
|
||||
}
|
||||
]
|
||||
, "clickableids":
|
||||
[
|
||||
|
||||
]
|
||||
, "info":
|
||||
[
|
||||
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>>"
|
||||
},
|
||||
{
|
||||
"COLURI": "",
|
||||
"COLNAME": "changed_records",
|
||||
"TABURI": "OMSOBJ:PhysicalTable\A59LNVZG.BR00002J",
|
||||
"TABNAME": "MPE_DATALOADS",
|
||||
"LIBURI": "OMSOBJ:SASLibrary\A59LNVZG.B500000K",
|
||||
"LIBREF": "DC_JSLIB",
|
||||
"RC": 0
|
||||
STRING: 'x [label="No lineage found" shape=Mdiamond]}'
|
||||
}
|
||||
]
|
||||
, "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', '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
|
||||
}
|
||||
`
|
||||
])
|
||||
webout('OBJ', 'flatdata', [])
|
||||
webout('CLOSE')
|
||||
@@ -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, [])
|
||||
})
|
||||
Reference in new issue
Block a user