fix(mocks): address the audit-pack review findings
Sites / deploy (push) Successful in 3m52s
Build / Build-and-ng-test (pull_request) Successful in 5m27s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m5s
Build / Build-and-test-development (pull_request) Successful in 33m8s

- zip entries carry flag bit 11 when the name is non-ASCII, so an extractor
  reads `BKP_` plus an accented filename back by name instead of CP437
- loadfile and stagedata name the staged CSV after the libds the request
  arrived with, before a hook can re-point the submission
- stagedata writes that CSV at all (%mp_ds2csv does), so a grid submission's
  audit pack carries the same artefact as an upload's
- loadfile writes no BKP_ file: loadfile.sas copies one only for a second,
  non-CSV fileref, which the mock does not accept
- getauditfile says access is unenforced rather than that there is no
  permission store (MPE_SECURITY is seeded)
- the special-missings clip no longer claims the estate carries a valid key
This commit is contained in:
dc committed 2026-10-24 20:06:09 +00:00
1 parent 0b7cf6a724
commit 489819b051
5 files changed
+45 -13

No files matched your search

@@ -40,8 +40,8 @@ context('special missings clip (DEMO_01)', function () {
this.beforeEach(() => {
cy.visit(hostUrl + appLocation)
// The mock estate carries a valid licence key (mock-storage/licence.json),
// so the app activates directly with no free-tier banner in shot.
// Assumes the estate is already activated (the licensing spec registers a
// valid key); otherwise a licensing page would sit in shot.
cy.get('.nav-tree', { timeout: longerCommandTimeout }).should('exist')
})
@@ -47,7 +47,8 @@ if (!tableId || !libds) {
webOutClose()
} else {
// The SAS service also checks EDIT or APPROVE access (mpe_accesscheck). The
// mock has no permission store, so access is not emulated.
// mock seeds MPE_SECURITY but no mock service enforces it, so access is not
// emulated here.
// The approval folder is zipped as it stands. getauditfile.sas lists it with
// mp_dirlist and adds each file to the package under contents/.
+26 -2
View File
@@ -641,6 +641,27 @@ function zipUtf8(value) {
return Uint8Array.from(out)
}
/**
* Renders rows as CSV, the counterpart of %mp_ds2csv: the header row, then one
* line per row, with values quoted only when they carry a delimiter, quote or
* line break.
*
* @param {string[]} headers column names, in order
* @param {Array<Object>} rows objects keyed by column name
* @returns {string} CSV text with a trailing newline
*/
function toCsv(headers, rows) {
const cell = (value) => {
const text = value === undefined || value === null ? '' : String(value)
return /[",\n\r]/.test(text) ? '"' + text.replace(/"/g, '""') + '"' : text
}
const lines = [headers.map(cell).join(',')]
for (const row of rows) {
lines.push(headers.map((header) => cell(row[header])).join(','))
}
return lines.join('\n') + '\n'
}
/**
* Builds a zip archive in memory, entries stored rather than deflated - the JS
* counterpart of the ODS package getauditfile.sas publishes. The zip format
@@ -661,11 +682,14 @@ function mpZipPackage(entries) {
const name = zipUtf8(entry.name)
const data = zipUtf8(entry.content)
const crc = zipCrc32(data)
// Bit 11 marks the name as UTF-8. Without it an extractor decodes a
// non-ASCII name as CP437, so the entry cannot be read back by name.
const flags = name.some((byte) => byte >= 0x80) ? 0x0800 : 0
// 1980-01-01 00:00, the earliest DOS timestamp: a stable value keeps
// the archive byte-identical for identical inputs.
const header = [].concat(
u32(0x04034b50), u16(20), u16(0), u16(0), u16(0), u16(0x21),
u32(0x04034b50), u16(20), u16(flags), u16(0), u16(0), u16(0x21),
u32(crc), u32(data.length), u32(data.length), u16(name.length), u16(0)
)
@@ -678,7 +702,7 @@ function mpZipPackage(entries) {
let directorySize = 0
for (const entry of central) {
const header = [].concat(
u32(0x02014b50), u16(20), u16(20), u16(0), u16(0), u16(0), u16(0x21),
u32(0x02014b50), u16(20), u16(20), u16(flags), u16(0), u16(0), u16(0x21),
u32(entry.crc), u32(entry.size), u32(entry.size),
u16(entry.name.length), u16(0), u16(0), u16(0), u16(0), u32(0),
u32(entry.offset)
+8 -7
View File
@@ -59,6 +59,9 @@ for (let i = 1; i <= 10; i++) {
let libds = ''
if (typeof table !== 'undefined' && table) libds = String(table).toUpperCase()
// captured before any hook can re-point the submission: the cleaned CSV is
// named after the original table, as loadfile.sas names it
const origLibds = libds
let libref = libds.split('.')[0] || dcLibref
let dsn = libds.split('.')[1] || ''
@@ -224,13 +227,11 @@ if (!msg) {
message: 'File upload: ' + csvName,
headers,
rows,
// loadfile.sas leaves two files in the approval folder: the cleaned
// CSV (%mp_cleancsv) and a copy of the upload (BKP_<name>). The mock
// only accepts CSV, so both hold the uploaded text.
auditFiles: [
{ name: libds + '.csv', content: csvText },
{ name: 'BKP_' + csvName, content: csvText }
]
// loadfile.sas leaves the cleaned CSV (%mp_cleancsv) in the approval
// folder, named after the table as the request found it. It copies
// the upload as BKP_<name> only when a second, non-CSV fileref came
// with the request, which the mock does not accept - so no BKP here.
auditFiles: [{ name: origLibds + '.csv', content: csvText }]
})
}
}
@@ -22,6 +22,9 @@ let message = _sctRow.MESSAGE || ''
let libds = _sctRow.LIBDS || ''
let libref = libds.split('.')[0] || dcLibref
let dsn = libds.split('.')[1] || ''
// stagedata.sas names the CSV it stages after the table as the request found
// it, before a hook can re-point the submission
const origLibds = libds
// The base table lives in its own library (DC_JSLIB for control tables,
// TESTDATA for demo user tables); the staging dir stays in the control lib.
@@ -117,7 +120,10 @@ stageSubmission({
dsn,
message,
headers: jsDataHeaders,
rows: jsDataRows
rows: jsDataRows,
// stagedata.sas writes the staged rows as <orig_libds>.csv alongside them
// (%mp_ds2csv into the approval folder), so the audit pack carries it
auditFiles: [{ name: origLibds + '.csv', content: toCsv(jsDataHeaders, jsDataRows) }]
})
webOutOpen()