fix(mocks): address the audit-pack review findings
- 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:
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/.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user