feat: validation on RLS for REPLACE, + docs + tests. Closes #211
Build / Build-and-ng-test (pull_request) Successful in 5m24s
Build / Build-and-test-development (pull_request) Successful in 15m37s
Lighthouse Checks / lighthouse (pull_request) Successful in 21m43s

This commit is contained in:
4gl
2026-07-22 19:01:21 +01:00
parent ea00c5afad
commit 7378f3ba30
12 changed files with 114 additions and 26 deletions
+21
View File
@@ -0,0 +1,21 @@
# Testing
Backend (SAS) tests are run with the sasjs CLI from the `sas/` directory.
## Commands
- Full deploy + test cycle: `npm run 4gl` (compiles, deploys to the 4gl target, runs makedata) then `sasjs test -t 4gl`.
- Run tests only (no rebuild/redeploy): `sasjs test -t 4gl`.
- Run a subset of tests: `sasjs test -t 4gl SOMESTRING` — the positional argument filters to matching tests (e.g. `sasjs test -t 4gl stagedata.test.3`).
## Results
- Test results: `sas/sasjsresults/testResults.json` / `.csv` / `.xml`
- Individual test logs: `sas/sasjsresults/logs/<testsuite>.log` (e.g. `services_editors_stagedata.test.3.log`)
- Coverage: `sas/sasjsresults/coverage.lcov`
## Notes
- Test source files live in `sas/sasjs/**`; `sas/sasjsbuild/` is generated build output — do not hand-edit it.
- `sasjs test` executes tests against the deployed app, so run `npm run 4gl` first after changing any service/hook/test code.
- Assertions are made with `%mp_assert()`; results are written to `work.test_results`.
+4
View File
@@ -8,6 +8,10 @@ This is different from `.sas` files, where a maximum line length applies. The no
Rationale: hard-wrapped prose produces noisy diffs when sentences are edited and reflowed, and Markdown renderers already handle wrapping.
## SAS files
After creating or modifying any `.sas` files, run `sasjs lint` from the `sas/` directory and ensure the files you touched have no lint warnings (the repo currently has pre-existing warnings in other files, which can be ignored).
## The .agent folder
Agent-related content lives in `.agent/`: technical/agent-facing documentation goes in `.agent/docs/` (not `docs/`), and skills in `.agent/skills/`. When writing explanatory or technical docs about the codebase, put them in `.agent/docs/`.
+3 -2
View File
@@ -2,7 +2,8 @@
"fromjs": [
{
"ADMIN": "AllUsers",
"DCPATH": "/opt/data/DataController"
"DCPATH": "/opt/data/DataController",
"OUTFMT": "JSON"
}
]
}
}
+3 -2
View File
@@ -2,7 +2,8 @@
"fromjs": [
{
"ADMIN": "AllUsers",
"DCPATH": "/tmp/dcdata"
"DCPATH": "/tmp/dcdata",
"OUTFMT": "JSON"
}
]
}
}
+10 -4
View File
@@ -7,7 +7,7 @@
"name": "dc-sas",
"dependencies": {
"@sasjs/cli": "4.18.4",
"@sasjs/core": "4.68.1"
"@sasjs/core": "4.68.2"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -279,6 +279,12 @@
"sasjs": "build/index.js"
}
},
"node_modules/@sasjs/cli/node_modules/@sasjs/core": {
"version": "4.68.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.1.tgz",
"integrity": "sha512-BjoOzV7h7guCShW4JsFHvB1NNLCKHhPI5708OtEk29itQbgVCMRlEXKFRbnMiuNklktDQDi8dR4XZZ0aK7/4Wg==",
"license": "MIT"
},
"node_modules/@sasjs/cli/node_modules/@sasjs/utils": {
"version": "3.5.9",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.5.9.tgz",
@@ -315,9 +321,9 @@
}
},
"node_modules/@sasjs/core": {
"version": "4.68.1",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.1.tgz",
"integrity": "sha512-BjoOzV7h7guCShW4JsFHvB1NNLCKHhPI5708OtEk29itQbgVCMRlEXKFRbnMiuNklktDQDi8dR4XZZ0aK7/4Wg==",
"version": "4.68.2",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-4.68.2.tgz",
"integrity": "sha512-flbx36sNN7PvEIjX8H6UuGIKMQ8Ra3ZHEze9O66Wtk6cU+T0RV1i199LYXVumkja/VXSW334HlNpiSVR4o9qqg==",
"license": "MIT"
},
"node_modules/@sasjs/lint": {
+1 -1
View File
@@ -29,6 +29,6 @@
"private": true,
"dependencies": {
"@sasjs/cli": "4.18.4",
"@sasjs/core": "4.68.1"
"@sasjs/core": "4.68.2"
}
}
+26 -5
View File
@@ -3,7 +3,10 @@
@brief Testing mpe_refreshtables macro
<h4> SAS Macros </h4>
@li mpe_refreshlibs.sas
@li mpe_refreshtables.sas
@li mf_getuniquefileref.sas
@li mf_mkdir.sas
@li mp_assert.sas
@li mp_assertscope.sas
@@ -14,14 +17,32 @@
**/
/* create a new library with a demo format catalog, so it always exists */
%let reftstloc=%sysfunc(pathname(&dc_libref))/reftst%mf_getuniquefileref();
%mf_mkdir(&reftstloc)
libname reftst "&reftstloc";
data work.reftstfmts;
length fmtname $32;
fmtname='REFFMT';
do start=1 to 10;
label=cats('Format ',start);
output;
end;
run;
proc format cntlin=work.reftstfmts library=reftst.dcfmts;
run;
/* register the library in mpe_datacatalog_libs */
%mpe_refreshlibs(lib=REFTST)
%mp_assertscope(SNAPSHOT)
%mpe_refreshtables(FMTONLY)
%mpe_refreshtables(REFTST)
%mp_assertscope(COMPARE,
desc=Checking macro variables against previous snapshot
)
/* make sure that the process picks up a library that contains only a single
catalog */
/* make sure that the process picks up a library that contains a catalog */
proc sql;
create table work.libinfo as
select a.engine,
@@ -37,7 +58,7 @@ create table work.libinfo as
from &mpelib..mpe_datacatalog_libs(where=(&dc_dttmtfmt. lt tx_to)) a
left join &mpelib..mpe_datastatus_libs(where=(&dc_dttmtfmt. lt tx_to)) b
on a.libref=b.libref
where a.libref="FMTONLY";
where a.libref="REFTST";
%let test1=0;
data _null_;
@@ -46,5 +67,5 @@ data _null_;
run;
%mp_assert(
iftrue=(&test1>0),
desc=Checking fmtonly.dcfmts was picked up
desc=Checking reftst.dcfmts was picked up
)
@@ -105,7 +105,7 @@ run;
desc=Checking leading blanks were applied
)
%mp_assert(
iftrue=(&leadcheck=1),
iftrue=(&speshcheck=1),
desc=Checking special characters were applied
)
@@ -72,14 +72,16 @@ run;
mdebug=&sasjs_mdebug
)
/* search terms may span multiple lines, so track matches across records */
%let abort1=0;
data _null_;
retain foundabort foundmsg 0;
infile wb1;
input;
putlog _infile_;
if index(_infile_,'sasjsAbort')
and index(_infile_,'REPLACE loadtype')
then call symputx('abort1',1);
if index(_infile_,'sasjsAbort') then foundabort=1;
if index(_infile_,'REPLACE loadtype') then foundmsg=1;
if foundabort=1 and foundmsg=1 then call symputx('abort1',1);
run;
%mp_assert(
@@ -140,12 +142,13 @@ run;
%let abort2=0;
data _null_;
retain foundabort foundmsg 0;
infile wb2;
input;
putlog _infile_;
if index(_infile_,'sasjsAbort')
and index(_infile_,'row level EDIT security')
then call symputx('abort2',1);
if index(_infile_,'sasjsAbort') then foundabort=1;
if index(_infile_,'EDIT security') then foundmsg=1;
if foundabort=1 and foundmsg=1 then call symputx('abort2',1);
run;
%mp_assert(
@@ -50,8 +50,8 @@ run;
%mp_abort(iftrue=(&badloadtypes ne 0)
,mac=mpe_row_level_security_postedit
,msg=%str(Row level EDIT security cannot be applied to table(s) with a%trim(
) REPLACE loadtype, eg: &badloadtype. Remove the security rule(s) or %trim(
)change the loadtype.)
) REPLACE loadtype, eg: &badloadtype. Remove the security rule(s) or%trim(
) change the loadtype.)
)
/* ignore scope and group for validation */
@@ -91,8 +91,8 @@ run;
%mp_abort(iftrue=(&badloadtypes ne 0)
,mac=mpe_tables_postedit
,msg=%str(REPLACE loadtype cannot be applied to table(s) with row level %trim(
)EDIT security, eg: &badloadtype. Remove the security rule(s) or change%trim(
,msg=%str(REPLACE loadtype cannot be applied to table(s) with row level%trim(
) EDIT security, eg: &badloadtype. Remove the security rule(s) or change%trim(
) the loadtype.)
)
@@ -26,7 +26,10 @@
**/
%global path ADMIN ;
%global path ADMIN OUTFMT;
/* response format - HTML (default) or JSON */
%let OUTFMT=HTML;
%webout(FETCH)
@@ -39,6 +42,19 @@ data _null_;
call symputx('ADMIN',ADMIN);
run;
/* check if a JSON response was requested (OUTFMT column in fromjs) */
%if &exist=1 %then %do;
data _null_;
dsid=open('work.fromjs');
if varnum(dsid,'OUTFMT')>0 then do;
set work.fromjs;
call symputx('OUTFMT',upcase(outfmt));
end;
rc=close(dsid);
stop;
run;
%end;
%mp_abort(iftrue= (&syscc ne 0)
,mac=&_program
,msg=%str(Issue on makedata entry)
@@ -140,6 +156,20 @@ run;
)
%if &OUTFMT=JSON %then %do;
data work.makedata_response;
length STATUS $10 MESSAGE $256 DCPATH $256 LIBREF $8 ADMIN $64;
STATUS='SUCCESS';
MESSAGE="Data Controller Config created by &sysuserid";
DCPATH="&dcpath";
LIBREF="&DC_LIBREF";
ADMIN="&admin";
run;
%webout(OPEN)
%webout(OBJ, makedata_response)
%webout(CLOSE)
%end;
%else %do;
data _null_;
file _webout;
put '<h3>Data Controller Config</h3>';
@@ -159,6 +189,7 @@ data _null_;
put "<li><a target='_blank' href='/AppStream/DataController'>Launch!</a>";
put "</li></ol>";
run;
%end;
/* We ran successfully, now remove configurator and makedata STPs */
%ms_deletefile(&root/services/admin/configurator.sas)