@o3co/auth.policy-verifier.builtins 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.ja.md +104 -2
  2. package/README.md +102 -0
  3. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +2 -0
  4. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +1 -0
  5. package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +253 -0
  6. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +2 -0
  7. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +1 -0
  8. package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +138 -0
  9. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +2 -0
  10. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +1 -0
  11. package/dist/__tests__/rules/AttrLiteralIn.test.mjs +160 -0
  12. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +2 -0
  13. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +1 -0
  14. package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +157 -0
  15. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +2 -0
  16. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +1 -0
  17. package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +168 -0
  18. package/dist/__tests__/rules/AttrMatchRule.test.mjs +8 -0
  19. package/dist/__tests__/rules/AttrPairCompare.test.d.mts +2 -0
  20. package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +1 -0
  21. package/dist/__tests__/rules/AttrPairCompare.test.mjs +319 -0
  22. package/dist/__tests__/rules/AttrPairEqual.test.d.mts +2 -0
  23. package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +1 -0
  24. package/dist/__tests__/rules/AttrPairEqual.test.mjs +155 -0
  25. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +2 -0
  26. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +1 -0
  27. package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +155 -0
  28. package/dist/__tests__/rules/_sharedValidation.test.d.mts +2 -0
  29. package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +1 -0
  30. package/dist/__tests__/rules/_sharedValidation.test.mjs +325 -0
  31. package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +1 -0
  32. package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.mjs → collectors/ResourceActionPermissionRuleCollector.test.mjs} +1 -1
  33. package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +1 -0
  34. package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.mjs → collectors/ResourceActionScopeRuleCollector.test.mjs} +1 -1
  35. package/dist/index.d.mts +10 -2
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs +10 -2
  38. package/dist/module.mjs +2 -2
  39. package/dist/rules/AttrLiteralCompare.d.mts +31 -0
  40. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -0
  41. package/dist/rules/AttrLiteralCompare.mjs +36 -0
  42. package/dist/rules/AttrLiteralEqual.d.mts +37 -0
  43. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -0
  44. package/dist/rules/AttrLiteralEqual.mjs +43 -0
  45. package/dist/rules/AttrLiteralIn.d.mts +38 -0
  46. package/dist/rules/AttrLiteralIn.d.mts.map +1 -0
  47. package/dist/rules/AttrLiteralIn.mjs +46 -0
  48. package/dist/rules/AttrLiteralNotEqual.d.mts +36 -0
  49. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -0
  50. package/dist/rules/AttrLiteralNotEqual.mjs +42 -0
  51. package/dist/rules/AttrLiteralNotIn.d.mts +33 -0
  52. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -0
  53. package/dist/rules/AttrLiteralNotIn.mjs +41 -0
  54. package/dist/rules/AttrMatchRule.d.mts +7 -34
  55. package/dist/rules/AttrMatchRule.d.mts.map +1 -1
  56. package/dist/rules/AttrMatchRule.mjs +7 -32
  57. package/dist/rules/AttrPairCompare.d.mts +35 -0
  58. package/dist/rules/AttrPairCompare.d.mts.map +1 -0
  59. package/dist/rules/AttrPairCompare.mjs +43 -0
  60. package/dist/rules/AttrPairEqual.d.mts +35 -0
  61. package/dist/rules/AttrPairEqual.d.mts.map +1 -0
  62. package/dist/rules/AttrPairEqual.mjs +44 -0
  63. package/dist/rules/AttrPairNotEqual.d.mts +34 -0
  64. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -0
  65. package/dist/rules/AttrPairNotEqual.mjs +43 -0
  66. package/dist/rules/_sharedValidation.d.mts +87 -0
  67. package/dist/rules/_sharedValidation.d.mts.map +1 -0
  68. package/dist/rules/_sharedValidation.mjs +189 -0
  69. package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -0
  70. package/dist/rules/{ResourceActionPermissionRuleCollector.mjs → collectors/ResourceActionPermissionRuleCollector.mjs} +1 -1
  71. package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -0
  72. package/dist/rules/{ResourceActionScopeRuleCollector.mjs → collectors/ResourceActionScopeRuleCollector.mjs} +1 -1
  73. package/package.json +5 -2
  74. package/dist/__tests__/rules/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
  75. package/dist/__tests__/rules/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
  76. package/dist/rules/ResourceActionPermissionRuleCollector.d.mts.map +0 -1
  77. package/dist/rules/ResourceActionScopeRuleCollector.d.mts.map +0 -1
  78. /package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.d.mts → collectors/ResourceActionPermissionRuleCollector.test.d.mts} +0 -0
  79. /package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.d.mts → collectors/ResourceActionScopeRuleCollector.test.d.mts} +0 -0
  80. /package/dist/rules/{ResourceActionPermissionRuleCollector.d.mts → collectors/ResourceActionPermissionRuleCollector.d.mts} +0 -0
  81. /package/dist/rules/{ResourceActionScopeRuleCollector.d.mts → collectors/ResourceActionScopeRuleCollector.d.mts} +0 -0
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrPairEqual } from "#/rules/AttrPairEqual.mjs";
3
+ describe("AttrPairEqual", () => {
4
+ // ---------------------------------------------------------------------------
5
+ // Happy path: verify() returns true
6
+ // ---------------------------------------------------------------------------
7
+ it("returns true when both attrs are equal non-empty strings", () => {
8
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
9
+ const attrs = new Map([
10
+ ["subject", "alice"],
11
+ ["owner", "alice"],
12
+ ]);
13
+ expect(rule.verify(attrs)).toBe(true);
14
+ });
15
+ // ---------------------------------------------------------------------------
16
+ // verify() returns false
17
+ // ---------------------------------------------------------------------------
18
+ it("returns false when the two strings are different", () => {
19
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
20
+ const attrs = new Map([
21
+ ["subject", "alice"],
22
+ ["owner", "bob"],
23
+ ]);
24
+ expect(rule.verify(attrs)).toBe(false);
25
+ });
26
+ it("returns false when attr 'a' is an empty string", () => {
27
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
28
+ const attrs = new Map([
29
+ ["subject", ""],
30
+ ["owner", "alice"],
31
+ ]);
32
+ expect(rule.verify(attrs)).toBe(false);
33
+ });
34
+ it("returns false when attr 'b' is an empty string", () => {
35
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
36
+ const attrs = new Map([
37
+ ["subject", "alice"],
38
+ ["owner", ""],
39
+ ]);
40
+ expect(rule.verify(attrs)).toBe(false);
41
+ });
42
+ it("returns false when attr 'a' is a number (non-string)", () => {
43
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
44
+ const attrs = new Map([
45
+ ["subject", 42],
46
+ ["owner", "alice"],
47
+ ]);
48
+ expect(rule.verify(attrs)).toBe(false);
49
+ });
50
+ it("returns false when attr 'b' is a number (non-string)", () => {
51
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
52
+ const attrs = new Map([
53
+ ["subject", "alice"],
54
+ ["owner", 42],
55
+ ]);
56
+ expect(rule.verify(attrs)).toBe(false);
57
+ });
58
+ it("returns false when attr 'a' is missing", () => {
59
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
60
+ const attrs = new Map([["owner", "alice"]]);
61
+ expect(rule.verify(attrs)).toBe(false);
62
+ });
63
+ it("returns false when attr 'b' is missing", () => {
64
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
65
+ const attrs = new Map([["subject", "alice"]]);
66
+ expect(rule.verify(attrs)).toBe(false);
67
+ });
68
+ it("returns false when attr 'a' is null", () => {
69
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
70
+ const attrs = new Map([
71
+ ["subject", null],
72
+ ["owner", "alice"],
73
+ ]);
74
+ expect(rule.verify(attrs)).toBe(false);
75
+ });
76
+ it("returns false when attr 'b' is null", () => {
77
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
78
+ const attrs = new Map([
79
+ ["subject", "alice"],
80
+ ["owner", null],
81
+ ]);
82
+ expect(rule.verify(attrs)).toBe(false);
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // Construction errors: invalid 'a'
86
+ // ---------------------------------------------------------------------------
87
+ it("throws when 'a' is undefined", () => {
88
+ expect(() => new AttrPairEqual({ a: undefined, b: "owner" })).toThrow(/AttrPairEqual.*'a'.*(non-empty string|got undefined)/);
89
+ });
90
+ it("throws when 'a' is a non-string (number 42)", () => {
91
+ expect(() => new AttrPairEqual({ a: 42, b: "owner" })).toThrow(/AttrPairEqual.*'a'.*got number/);
92
+ });
93
+ it("throws when 'a' is an empty string", () => {
94
+ expect(() => new AttrPairEqual({ a: "", b: "owner" })).toThrow(/AttrPairEqual.*'a'.*empty string/);
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // Construction errors: invalid 'b'
98
+ // ---------------------------------------------------------------------------
99
+ it("throws when 'b' is undefined", () => {
100
+ expect(() => new AttrPairEqual({ a: "subject", b: undefined })).toThrow(/AttrPairEqual.*'b'.*(non-empty string|got undefined)/);
101
+ });
102
+ it("throws when 'b' is a non-string (number 42)", () => {
103
+ expect(() => new AttrPairEqual({ a: "subject", b: 42 })).toThrow(/AttrPairEqual.*'b'.*got number/);
104
+ });
105
+ it("throws when 'b' is an empty string", () => {
106
+ expect(() => new AttrPairEqual({ a: "subject", b: "" })).toThrow(/AttrPairEqual.*'b'.*empty string/);
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // Construction errors: invalid 'group'
110
+ // ---------------------------------------------------------------------------
111
+ it("throws when 'group' is present but is a non-string (number 42)", () => {
112
+ expect(() => new AttrPairEqual({ a: "subject", b: "owner", group: 42 })).toThrow(/AttrPairEqual.*'group'.*got number/);
113
+ });
114
+ it("throws when 'group' is present but is an empty string", () => {
115
+ expect(() => new AttrPairEqual({ a: "subject", b: "owner", group: "" })).toThrow(/AttrPairEqual.*'group'.*empty string/);
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // ruleType: default derived from 'a' and 'b'
119
+ // ---------------------------------------------------------------------------
120
+ it("derives default ruleType from a and b", () => {
121
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
122
+ expect(rule.ruleType).toBe("attr_pair_equal:subject:owner");
123
+ });
124
+ it("distinct a/b produce distinct default ruleTypes (AND semantics)", () => {
125
+ const r1 = new AttrPairEqual({ a: "subject", b: "owner" });
126
+ const r2 = new AttrPairEqual({ a: "subject", b: "tenant" });
127
+ expect(r1.ruleType).not.toBe(r2.ruleType);
128
+ });
129
+ // ---------------------------------------------------------------------------
130
+ // ruleType: group override
131
+ // ---------------------------------------------------------------------------
132
+ it("uses the provided group string as ruleType, overriding the default", () => {
133
+ const r1 = new AttrPairEqual({ a: "subject", b: "owner", group: "identity-check" });
134
+ const r2 = new AttrPairEqual({ a: "subject", b: "creator", group: "identity-check" });
135
+ expect(r1.ruleType).toBe("identity-check");
136
+ expect(r2.ruleType).toBe("identity-check");
137
+ });
138
+ // ---------------------------------------------------------------------------
139
+ // code and message fields
140
+ // ---------------------------------------------------------------------------
141
+ it("has code 'attr_mismatch'", () => {
142
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
143
+ expect(rule.code).toBe("attr_mismatch");
144
+ });
145
+ it("message contains 'a' name, 'b' name, and the phrase 'must equal'", () => {
146
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
147
+ expect(rule.message).toContain("subject");
148
+ expect(rule.message).toContain("owner");
149
+ expect(rule.message).toContain("must equal");
150
+ });
151
+ it("message starts with a capital letter", () => {
152
+ const rule = new AttrPairEqual({ a: "subject", b: "owner" });
153
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
154
+ });
155
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrPairNotEqual.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairNotEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrPairNotEqual.test.mts"],"names":[],"mappings":""}
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrPairNotEqual } from "#/rules/AttrPairNotEqual.mjs";
3
+ describe("AttrPairNotEqual", () => {
4
+ // ---------------------------------------------------------------------------
5
+ // Happy path: verify() returns true
6
+ // ---------------------------------------------------------------------------
7
+ it("returns true when both attrs are non-empty strings and not equal", () => {
8
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
9
+ const attrs = new Map([
10
+ ["subject", "alice"],
11
+ ["owner", "bob"],
12
+ ]);
13
+ expect(rule.verify(attrs)).toBe(true);
14
+ });
15
+ // ---------------------------------------------------------------------------
16
+ // verify() returns false
17
+ // ---------------------------------------------------------------------------
18
+ it("returns false when the two strings are equal", () => {
19
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
20
+ const attrs = new Map([
21
+ ["subject", "alice"],
22
+ ["owner", "alice"],
23
+ ]);
24
+ expect(rule.verify(attrs)).toBe(false);
25
+ });
26
+ it("returns false when attr 'a' is an empty string", () => {
27
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
28
+ const attrs = new Map([
29
+ ["subject", ""],
30
+ ["owner", "bob"],
31
+ ]);
32
+ expect(rule.verify(attrs)).toBe(false);
33
+ });
34
+ it("returns false when attr 'b' is an empty string", () => {
35
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
36
+ const attrs = new Map([
37
+ ["subject", "alice"],
38
+ ["owner", ""],
39
+ ]);
40
+ expect(rule.verify(attrs)).toBe(false);
41
+ });
42
+ it("returns false when attr 'a' is a number (non-string)", () => {
43
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
44
+ const attrs = new Map([
45
+ ["subject", 42],
46
+ ["owner", "bob"],
47
+ ]);
48
+ expect(rule.verify(attrs)).toBe(false);
49
+ });
50
+ it("returns false when attr 'b' is a number (non-string)", () => {
51
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
52
+ const attrs = new Map([
53
+ ["subject", "alice"],
54
+ ["owner", 42],
55
+ ]);
56
+ expect(rule.verify(attrs)).toBe(false);
57
+ });
58
+ it("returns false when attr 'a' is missing", () => {
59
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
60
+ const attrs = new Map([["owner", "bob"]]);
61
+ expect(rule.verify(attrs)).toBe(false);
62
+ });
63
+ it("returns false when attr 'b' is missing", () => {
64
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
65
+ const attrs = new Map([["subject", "alice"]]);
66
+ expect(rule.verify(attrs)).toBe(false);
67
+ });
68
+ it("returns false when attr 'a' is null", () => {
69
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
70
+ const attrs = new Map([
71
+ ["subject", null],
72
+ ["owner", "bob"],
73
+ ]);
74
+ expect(rule.verify(attrs)).toBe(false);
75
+ });
76
+ it("returns false when attr 'b' is null", () => {
77
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
78
+ const attrs = new Map([
79
+ ["subject", "alice"],
80
+ ["owner", null],
81
+ ]);
82
+ expect(rule.verify(attrs)).toBe(false);
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // Construction errors: invalid 'a'
86
+ // ---------------------------------------------------------------------------
87
+ it("throws when 'a' is undefined", () => {
88
+ expect(() => new AttrPairNotEqual({ a: undefined, b: "owner" })).toThrow(/AttrPairNotEqual.*'a'.*(non-empty string|got undefined)/);
89
+ });
90
+ it("throws when 'a' is a non-string (number 42)", () => {
91
+ expect(() => new AttrPairNotEqual({ a: 42, b: "owner" })).toThrow(/AttrPairNotEqual.*'a'.*got number/);
92
+ });
93
+ it("throws when 'a' is an empty string", () => {
94
+ expect(() => new AttrPairNotEqual({ a: "", b: "owner" })).toThrow(/AttrPairNotEqual.*'a'.*empty string/);
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // Construction errors: invalid 'b'
98
+ // ---------------------------------------------------------------------------
99
+ it("throws when 'b' is undefined", () => {
100
+ expect(() => new AttrPairNotEqual({ a: "subject", b: undefined })).toThrow(/AttrPairNotEqual.*'b'.*(non-empty string|got undefined)/);
101
+ });
102
+ it("throws when 'b' is a non-string (number 42)", () => {
103
+ expect(() => new AttrPairNotEqual({ a: "subject", b: 42 })).toThrow(/AttrPairNotEqual.*'b'.*got number/);
104
+ });
105
+ it("throws when 'b' is an empty string", () => {
106
+ expect(() => new AttrPairNotEqual({ a: "subject", b: "" })).toThrow(/AttrPairNotEqual.*'b'.*empty string/);
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // Construction errors: invalid 'group'
110
+ // ---------------------------------------------------------------------------
111
+ it("throws when 'group' is present but is a non-string (number 42)", () => {
112
+ expect(() => new AttrPairNotEqual({ a: "subject", b: "owner", group: 42 })).toThrow(/AttrPairNotEqual.*'group'.*got number/);
113
+ });
114
+ it("throws when 'group' is present but is an empty string", () => {
115
+ expect(() => new AttrPairNotEqual({ a: "subject", b: "owner", group: "" })).toThrow(/AttrPairNotEqual.*'group'.*empty string/);
116
+ });
117
+ // ---------------------------------------------------------------------------
118
+ // ruleType: default derived from 'a' and 'b'
119
+ // ---------------------------------------------------------------------------
120
+ it("derives default ruleType from a and b", () => {
121
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
122
+ expect(rule.ruleType).toBe("attr_pair_not_equal:subject:owner");
123
+ });
124
+ it("distinct a/b produce distinct default ruleTypes (AND semantics)", () => {
125
+ const r1 = new AttrPairNotEqual({ a: "subject", b: "owner" });
126
+ const r2 = new AttrPairNotEqual({ a: "subject", b: "tenant" });
127
+ expect(r1.ruleType).not.toBe(r2.ruleType);
128
+ });
129
+ // ---------------------------------------------------------------------------
130
+ // ruleType: group override
131
+ // ---------------------------------------------------------------------------
132
+ it("uses the provided group string as ruleType, overriding the default", () => {
133
+ const r1 = new AttrPairNotEqual({ a: "subject", b: "owner", group: "not-self" });
134
+ const r2 = new AttrPairNotEqual({ a: "subject", b: "creator", group: "not-self" });
135
+ expect(r1.ruleType).toBe("not-self");
136
+ expect(r2.ruleType).toBe("not-self");
137
+ });
138
+ // ---------------------------------------------------------------------------
139
+ // code and message fields
140
+ // ---------------------------------------------------------------------------
141
+ it("has code 'attr_match'", () => {
142
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
143
+ expect(rule.code).toBe("attr_match");
144
+ });
145
+ it("message contains 'a' name, 'b' name, and the phrase 'must not equal'", () => {
146
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
147
+ expect(rule.message).toContain("subject");
148
+ expect(rule.message).toContain("owner");
149
+ expect(rule.message).toContain("must not equal");
150
+ });
151
+ it("message starts with a capital letter", () => {
152
+ const rule = new AttrPairNotEqual({ a: "subject", b: "owner" });
153
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
154
+ });
155
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=_sharedValidation.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_sharedValidation.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/_sharedValidation.test.mts"],"names":[],"mappings":""}
@@ -0,0 +1,325 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyCompare, COMPARE_OPS, computeValuesKey, requireAttrName, requireCompareOp, requireHomogeneousLiteralArray, requireLiteralValue, requireNumber, requireOptionalGroup, } from "#/rules/_sharedValidation.mjs";
3
+ // ---------------------------------------------------------------------------
4
+ // requireAttrName
5
+ // ---------------------------------------------------------------------------
6
+ describe("requireAttrName", () => {
7
+ it("returns the input string when value is a valid non-empty string", () => {
8
+ expect(requireAttrName("MyClass", "a", "role")).toBe("role");
9
+ });
10
+ it("returns the input string for fieldName 'b'", () => {
11
+ expect(requireAttrName("MyClass", "b", "score")).toBe("score");
12
+ });
13
+ it("throws with className and fieldName 'a' in message when value is undefined", () => {
14
+ expect(() => requireAttrName("MyClass", "a", undefined)).toThrow(/MyClass.*'a'.*non-empty string.*got undefined/);
15
+ });
16
+ it("throws with className and fieldName 'b' in message when value is undefined", () => {
17
+ expect(() => requireAttrName("MyClass", "b", undefined)).toThrow(/MyClass.*'b'.*non-empty string.*got undefined/);
18
+ });
19
+ it("throws with 'empty string' in message when value is ''", () => {
20
+ expect(() => requireAttrName("MyClass", "a", "")).toThrow(/MyClass.*'a'.*got empty string/);
21
+ });
22
+ it("throws with 'got number' in message when value is 42", () => {
23
+ expect(() => requireAttrName("MyClass", "a", 42)).toThrow(/MyClass.*'a'.*got number/);
24
+ });
25
+ it("throws with 'null' in message when value is null", () => {
26
+ expect(() => requireAttrName("MyClass", "a", null)).toThrow(/null/);
27
+ });
28
+ it("throws when value contains ':' (reserved as ruleType separator)", () => {
29
+ // Regression guard for an authorization-weakening bug: with ':' allowed,
30
+ // (a="x:y", b="z") and (a="x", b="y:z") would both produce the same
31
+ // `attr_pair_equal:x:y:z` default ruleType and be silently OR-combined
32
+ // by the evaluator. Fail-fast at construction instead.
33
+ expect(() => requireAttrName("MyClass", "a", "name:space")).toThrow(/must not contain ':'.*got 'name:space'/);
34
+ });
35
+ });
36
+ // ---------------------------------------------------------------------------
37
+ // requireLiteralValue
38
+ // ---------------------------------------------------------------------------
39
+ describe("requireLiteralValue", () => {
40
+ it("returns the value when it is a string", () => {
41
+ expect(requireLiteralValue("MyClass", "v", "hello")).toBe("hello");
42
+ });
43
+ it("returns the value when it is a number", () => {
44
+ expect(requireLiteralValue("MyClass", "v", 42)).toBe(42);
45
+ });
46
+ it("returns the value when it is a boolean", () => {
47
+ expect(requireLiteralValue("MyClass", "v", false)).toBe(false);
48
+ });
49
+ it("throws with 'null' in message when value is null", () => {
50
+ expect(() => requireLiteralValue("MyClass", "v", null)).toThrow(/null/);
51
+ });
52
+ it("throws with 'undefined' in message when value is undefined", () => {
53
+ expect(() => requireLiteralValue("MyClass", "v", undefined)).toThrow(/undefined/);
54
+ });
55
+ it("throws with 'object' in message when value is a plain object", () => {
56
+ expect(() => requireLiteralValue("MyClass", "v", {})).toThrow(/object/);
57
+ });
58
+ it("throws with 'object' in message when value is an array (typeof [] === 'object')", () => {
59
+ expect(() => requireLiteralValue("MyClass", "v", [])).toThrow(/object/);
60
+ });
61
+ it("throws on NaN (NaN === NaN is false; would invert Equal/NotEqual rules)", () => {
62
+ // Regression guard for an authorization-weakening bug: with NaN accepted,
63
+ // AttrLiteralNotEqual({ v: NaN }) would always pass for any numeric
64
+ // attribute because `x !== NaN` is always true. Must fail-fast in
65
+ // construction instead.
66
+ expect(() => requireLiteralValue("MyClass", "v", Number.NaN)).toThrow(/must not be NaN/);
67
+ });
68
+ });
69
+ // ---------------------------------------------------------------------------
70
+ // requireNumber
71
+ // ---------------------------------------------------------------------------
72
+ describe("requireNumber", () => {
73
+ it("returns 0 (zero)", () => {
74
+ expect(requireNumber("MyClass", "v", 0)).toBe(0);
75
+ });
76
+ it("returns -0 (negative zero)", () => {
77
+ expect(requireNumber("MyClass", "v", -0)).toBe(-0);
78
+ });
79
+ it("returns a large finite number", () => {
80
+ expect(requireNumber("MyClass", "v", 1e308)).toBe(1e308);
81
+ });
82
+ it("returns Infinity without throwing (Infinity is a valid number per spec)", () => {
83
+ expect(requireNumber("MyClass", "v", Infinity)).toBe(Infinity);
84
+ });
85
+ it("returns -Infinity without throwing", () => {
86
+ expect(requireNumber("MyClass", "v", -Infinity)).toBe(-Infinity);
87
+ });
88
+ it("throws on NaN with message matching /must not be NaN/", () => {
89
+ expect(() => requireNumber("MyClass", "v", NaN)).toThrow(/must not be NaN/);
90
+ });
91
+ it("throws on a string '42' with message containing 'got string'", () => {
92
+ expect(() => requireNumber("MyClass", "v", "42")).toThrow(/got string/);
93
+ });
94
+ it("throws on null with message containing 'null'", () => {
95
+ expect(() => requireNumber("MyClass", "v", null)).toThrow(/null/);
96
+ });
97
+ it("throws on undefined with message containing 'undefined'", () => {
98
+ expect(() => requireNumber("MyClass", "v", undefined)).toThrow(/undefined/);
99
+ });
100
+ });
101
+ // ---------------------------------------------------------------------------
102
+ // requireHomogeneousLiteralArray
103
+ // ---------------------------------------------------------------------------
104
+ describe("requireHomogeneousLiteralArray", () => {
105
+ it("returns a non-empty all-string array", () => {
106
+ const arr = ["a", "b", "c"];
107
+ expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
108
+ });
109
+ it("returns a non-empty all-number array", () => {
110
+ const arr = [1, 2, 3];
111
+ expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
112
+ });
113
+ it("returns a non-empty all-boolean array", () => {
114
+ const arr = [true, false];
115
+ expect(requireHomogeneousLiteralArray("MyClass", arr)).toEqual(arr);
116
+ });
117
+ it("throws with 'got string' when value is not an array", () => {
118
+ expect(() => requireHomogeneousLiteralArray("MyClass", "abc")).toThrow(/got string/);
119
+ });
120
+ it("throws with 'undefined' when value is undefined", () => {
121
+ expect(() => requireHomogeneousLiteralArray("MyClass", undefined)).toThrow(/undefined/);
122
+ });
123
+ it("throws with 'null' when value is null", () => {
124
+ expect(() => requireHomogeneousLiteralArray("MyClass", null)).toThrow(/null/);
125
+ });
126
+ it("throws with 'non-empty array' on empty array", () => {
127
+ expect(() => requireHomogeneousLiteralArray("MyClass", [])).toThrow(/non-empty array/);
128
+ });
129
+ it("throws with 'null' when an element is null", () => {
130
+ expect(() => requireHomogeneousLiteralArray("MyClass", [null])).toThrow(/null/);
131
+ });
132
+ it("throws with 'undefined' when an element is undefined", () => {
133
+ expect(() => requireHomogeneousLiteralArray("MyClass", [undefined])).toThrow(/undefined/);
134
+ });
135
+ it("throws with 'object' when an element is an object", () => {
136
+ expect(() => requireHomogeneousLiteralArray("MyClass", [{}])).toThrow(/object/);
137
+ });
138
+ it("throws with mixed types message for ['a', 1] (left-to-right Set insertion order)", () => {
139
+ expect(() => requireHomogeneousLiteralArray("MyClass", ["a", 1])).toThrow(/mixed types \[string, number\]/);
140
+ });
141
+ it("throws when an element is NaN (would silently mismatch AttrLiteralIn/NotIn)", () => {
142
+ // Regression guard: AttrLiteralIn uses strict equality against each element;
143
+ // a NaN element could never match any attribute value because NaN !== NaN.
144
+ // Must fail-fast at construction rather than ship an always-false element.
145
+ expect(() => requireHomogeneousLiteralArray("MyClass", [1, Number.NaN, 3])).toThrow(/must not contain NaN/);
146
+ });
147
+ });
148
+ // ---------------------------------------------------------------------------
149
+ // requireCompareOp
150
+ // ---------------------------------------------------------------------------
151
+ describe("requireCompareOp", () => {
152
+ it("returns 'lt' for valid op 'lt'", () => {
153
+ expect(requireCompareOp("MyClass", "lt")).toBe("lt");
154
+ });
155
+ it("returns 'le' for valid op 'le'", () => {
156
+ expect(requireCompareOp("MyClass", "le")).toBe("le");
157
+ });
158
+ it("returns 'gt' for valid op 'gt'", () => {
159
+ expect(requireCompareOp("MyClass", "gt")).toBe("gt");
160
+ });
161
+ it("returns 'ge' for valid op 'ge'", () => {
162
+ expect(requireCompareOp("MyClass", "ge")).toBe("ge");
163
+ });
164
+ it("throws on invalid string 'eq' with message listing valid ops joined by ' | '", () => {
165
+ expect(() => requireCompareOp("MyClass", "eq")).toThrow(/lt \| le \| gt \| ge/);
166
+ });
167
+ it("throws on empty string '' with message listing valid ops", () => {
168
+ expect(() => requireCompareOp("MyClass", "")).toThrow(/lt \| le \| gt \| ge/);
169
+ });
170
+ it("throws on symbolic string '<' with message listing valid ops", () => {
171
+ expect(() => requireCompareOp("MyClass", "<")).toThrow(/lt \| le \| gt \| ge/);
172
+ });
173
+ it("throws on number 42 with message containing the type label", () => {
174
+ expect(() => requireCompareOp("MyClass", 42)).toThrow(/got number/);
175
+ });
176
+ it("throws on null with message containing the type label", () => {
177
+ expect(() => requireCompareOp("MyClass", null)).toThrow(/null/);
178
+ });
179
+ it("throws on undefined with message containing the type label", () => {
180
+ expect(() => requireCompareOp("MyClass", undefined)).toThrow(/undefined/);
181
+ });
182
+ });
183
+ // ---------------------------------------------------------------------------
184
+ // requireOptionalGroup
185
+ // ---------------------------------------------------------------------------
186
+ describe("requireOptionalGroup", () => {
187
+ it("returns undefined when value is undefined", () => {
188
+ expect(requireOptionalGroup("MyClass", undefined)).toBeUndefined();
189
+ });
190
+ it("returns the input string when value is a non-empty string", () => {
191
+ expect(requireOptionalGroup("MyClass", "my-group")).toBe("my-group");
192
+ });
193
+ it("throws with 'empty string' message on ''", () => {
194
+ expect(() => requireOptionalGroup("MyClass", "")).toThrow(/empty string/);
195
+ });
196
+ it("throws with 'got number' message on 42", () => {
197
+ expect(() => requireOptionalGroup("MyClass", 42)).toThrow(/got number/);
198
+ });
199
+ it("throws with 'null' in message on null", () => {
200
+ expect(() => requireOptionalGroup("MyClass", null)).toThrow(/null/);
201
+ });
202
+ });
203
+ // ---------------------------------------------------------------------------
204
+ // computeValuesKey
205
+ // ---------------------------------------------------------------------------
206
+ describe("computeValuesKey", () => {
207
+ it("returns a string matching /{type}:{count}:{8-hex-chars}/ format", () => {
208
+ expect(computeValuesKey(["admin"])).toMatch(/^(string|number|boolean):[0-9]+:[0-9a-f]{8}$/);
209
+ });
210
+ it("sort determinism: same content in different order produces the same key", () => {
211
+ expect(computeValuesKey(["b", "a"])).toBe(computeValuesKey(["a", "b"]));
212
+ });
213
+ it("count discrimination: ['a'] !== ['a', 'b'] (distinct content → distinct key)", () => {
214
+ expect(computeValuesKey(["a"])).not.toBe(computeValuesKey(["a", "b"]));
215
+ });
216
+ it("dedup parity: ['a'] === ['a', 'a'] (duplicates do not affect the Set-based rule behavior, so they must not affect ruleType either)", () => {
217
+ // AttrLiteralIn / AttrLiteralNotIn use `new Set(values)` for verify(),
218
+ // so duplicate elements have no semantic effect. The ruleType must
219
+ // mirror that — otherwise two logically-equivalent rules would be
220
+ // placed in different evaluator groups and be AND-combined, when
221
+ // the caller intended them to be OR-combined (shared ruleType).
222
+ expect(computeValuesKey(["a"])).toBe(computeValuesKey(["a", "a"]));
223
+ });
224
+ it("content discrimination: ['a', 'b'] !== ['a', 'c'] (hash differs)", () => {
225
+ expect(computeValuesKey(["a", "b"])).not.toBe(computeValuesKey(["a", "c"]));
226
+ });
227
+ it("type segment correctness: string array starts with 'string:'", () => {
228
+ expect(computeValuesKey(["admin"])).toMatch(/^string:/);
229
+ });
230
+ it("type segment correctness: number array starts with 'number:'", () => {
231
+ expect(computeValuesKey([1])).toMatch(/^number:/);
232
+ });
233
+ it("type segment correctness: boolean array starts with 'boolean:'", () => {
234
+ expect(computeValuesKey([true])).toMatch(/^boolean:/);
235
+ });
236
+ it("type divergence: computeValuesKey(['1']) !== computeValuesKey([1]) (type segment differs)", () => {
237
+ expect(computeValuesKey(["1"])).not.toBe(computeValuesKey([1]));
238
+ });
239
+ it("separator collision guard: ['a,b', 'c'] !== ['a', 'b,c'] (values containing commas do not collide)", () => {
240
+ // Regression guard: a naive implementation of the form
241
+ // values.map(String).sort().join(",")
242
+ // would produce the same string "a,b,c" for both inputs and thus the
243
+ // same ruleType. That would cause the evaluator to OR what the caller
244
+ // meant as two independent set constraints, weakening authorization.
245
+ // The canonical form JSON.stringify-es each element before joining,
246
+ // so quotes/escapes disambiguate the element boundary.
247
+ expect(computeValuesKey(["a,b", "c"])).not.toBe(computeValuesKey(["a", "b,c"]));
248
+ });
249
+ it("pure / referentially transparent: two calls with the same input return the same string", () => {
250
+ const input = ["x", "y", "z"];
251
+ expect(computeValuesKey(input)).toBe(computeValuesKey(input));
252
+ });
253
+ });
254
+ // ---------------------------------------------------------------------------
255
+ // applyCompare
256
+ // ---------------------------------------------------------------------------
257
+ describe("applyCompare", () => {
258
+ describe("op=lt", () => {
259
+ it("returns true when left < right (1 < 2)", () => {
260
+ expect(applyCompare("lt", 1, 2)).toBe(true);
261
+ });
262
+ it("returns false when left === right (2 < 2 is false)", () => {
263
+ expect(applyCompare("lt", 2, 2)).toBe(false);
264
+ });
265
+ it("returns false when left > right (3 < 2 is false)", () => {
266
+ expect(applyCompare("lt", 3, 2)).toBe(false);
267
+ });
268
+ it("edge: -Infinity < 0 returns true", () => {
269
+ expect(applyCompare("lt", -Infinity, 0)).toBe(true);
270
+ });
271
+ });
272
+ describe("op=le", () => {
273
+ it("returns true when left < right (1 <= 2)", () => {
274
+ expect(applyCompare("le", 1, 2)).toBe(true);
275
+ });
276
+ it("returns true when left === right (2 <= 2)", () => {
277
+ expect(applyCompare("le", 2, 2)).toBe(true);
278
+ });
279
+ it("returns false when left > right (3 <= 2 is false)", () => {
280
+ expect(applyCompare("le", 3, 2)).toBe(false);
281
+ });
282
+ it("edge: 0 >= -0 returns true (ge symmetry check; 0 <= -0 also true)", () => {
283
+ expect(applyCompare("le", 0, -0)).toBe(true);
284
+ });
285
+ });
286
+ describe("op=gt", () => {
287
+ it("returns true when left > right (3 > 2)", () => {
288
+ expect(applyCompare("gt", 3, 2)).toBe(true);
289
+ });
290
+ it("returns false when left === right (2 > 2 is false)", () => {
291
+ expect(applyCompare("gt", 2, 2)).toBe(false);
292
+ });
293
+ it("returns false when left < right (1 > 2 is false)", () => {
294
+ expect(applyCompare("gt", 1, 2)).toBe(false);
295
+ });
296
+ it("edge: Infinity > 1e308 returns true", () => {
297
+ expect(applyCompare("gt", Infinity, 1e308)).toBe(true);
298
+ });
299
+ });
300
+ describe("op=ge", () => {
301
+ it("returns true when left > right (3 >= 2)", () => {
302
+ expect(applyCompare("ge", 3, 2)).toBe(true);
303
+ });
304
+ it("returns true when left === right (2 >= 2)", () => {
305
+ expect(applyCompare("ge", 2, 2)).toBe(true);
306
+ });
307
+ it("returns false when left < right (1 >= 2 is false)", () => {
308
+ expect(applyCompare("ge", 1, 2)).toBe(false);
309
+ });
310
+ it("edge: 0 >= -0 returns true", () => {
311
+ expect(applyCompare("ge", 0, -0)).toBe(true);
312
+ });
313
+ });
314
+ });
315
+ // ---------------------------------------------------------------------------
316
+ // COMPARE_OPS
317
+ // ---------------------------------------------------------------------------
318
+ describe("COMPARE_OPS", () => {
319
+ it("exposes exactly 4 values", () => {
320
+ expect(COMPARE_OPS).toHaveLength(4);
321
+ });
322
+ it("contains 'lt', 'le', 'gt', 'ge' in order", () => {
323
+ expect(COMPARE_OPS).toEqual(["lt", "le", "gt", "ge"]);
324
+ });
325
+ });
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResourceActionPermissionRuleCollector.test.d.mts","sourceRoot":"","sources":["../../../../src/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.mts"],"names":[],"mappings":""}