public/getddl answered one fixed Postgres-flavoured body for every request - DC_JSLIB.MPE_X_TEST whatever dataset was asked for - which is the last read path the client calls where the mock invented its answer. It now builds the SAS-flavour DDL from the table file, the way mp_ds2ddl builds it from dictionary.columns and dictionary.indexes: the header, the create table with each column's type, format, NOT NULL and label, and the unique index. The list-output spacing was derived from the estate's own answers and reproduces them byte for byte for all forty control tables. Recorded as public/getddl/mpe_tables, so a mock change that alters the text fails the gate. The header credits a user and a timestamp, so the projection masks both - a user id is not something to publish, and the comparison is about the DDL. TSQL and PGSQL are not generated yet: a caller asking for one gets the SAS text, and the README says so rather than inventing a second dialect without a recording to check it against.
260 lines
8.5 KiB
JavaScript
260 lines
8.5 KiB
JavaScript
/**
|
|
* createdemodata carries the estate the Cypress suite needs but a Data
|
|
* Controller install does not have: the TESTDATA demo library, the deep and
|
|
* wide viewer fixtures and the multi-load library.
|
|
*
|
|
* It runs after makedata, so it must both add those and leave the control
|
|
* library alone otherwise; and it must be safe to run twice, because a reseed
|
|
* runs it twice on the same drive.
|
|
*
|
|
* 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 { seededEstate, runService, readTable } = require('./harness')
|
|
|
|
const FIXTURE_LIBS = ['TESTDATA', 'DC996664']
|
|
|
|
test('the fixtures are added to a makedata estate', () => {
|
|
const estate = seededEstate()
|
|
const data = path.join(estate.appDir, 'data')
|
|
assert.deepEqual(fs.readdirSync(data).sort(), [
|
|
'DC996664',
|
|
'DC_JSLIB',
|
|
'TESTDATA'
|
|
])
|
|
|
|
const tables = readTable(estate, 'MPE_TABLES')
|
|
for (const lib of FIXTURE_LIBS) {
|
|
assert.ok(
|
|
tables.some((row) => String(row.libref).toUpperCase() === lib),
|
|
lib + ' is registered in MPE_TABLES'
|
|
)
|
|
}
|
|
for (const name of ['MPE_X_SEARCH', 'MPE_X_WIDE']) {
|
|
assert.ok(
|
|
tables.some((row) => String(row.dsn).toUpperCase() === name),
|
|
name + ' is registered in MPE_TABLES'
|
|
)
|
|
}
|
|
|
|
assert.equal(readTable(estate, 'MPE_X_SEARCH').length, 1000)
|
|
assert.equal(readTable(estate, 'MPE_X_WIDE').length, 200)
|
|
assert.ok(readTable(estate, 'MPE_DERIVED').length > 0)
|
|
assert.ok(
|
|
readTable(estate, 'MPE_VALIDATIONS').some(
|
|
(row) => String(row.base_lib).toUpperCase() === 'TESTDATA'
|
|
),
|
|
'the TESTDATA rules are present'
|
|
)
|
|
assert.ok(
|
|
fs.existsSync(path.join(data, 'TESTDATA', 'demo_01.json')),
|
|
'the TESTDATA tables are written'
|
|
)
|
|
})
|
|
|
|
test('MPE_X_TEST carries the excel/viewer fixtures, not the SAS demo rules', () => {
|
|
// makedata brings SAS's demo rules for MPE_X_TEST (a HARDREGEX on SOME_CHAR
|
|
// among them), which block the excel fixtures' arbitrary values before they
|
|
// reach the approval screen. The rules are dropped here and the LABELs the
|
|
// viewer-labels spec asserts are added; both are fixture decisions.
|
|
const estate = seededEstate()
|
|
const rules = readTable(estate, 'MPE_VALIDATIONS').filter(
|
|
(row) => String(row.base_ds).toUpperCase() === 'MPE_X_TEST'
|
|
)
|
|
// SAS's other demo rules for the table are the ones the mock has always had
|
|
// (the editor specs click a cell and expect a dropdown because of them), so
|
|
// only the SOME_SHORTNUM HARDREGEX is dropped - the excel fixtures hold 3,
|
|
// 18, 34 ... which it rejects.
|
|
const ruleTypes = rules.map((row) => row.base_col + ':' + row.rule_type)
|
|
assert.ok(
|
|
ruleTypes.includes('SOME_NUM:HARDSELECT_HOOK'),
|
|
'the select hook the editor specs rely on stays'
|
|
)
|
|
assert.ok(
|
|
!ruleTypes.some((rule) => rule === 'SOME_SHORTNUM:HARDREGEX'),
|
|
'the rule the excel fixtures cannot satisfy is gone'
|
|
)
|
|
|
|
const table = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(estate.appDir, 'data', 'DC_JSLIB', 'mpe_x_test.json'),
|
|
'utf8'
|
|
)
|
|
)
|
|
const byName = {}
|
|
// the table keeps the case the SAS model creates it with, which is lower
|
|
for (const column of table.columns) byName[column.name.toUpperCase()] = column.label
|
|
assert.equal(byName.SOME_CHAR, 'Some Character Column')
|
|
assert.equal(byName.SOME_DATE, 'Some Date')
|
|
assert.equal(byName.SOME_NUM, '', 'a blank label falls back to the name')
|
|
})
|
|
|
|
test('every fixture table is written with its column metadata', () => {
|
|
// saveTable falls back to the columns of the file already on the drive, and
|
|
// a fresh estate has none - so a fixture with no schema of its own is
|
|
// written with columns: [] and the viewer loses the order it renders.
|
|
const estate = seededEstate()
|
|
const tables = [
|
|
['DC_JSLIB', 'MPE_X_SEARCH'],
|
|
['DC_JSLIB', 'MPE_X_WIDE'],
|
|
['TESTDATA', 'MPE_X_NEW'],
|
|
['TESTDATA', 'DEMO_01'],
|
|
['DC996664', 'MPE_X_TEST']
|
|
]
|
|
for (const [lib, name] of tables) {
|
|
const file = path.join(
|
|
estate.appDir,
|
|
'data',
|
|
lib,
|
|
name.toLowerCase() + '.json'
|
|
)
|
|
const table = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
assert.ok(table.columns.length > 0, lib + '.' + name + ' has no columns')
|
|
const described = table.columns.map((column) => column.name.toUpperCase())
|
|
for (const field of Object.keys(table.rows[0] || {})) {
|
|
assert.ok(
|
|
described.includes(field.toUpperCase()),
|
|
lib + '.' + name + '.' + field + ' has no column metadata'
|
|
)
|
|
}
|
|
}
|
|
|
|
const search = JSON.parse(
|
|
fs.readFileSync(
|
|
path.join(estate.appDir, 'data', 'DC_JSLIB', 'mpe_x_search.json'),
|
|
'utf8'
|
|
)
|
|
)
|
|
assert.deepEqual(
|
|
search.columns.map((column) => column.name),
|
|
[
|
|
'PRIMARY_KEY_FIELD',
|
|
'SITE_NAME',
|
|
'VESSEL',
|
|
'SAMPLE_COUNT',
|
|
'DEPTH_M',
|
|
'SPECIES',
|
|
'CRUISE_DATE',
|
|
'EXPEDITION_ID',
|
|
'NOTES'
|
|
],
|
|
'the demo column order the search spec derives its counts from'
|
|
)
|
|
})
|
|
|
|
test('a second run does not accumulate rows', () => {
|
|
const estate = seededEstate()
|
|
const counted = () =>
|
|
[
|
|
'MPE_TABLES',
|
|
'MPE_VALIDATIONS',
|
|
'MPE_SELECTBOX',
|
|
'MPE_SECURITY',
|
|
'MPE_DERIVED'
|
|
]
|
|
.map((name) => name + '=' + readTable(estate, name).length)
|
|
.join(' ')
|
|
|
|
const first = counted()
|
|
runService(estate, 'admin/createdemodata', {})
|
|
assert.equal(counted(), first, 'createdemodata must be idempotent')
|
|
})
|
|
|
|
test('a derived row makedata owns survives the fixture rows', () => {
|
|
// MPE_DERIVED is a control table: createdemodata must add its TESTDATA rows
|
|
// without replacing what makedata wrote, or the first real derived rule on
|
|
// the estate would vanish on the next reseed.
|
|
const { writeTable } = require('./harness')
|
|
const estate = seededEstate()
|
|
const owned = {
|
|
tx_from: 0,
|
|
tx_to: 127490111999,
|
|
libref: 'DC_JSLIB',
|
|
dsn: 'MPE_X_TEST',
|
|
var_nm: 'MOCK_OWNED',
|
|
var_logic: 'SOME_NUM * 2',
|
|
var_type: 'N',
|
|
active: 1
|
|
}
|
|
writeTable(estate, 'MPE_DERIVED', readTable(estate, 'MPE_DERIVED').concat([owned]))
|
|
|
|
runService(estate, 'admin/createdemodata', {})
|
|
|
|
const rows = readTable(estate, 'MPE_DERIVED')
|
|
assert.ok(
|
|
rows.some((row) => row.var_nm === 'MOCK_OWNED'),
|
|
'the makedata-owned row is kept'
|
|
)
|
|
assert.equal(
|
|
rows.filter((row) => row.var_nm === 'MOCK_OWNED').length,
|
|
1,
|
|
'and it is not duplicated'
|
|
)
|
|
assert.ok(
|
|
rows.some((row) => String(row.libref).toUpperCase() === 'TESTDATA'),
|
|
'the fixture rows are still added'
|
|
)
|
|
})
|
|
|
|
test('it refuses to run before makedata', () => {
|
|
const { makeEstate } = require('./harness')
|
|
const estate = makeEstate()
|
|
assert.throws(
|
|
() => runService(estate, 'admin/createdemodata', {}),
|
|
/run services\/admin\/makedata first/,
|
|
'a missing control library is reported, not half-created'
|
|
)
|
|
})
|
|
|
|
test('it seeds into the library the settings name', () => {
|
|
// a deployment that moves the control library writes the new libref into
|
|
// settings.js; the service has to follow it rather than assume DC_JSLIB
|
|
const estate = seededEstate({ demo: false })
|
|
fs.renameSync(
|
|
path.join(estate.appDir, 'data', 'DC_JSLIB'),
|
|
path.join(estate.appDir, 'data', 'TESTFIX')
|
|
)
|
|
fs.writeFileSync(
|
|
path.join(estate.drive, 'files', 'Public', 'app', 'dc', 'services', 'settings.js'),
|
|
'var dcSettings = ' +
|
|
JSON.stringify({
|
|
dc_libref: 'TESTFIX',
|
|
dc_admin_group: 'AllUsers',
|
|
dcpath: 'x'
|
|
}) +
|
|
'\n'
|
|
)
|
|
|
|
runService(estate, 'admin/createdemodata', {})
|
|
|
|
assert.ok(
|
|
fs.existsSync(path.join(estate.appDir, 'data', 'TESTFIX', 'mpe_x_search.json')),
|
|
'the fixtures land in the control library the settings name'
|
|
)
|
|
assert.ok(
|
|
!fs.existsSync(path.join(estate.appDir, 'data', 'DC_JSLIB')),
|
|
'and not in the default one'
|
|
)
|
|
})
|
|
|
|
test('it still seeds when the settings parser is not deployed', () => {
|
|
// a drive whose settings parser has not been compiled is still a drive
|
|
// makedata seeded: the control library falls back to its default name rather
|
|
// than the service failing at load with an ENOENT before it can say anything
|
|
const estate = seededEstate({ demo: false })
|
|
fs.unlinkSync(
|
|
path.join(estate.drive, 'files', 'Public', 'app', 'dc', 'services', 'dcSettings.js')
|
|
)
|
|
|
|
const out = runService(estate, 'admin/createdemodata', {})
|
|
assert.ok(!/ENOENT/.test(out))
|
|
assert.ok(
|
|
fs.existsSync(path.join(estate.appDir, 'data', 'DC_JSLIB', 'mpe_x_search.json')),
|
|
'the fixtures land in the default control library'
|
|
)
|
|
})
|