@@ -297,6 +297,158 @@ END;`
297297 }
298298}
299299
300+ // --- CE0066 "Entity access is out of date" scenario tests ---
301+ //
302+ // These tests enumerate mutation orderings that might trigger CE0066 when
303+ // entity structure changes after access rules have been written.
304+ //
305+ // Scenarios to test:
306+ // S1: CREATE ENTITY (with attrs) → GRANT (baseline)
307+ // S2: CREATE ENTITY → GRANT → ALTER ADD ATTRIBUTE (attribute added after security)
308+ // S3: CREATE ENTITY → GRANT → CREATE ASSOCIATION (association added after security)
309+ // S4: CREATE ENTITY → GRANT → ALTER DROP ATTRIBUTE (attribute removed after security)
310+ // S5: CREATE ENTITY → ALTER ADD ATTRIBUTE → GRANT (security after mutation — should always work)
311+ // S6: CREATE ENTITY → GRANT(READ *) → ALTER → GRANT(READ *) (re-grant after mutation)
312+ // S7: CREATE ENTITY + GRANT in single script (typical user script pattern)
313+ // S8: CREATE ENTITY → GRANT multiple roles → ALTER (multiple rules + mutation)
314+ // S9: CREATE ENTITY → GRANT → ALTER + CREATE ASSOC (combined) (both attrs and assocs change)
315+
316+ // ce0066Scenario describes a single CE0066 test case.
317+ type ce0066Scenario struct {
318+ name string // subtest name
319+ steps []string // MDL statements to execute in order
320+ }
321+
322+ func TestMxCheck_CE0066_Scenarios (t * testing.T ) {
323+ if ! mxCheckAvailable () {
324+ t .Skip ("mx command not available" )
325+ }
326+
327+ mod := testModule
328+ scenarios := []ce0066Scenario {
329+ {
330+ name : "S1_CreateEntity_Grant" ,
331+ steps : []string {
332+ `CREATE MODULE ROLE ` + mod + `.S1Admin;` ,
333+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S1Entity (Name: String(100), Email: String(200));` ,
334+ `GRANT ` + mod + `.S1Admin ON ` + mod + `.S1Entity (CREATE, DELETE, READ *, WRITE *);` ,
335+ },
336+ },
337+ {
338+ name : "S2_Grant_ThenAddAttribute" ,
339+ steps : []string {
340+ `CREATE MODULE ROLE ` + mod + `.S2Admin;` ,
341+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S2Entity (Name: String(100));` ,
342+ `GRANT ` + mod + `.S2Admin ON ` + mod + `.S2Entity (CREATE, DELETE, READ *, WRITE *);` ,
343+ `ALTER ENTITY ` + mod + `.S2Entity ADD ATTRIBUTE Email String(200);` ,
344+ `ALTER ENTITY ` + mod + `.S2Entity ADD ATTRIBUTE Active Boolean DEFAULT false;` ,
345+ },
346+ },
347+ {
348+ name : "S3_Grant_ThenAddAssociation" ,
349+ steps : []string {
350+ `CREATE MODULE ROLE ` + mod + `.S3Admin;` ,
351+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S3Parent (Name: String(100));` ,
352+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S3Child (Label: String(100));` ,
353+ `GRANT ` + mod + `.S3Admin ON ` + mod + `.S3Parent (CREATE, DELETE, READ *, WRITE *);` ,
354+ `CREATE ASSOCIATION ` + mod + `.S3Child_S3Parent (` + mod + `.S3Child -> ` + mod + `.S3Parent);` ,
355+ },
356+ },
357+ {
358+ name : "S4_Grant_ThenDropAttribute" ,
359+ steps : []string {
360+ `CREATE MODULE ROLE ` + mod + `.S4Admin;` ,
361+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S4Entity (Name: String(100), Temp: String(50));` ,
362+ `GRANT ` + mod + `.S4Admin ON ` + mod + `.S4Entity (CREATE, DELETE, READ *, WRITE *);` ,
363+ `ALTER ENTITY ` + mod + `.S4Entity DROP ATTRIBUTE Temp;` ,
364+ },
365+ },
366+ {
367+ name : "S5_AddAttribute_ThenGrant" ,
368+ steps : []string {
369+ `CREATE MODULE ROLE ` + mod + `.S5Admin;` ,
370+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S5Entity (Name: String(100));` ,
371+ `ALTER ENTITY ` + mod + `.S5Entity ADD ATTRIBUTE Email String(200);` ,
372+ `GRANT ` + mod + `.S5Admin ON ` + mod + `.S5Entity (CREATE, DELETE, READ *, WRITE *);` ,
373+ },
374+ },
375+ {
376+ name : "S6_Grant_Alter_Regrant" ,
377+ steps : []string {
378+ `CREATE MODULE ROLE ` + mod + `.S6Admin;` ,
379+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S6Entity (Name: String(100));` ,
380+ `GRANT ` + mod + `.S6Admin ON ` + mod + `.S6Entity (READ *);` ,
381+ `ALTER ENTITY ` + mod + `.S6Entity ADD ATTRIBUTE Code String(50);` ,
382+ `GRANT ` + mod + `.S6Admin ON ` + mod + `.S6Entity (CREATE, DELETE, READ *, WRITE *);` ,
383+ },
384+ },
385+ {
386+ name : "S7_SingleScript_CreateAndGrant" ,
387+ steps : []string {
388+ `CREATE MODULE ROLE ` + mod + `.S7Admin;` ,
389+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S7Entity (
390+ Code: String(50) NOT NULL,
391+ Description: String(500),
392+ Count: Integer DEFAULT 0
393+ );` + "\n " +
394+ `GRANT ` + mod + `.S7Admin ON ` + mod + `.S7Entity (CREATE, DELETE, READ *, WRITE *);` ,
395+ },
396+ },
397+ {
398+ name : "S8_MultipleRoles_ThenAlter" ,
399+ steps : []string {
400+ `CREATE MODULE ROLE ` + mod + `.S8Admin;` ,
401+ `CREATE MODULE ROLE ` + mod + `.S8Viewer;` ,
402+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S8Entity (Name: String(100));` ,
403+ `GRANT ` + mod + `.S8Admin ON ` + mod + `.S8Entity (CREATE, DELETE, READ *, WRITE *);` ,
404+ `GRANT ` + mod + `.S8Viewer ON ` + mod + `.S8Entity (READ *);` ,
405+ `ALTER ENTITY ` + mod + `.S8Entity ADD ATTRIBUTE Status String(50);` ,
406+ },
407+ },
408+ {
409+ name : "S9_Grant_ThenAlterAndAssoc" ,
410+ steps : []string {
411+ `CREATE MODULE ROLE ` + mod + `.S9Admin;` ,
412+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S9Main (Name: String(100));` ,
413+ `CREATE OR MODIFY PERSISTENT ENTITY ` + mod + `.S9Related (Value: Integer);` ,
414+ `GRANT ` + mod + `.S9Admin ON ` + mod + `.S9Main (CREATE, DELETE, READ *, WRITE *);` ,
415+ `ALTER ENTITY ` + mod + `.S9Main ADD ATTRIBUTE Extra String(200);` ,
416+ `CREATE ASSOCIATION ` + mod + `.S9Related_S9Main (` + mod + `.S9Related -> ` + mod + `.S9Main);` ,
417+ },
418+ },
419+ }
420+
421+ for _ , sc := range scenarios {
422+ t .Run (sc .name , func (t * testing.T ) {
423+ env := setupTestEnv (t )
424+ defer env .teardown ()
425+
426+ for i , step := range sc .steps {
427+ if err := env .executeMDL (step ); err != nil {
428+ if ! strings .Contains (err .Error (), "already exists" ) {
429+ t .Fatalf ("Step %d failed: %v\n MDL: %s" , i + 1 , err , step )
430+ }
431+ }
432+ }
433+
434+ env .executor .Execute (& ast.DisconnectStmt {})
435+
436+ output , err := runMxCheck (t , env .projectPath )
437+ if err != nil {
438+ if strings .Contains (output , "CE0066" ) || strings .Contains (output , "out of date" ) {
439+ t .Errorf ("CE0066 entity access out of date:\n %s" , output )
440+ } else if strings .Contains (output , "error" ) || strings .Contains (output , "Error" ) {
441+ t .Errorf ("mx check found errors:\n %s" , output )
442+ } else {
443+ t .Logf ("mx check output (non-zero exit but no errors):\n %s" , output )
444+ }
445+ } else {
446+ t .Logf ("mx check passed" )
447+ }
448+ })
449+ }
450+ }
451+
300452// TestMxCheck_MicroflowWithCallParams tests microflow with CALL unified param syntax.
301453func TestMxCheck_MicroflowWithCallParams (t * testing.T ) {
302454 if ! mxCheckAvailable () {
0 commit comments