@o3co/auth.policy-verifier.builtins 0.1.1 → 0.3.0

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 (79) hide show
  1. package/README.ja.md +116 -2
  2. package/README.md +116 -2
  3. package/dist/__tests__/module.test.mjs +0 -1
  4. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +2 -0
  5. package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +1 -0
  6. package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +253 -0
  7. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +2 -0
  8. package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +1 -0
  9. package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +138 -0
  10. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +2 -0
  11. package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +1 -0
  12. package/dist/__tests__/rules/AttrLiteralIn.test.mjs +160 -0
  13. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +2 -0
  14. package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +1 -0
  15. package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +157 -0
  16. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +2 -0
  17. package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +1 -0
  18. package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +168 -0
  19. package/dist/__tests__/rules/AttrMatchRule.test.d.mts +2 -0
  20. package/dist/__tests__/rules/AttrMatchRule.test.d.mts.map +1 -0
  21. package/dist/__tests__/rules/AttrMatchRule.test.mjs +79 -0
  22. package/dist/__tests__/rules/AttrPairCompare.test.d.mts +2 -0
  23. package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +1 -0
  24. package/dist/__tests__/rules/AttrPairCompare.test.mjs +319 -0
  25. package/dist/__tests__/rules/AttrPairEqual.test.d.mts +2 -0
  26. package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +1 -0
  27. package/dist/__tests__/rules/AttrPairEqual.test.mjs +155 -0
  28. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +2 -0
  29. package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +1 -0
  30. package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +155 -0
  31. package/dist/__tests__/rules/ResourceActionScopeRuleCollector.test.mjs +25 -4
  32. package/dist/__tests__/rules/_sharedValidation.test.d.mts +2 -0
  33. package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +1 -0
  34. package/dist/__tests__/rules/_sharedValidation.test.mjs +325 -0
  35. package/dist/index.d.mts +9 -1
  36. package/dist/index.d.mts.map +1 -1
  37. package/dist/index.mjs +9 -1
  38. package/dist/module.d.mts.map +1 -1
  39. package/dist/module.mjs +0 -2
  40. package/dist/rules/AttrLiteralCompare.d.mts +31 -0
  41. package/dist/rules/AttrLiteralCompare.d.mts.map +1 -0
  42. package/dist/rules/AttrLiteralCompare.mjs +36 -0
  43. package/dist/rules/AttrLiteralEqual.d.mts +37 -0
  44. package/dist/rules/AttrLiteralEqual.d.mts.map +1 -0
  45. package/dist/rules/AttrLiteralEqual.mjs +43 -0
  46. package/dist/rules/AttrLiteralIn.d.mts +38 -0
  47. package/dist/rules/AttrLiteralIn.d.mts.map +1 -0
  48. package/dist/rules/AttrLiteralIn.mjs +46 -0
  49. package/dist/rules/AttrLiteralNotEqual.d.mts +36 -0
  50. package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -0
  51. package/dist/rules/AttrLiteralNotEqual.mjs +42 -0
  52. package/dist/rules/AttrLiteralNotIn.d.mts +33 -0
  53. package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -0
  54. package/dist/rules/AttrLiteralNotIn.mjs +41 -0
  55. package/dist/rules/AttrMatchRule.d.mts +15 -0
  56. package/dist/rules/AttrMatchRule.d.mts.map +1 -0
  57. package/dist/rules/AttrMatchRule.mjs +17 -0
  58. package/dist/rules/AttrPairCompare.d.mts +35 -0
  59. package/dist/rules/AttrPairCompare.d.mts.map +1 -0
  60. package/dist/rules/AttrPairCompare.mjs +43 -0
  61. package/dist/rules/AttrPairEqual.d.mts +35 -0
  62. package/dist/rules/AttrPairEqual.d.mts.map +1 -0
  63. package/dist/rules/AttrPairEqual.mjs +44 -0
  64. package/dist/rules/AttrPairNotEqual.d.mts +34 -0
  65. package/dist/rules/AttrPairNotEqual.d.mts.map +1 -0
  66. package/dist/rules/AttrPairNotEqual.mjs +43 -0
  67. package/dist/rules/ResourceActionScopeRuleCollector.d.mts +9 -0
  68. package/dist/rules/ResourceActionScopeRuleCollector.d.mts.map +1 -1
  69. package/dist/rules/ResourceActionScopeRuleCollector.mjs +12 -0
  70. package/dist/rules/_sharedValidation.d.mts +87 -0
  71. package/dist/rules/_sharedValidation.d.mts.map +1 -0
  72. package/dist/rules/_sharedValidation.mjs +189 -0
  73. package/package.json +5 -2
  74. package/dist/__tests__/collectors/RequestContextCollector.test.d.mts +0 -2
  75. package/dist/__tests__/collectors/RequestContextCollector.test.d.mts.map +0 -1
  76. package/dist/__tests__/collectors/RequestContextCollector.test.mjs +0 -42
  77. package/dist/collectors/RequestContextCollector.d.mts +0 -5
  78. package/dist/collectors/RequestContextCollector.d.mts.map +0 -1
  79. package/dist/collectors/RequestContextCollector.mjs +0 -15
@@ -0,0 +1,168 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrLiteralNotIn } from "#/rules/AttrLiteralNotIn.mjs";
3
+ describe("AttrLiteralNotIn", () => {
4
+ // ---------------------------------------------------------------------------
5
+ // Happy path: attr not in set → true
6
+ // ---------------------------------------------------------------------------
7
+ it("returns true when attr is a string NOT in the configured values set", () => {
8
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
9
+ const attrs = new Map([["role", "viewer"]]);
10
+ expect(rule.verify(attrs)).toBe(true);
11
+ });
12
+ it("returns true when attr is a number NOT in the configured values set", () => {
13
+ const rule = new AttrLiteralNotIn({ a: "level", values: [1, 2, 3] });
14
+ const attrs = new Map([["level", 5]]);
15
+ expect(rule.verify(attrs)).toBe(true);
16
+ });
17
+ it("returns true when attr is a boolean NOT in the configured values set", () => {
18
+ const rule = new AttrLiteralNotIn({ a: "active", values: [false] });
19
+ const attrs = new Map([["active", true]]);
20
+ expect(rule.verify(attrs)).toBe(true);
21
+ });
22
+ // ---------------------------------------------------------------------------
23
+ // Happy path: attr in set → false
24
+ // ---------------------------------------------------------------------------
25
+ it("returns false when attr is a string IN the configured values set", () => {
26
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
27
+ const attrs = new Map([["role", "admin"]]);
28
+ expect(rule.verify(attrs)).toBe(false);
29
+ });
30
+ it("returns false when attr is a number IN the configured values set", () => {
31
+ const rule = new AttrLiteralNotIn({ a: "level", values: [1, 2, 3] });
32
+ const attrs = new Map([["level", 2]]);
33
+ expect(rule.verify(attrs)).toBe(false);
34
+ });
35
+ it("returns false when attr is a boolean IN the configured values set", () => {
36
+ const rule = new AttrLiteralNotIn({ a: "active", values: [true] });
37
+ const attrs = new Map([["active", true]]);
38
+ expect(rule.verify(attrs)).toBe(false);
39
+ });
40
+ // ---------------------------------------------------------------------------
41
+ // Type mismatch: safe-deny (no coercion)
42
+ // ---------------------------------------------------------------------------
43
+ it("returns false when values are strings but attr is a number (type mismatch)", () => {
44
+ const rule = new AttrLiteralNotIn({ a: "x", values: ["a", "b"] });
45
+ const attrs = new Map([["x", 42]]);
46
+ expect(rule.verify(attrs)).toBe(false);
47
+ });
48
+ it("returns false when values are numbers but attr is a string (type mismatch)", () => {
49
+ const rule = new AttrLiteralNotIn({ a: "x", values: [1, 2, 3] });
50
+ const attrs = new Map([["x", "1"]]);
51
+ expect(rule.verify(attrs)).toBe(false);
52
+ });
53
+ // ---------------------------------------------------------------------------
54
+ // Evaluation-time safe-deny: missing / null attr
55
+ // ---------------------------------------------------------------------------
56
+ it("returns false when the target attribute is missing", () => {
57
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
58
+ const attrs = new Map();
59
+ expect(rule.verify(attrs)).toBe(false);
60
+ });
61
+ it("returns false when the target attribute is null", () => {
62
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
63
+ const attrs = new Map([["role", null]]);
64
+ expect(rule.verify(attrs)).toBe(false);
65
+ });
66
+ // ---------------------------------------------------------------------------
67
+ // Construction errors: invalid 'a'
68
+ // ---------------------------------------------------------------------------
69
+ it("throws when 'a' is undefined", () => {
70
+ expect(() => new AttrLiteralNotIn({ a: undefined, values: ["x"] })).toThrow(/AttrLiteralNotIn.*'a'.*(non-empty string|got undefined)/);
71
+ });
72
+ it("throws when 'a' is an empty string", () => {
73
+ expect(() => new AttrLiteralNotIn({ a: "", values: ["x"] })).toThrow(/AttrLiteralNotIn.*'a'.*empty string/);
74
+ });
75
+ it("throws when 'a' is a non-string (number 42)", () => {
76
+ expect(() => new AttrLiteralNotIn({ a: 42, values: ["x"] })).toThrow(/AttrLiteralNotIn.*'a'.*got number/);
77
+ });
78
+ // ---------------------------------------------------------------------------
79
+ // Construction errors: invalid 'values'
80
+ // ---------------------------------------------------------------------------
81
+ it("throws when 'values' is missing (undefined)", () => {
82
+ expect(() => new AttrLiteralNotIn({ a: "role", values: undefined })).toThrow(/AttrLiteralNotIn.*'values'/);
83
+ });
84
+ it("throws when 'values' is not an array (a string)", () => {
85
+ expect(() => new AttrLiteralNotIn({ a: "role", values: "admin" })).toThrow(/AttrLiteralNotIn.*'values'/);
86
+ });
87
+ it("throws when 'values' is an empty array", () => {
88
+ expect(() => new AttrLiteralNotIn({ a: "role", values: [] })).toThrow(/AttrLiteralNotIn.*'values'/);
89
+ });
90
+ it("throws when 'values' contains null", () => {
91
+ expect(() => new AttrLiteralNotIn({ a: "role", values: [null] })).toThrow(/AttrLiteralNotIn.*'values'/);
92
+ });
93
+ it("throws when 'values' contains undefined", () => {
94
+ expect(() => new AttrLiteralNotIn({ a: "role", values: [undefined] })).toThrow(/AttrLiteralNotIn.*'values'/);
95
+ });
96
+ it("throws when 'values' contains mixed types (string and number)", () => {
97
+ expect(() => new AttrLiteralNotIn({ a: "role", values: ["a", 1] })).toThrow(/AttrLiteralNotIn.*'values'.*mixed/);
98
+ });
99
+ it("throws when 'values' contains an object element", () => {
100
+ expect(() => new AttrLiteralNotIn({ a: "role", values: [{}] })).toThrow(/AttrLiteralNotIn.*'values'/);
101
+ });
102
+ it("throws when 'values' contains NaN", () => {
103
+ // A NaN element could never match any numeric attribute (NaN !== NaN),
104
+ // so including it in a "deny these" list is a silent policy mistake.
105
+ expect(() => new AttrLiteralNotIn({ a: "score", values: [1, Number.NaN, 3] })).toThrow(/AttrLiteralNotIn.*must not contain NaN/);
106
+ });
107
+ // ---------------------------------------------------------------------------
108
+ // Construction errors: invalid 'group'
109
+ // ---------------------------------------------------------------------------
110
+ it("throws when 'group' is present but is an empty string", () => {
111
+ expect(() => new AttrLiteralNotIn({ a: "role", values: ["admin"], group: "" })).toThrow(/AttrLiteralNotIn.*'group'.*empty string/);
112
+ });
113
+ it("throws when 'group' is present but is a non-string (number 42)", () => {
114
+ expect(() => new AttrLiteralNotIn({ a: "role", values: ["admin"], group: 42 })).toThrow(/AttrLiteralNotIn.*'group'.*got number/);
115
+ });
116
+ // ---------------------------------------------------------------------------
117
+ // ruleType: default includes valuesKey
118
+ // ---------------------------------------------------------------------------
119
+ it("derives default ruleType containing the attribute name and valuesKey", () => {
120
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
121
+ expect(rule.ruleType).toMatch(/^attr_literal_not_in:role:string:2:[0-9a-f]{8}$/);
122
+ });
123
+ it("two instances with same 'a' but different values produce different ruleTypes", () => {
124
+ const r1 = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
125
+ const r2 = new AttrLiteralNotIn({ a: "role", values: ["editor"] });
126
+ expect(r1.ruleType).not.toBe(r2.ruleType);
127
+ });
128
+ it("two instances with same 'a' and identical values (same order) produce same ruleType", () => {
129
+ const r1 = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
130
+ const r2 = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
131
+ expect(r1.ruleType).toBe(r2.ruleType);
132
+ });
133
+ it("two instances with same 'a' and same values in different order produce same ruleType (content-based sort)", () => {
134
+ const r1 = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
135
+ const r2 = new AttrLiteralNotIn({ a: "role", values: ["editor", "admin"] });
136
+ expect(r1.ruleType).toBe(r2.ruleType);
137
+ });
138
+ // ---------------------------------------------------------------------------
139
+ // ruleType: group override
140
+ // ---------------------------------------------------------------------------
141
+ it("uses the provided group string as ruleType, overriding the valuesKey-based default", () => {
142
+ const r1 = new AttrLiteralNotIn({ a: "role", values: ["admin"], group: "not-admin-set" });
143
+ const r2 = new AttrLiteralNotIn({ a: "role", values: ["editor"], group: "not-admin-set" });
144
+ expect(r1.ruleType).toBe("not-admin-set");
145
+ expect(r2.ruleType).toBe("not-admin-set");
146
+ });
147
+ // ---------------------------------------------------------------------------
148
+ // code and message fields
149
+ // ---------------------------------------------------------------------------
150
+ it("has code 'attr_in_set'", () => {
151
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
152
+ expect(rule.code).toBe("attr_in_set");
153
+ });
154
+ it("message contains the attribute name and lists all values", () => {
155
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin", "editor"] });
156
+ expect(rule.message).toContain("role");
157
+ expect(rule.message).toContain("admin");
158
+ expect(rule.message).toContain("editor");
159
+ });
160
+ it("message contains 'must not be one of'", () => {
161
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
162
+ expect(rule.message).toContain("must not be one of");
163
+ });
164
+ it("message starts with a capital letter", () => {
165
+ const rule = new AttrLiteralNotIn({ a: "role", values: ["admin"] });
166
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
167
+ });
168
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrMatchRule.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrMatchRule.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrMatchRule.test.mts"],"names":[],"mappings":""}
@@ -0,0 +1,79 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrMatchRule } from "#/rules/AttrMatchRule.mjs";
3
+ describe("AttrMatchRule", () => {
4
+ const rule = new AttrMatchRule({ a: "x", b: "y" });
5
+ it("returns true when both attrs are non-empty strings and equal", () => {
6
+ const attrs = new Map([
7
+ ["x", "same"],
8
+ ["y", "same"],
9
+ ]);
10
+ expect(rule.verify(attrs)).toBe(true);
11
+ });
12
+ it("returns false when the two attrs differ", () => {
13
+ const attrs = new Map([
14
+ ["x", "foo"],
15
+ ["y", "bar"],
16
+ ]);
17
+ expect(rule.verify(attrs)).toBe(false);
18
+ });
19
+ it("returns false when the first attr is missing", () => {
20
+ const attrs = new Map([["y", "same"]]);
21
+ expect(rule.verify(attrs)).toBe(false);
22
+ });
23
+ it("returns false when the second attr is missing", () => {
24
+ const attrs = new Map([["x", "same"]]);
25
+ expect(rule.verify(attrs)).toBe(false);
26
+ });
27
+ it("returns false when either attr is a non-string value", () => {
28
+ const attrs = new Map([
29
+ ["x", 42],
30
+ ["y", 42],
31
+ ]);
32
+ expect(rule.verify(attrs)).toBe(false);
33
+ });
34
+ it("returns false when either attr is an empty string", () => {
35
+ const attrs = new Map([
36
+ ["x", ""],
37
+ ["y", ""],
38
+ ]);
39
+ expect(rule.verify(attrs)).toBe(false);
40
+ });
41
+ it("derives ruleType from the pair of attribute keys by default", () => {
42
+ // Distinct pairs must produce distinct ruleType values. Otherwise
43
+ // evaluate() would OR them together within a single group, which
44
+ // weakens "require both comparisons to pass" intent.
45
+ const r1 = new AttrMatchRule({ a: "userId", b: "sub" });
46
+ const r2 = new AttrMatchRule({ a: "orgId", b: "org" });
47
+ expect(r1.ruleType).not.toBe(r2.ruleType);
48
+ expect(r1.ruleType).toContain("userId");
49
+ expect(r1.ruleType).toContain("sub");
50
+ });
51
+ it("preserves the legacy ruleType prefix 'attr_match:' for backward compatibility", () => {
52
+ // AttrMatchRule is a deprecated wrapper around AttrPairEqual. To keep
53
+ // downstream consumers (e.g. dplaas.auth) working without code changes
54
+ // across this release, the default ruleType must remain `attr_match:{a}:{b}`
55
+ // rather than adopting AttrPairEqual's `attr_pair_equal:...` form.
56
+ const r = new AttrMatchRule({ a: "userId", b: "sub" });
57
+ expect(r.ruleType).toBe("attr_match:userId:sub");
58
+ });
59
+ it("uses the provided group override when set, to enable explicit grouping", () => {
60
+ // When callers want two comparisons to be OR'd together (e.g. match by
61
+ // either DID or email), they opt in by passing the same `group`.
62
+ const r1 = new AttrMatchRule({ a: "x", b: "y", group: "identity" });
63
+ const r2 = new AttrMatchRule({ a: "e", b: "f", group: "identity" });
64
+ expect(r1.ruleType).toBe("identity");
65
+ expect(r2.ruleType).toBe("identity");
66
+ });
67
+ it("has code 'attr_mismatch'", () => {
68
+ expect(rule.code).toBe("attr_mismatch");
69
+ });
70
+ it("message describes the contract (both attrs must be non-empty strings and equal)", () => {
71
+ // The message should make the rule's contract explicit, not just say "do not match",
72
+ // because verify() fails closed on missing, non-string, and empty inputs too.
73
+ expect(rule.message).toContain("x");
74
+ expect(rule.message).toContain("y");
75
+ expect(rule.message).toMatch(/non-empty/i);
76
+ expect(rule.message).toMatch(/equal/i);
77
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
78
+ });
79
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrPairCompare.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairCompare.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrPairCompare.test.mts"],"names":[],"mappings":""}
@@ -0,0 +1,319 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { AttrPairCompare } from "#/rules/AttrPairCompare.mjs";
3
+ describe("AttrPairCompare", () => {
4
+ // ---------------------------------------------------------------------------
5
+ // Happy path: each op at boundary values
6
+ // ---------------------------------------------------------------------------
7
+ it("op=lt: returns true when attr(a) < attr(b) (5 < 10)", () => {
8
+ const rule = new AttrPairCompare({ a: "low", op: "lt", b: "high" });
9
+ const attrs = new Map([
10
+ ["low", 5],
11
+ ["high", 10],
12
+ ]);
13
+ expect(rule.verify(attrs)).toBe(true);
14
+ });
15
+ it("op=lt: returns false when attr(a) === attr(b) (10 < 10 is false)", () => {
16
+ const rule = new AttrPairCompare({ a: "low", op: "lt", b: "high" });
17
+ const attrs = new Map([
18
+ ["low", 10],
19
+ ["high", 10],
20
+ ]);
21
+ expect(rule.verify(attrs)).toBe(false);
22
+ });
23
+ it("op=lt: returns false when attr(a) > attr(b) (15 < 10 is false)", () => {
24
+ const rule = new AttrPairCompare({ a: "low", op: "lt", b: "high" });
25
+ const attrs = new Map([
26
+ ["low", 15],
27
+ ["high", 10],
28
+ ]);
29
+ expect(rule.verify(attrs)).toBe(false);
30
+ });
31
+ it("op=le: returns true when attr(a) === attr(b) (10 <= 10)", () => {
32
+ const rule = new AttrPairCompare({ a: "low", op: "le", b: "high" });
33
+ const attrs = new Map([
34
+ ["low", 10],
35
+ ["high", 10],
36
+ ]);
37
+ expect(rule.verify(attrs)).toBe(true);
38
+ });
39
+ it("op=le: returns true when attr(a) < attr(b) (5 <= 10)", () => {
40
+ const rule = new AttrPairCompare({ a: "low", op: "le", b: "high" });
41
+ const attrs = new Map([
42
+ ["low", 5],
43
+ ["high", 10],
44
+ ]);
45
+ expect(rule.verify(attrs)).toBe(true);
46
+ });
47
+ it("op=le: returns false when attr(a) > attr(b) (15 <= 10 is false)", () => {
48
+ const rule = new AttrPairCompare({ a: "low", op: "le", b: "high" });
49
+ const attrs = new Map([
50
+ ["low", 15],
51
+ ["high", 10],
52
+ ]);
53
+ expect(rule.verify(attrs)).toBe(false);
54
+ });
55
+ it("op=gt: returns true when attr(a) > attr(b) (15 > 10)", () => {
56
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
57
+ const attrs = new Map([
58
+ ["score", 15],
59
+ ["threshold", 10],
60
+ ]);
61
+ expect(rule.verify(attrs)).toBe(true);
62
+ });
63
+ it("op=gt: returns false when attr(a) === attr(b) (10 > 10 is false)", () => {
64
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
65
+ const attrs = new Map([
66
+ ["score", 10],
67
+ ["threshold", 10],
68
+ ]);
69
+ expect(rule.verify(attrs)).toBe(false);
70
+ });
71
+ it("op=gt: returns false when attr(a) < attr(b) (5 > 10 is false)", () => {
72
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
73
+ const attrs = new Map([
74
+ ["score", 5],
75
+ ["threshold", 10],
76
+ ]);
77
+ expect(rule.verify(attrs)).toBe(false);
78
+ });
79
+ it("op=ge: returns true when attr(a) === attr(b) (10 >= 10)", () => {
80
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
81
+ const attrs = new Map([
82
+ ["score", 10],
83
+ ["threshold", 10],
84
+ ]);
85
+ expect(rule.verify(attrs)).toBe(true);
86
+ });
87
+ it("op=ge: returns true when attr(a) > attr(b) (15 >= 10)", () => {
88
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
89
+ const attrs = new Map([
90
+ ["score", 15],
91
+ ["threshold", 10],
92
+ ]);
93
+ expect(rule.verify(attrs)).toBe(true);
94
+ });
95
+ it("op=ge: returns false when attr(a) < attr(b) (5 >= 10 is false)", () => {
96
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
97
+ const attrs = new Map([
98
+ ["score", 5],
99
+ ["threshold", 10],
100
+ ]);
101
+ expect(rule.verify(attrs)).toBe(false);
102
+ });
103
+ // ---------------------------------------------------------------------------
104
+ // Evaluation-time safe-deny: attr is not a number
105
+ // ---------------------------------------------------------------------------
106
+ it("returns false when attr(a) is a string (not a number)", () => {
107
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
108
+ const attrs = new Map([
109
+ ["score", "10"],
110
+ ["threshold", 5],
111
+ ]);
112
+ expect(rule.verify(attrs)).toBe(false);
113
+ });
114
+ it("returns false when attr(b) is a string (not a number)", () => {
115
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
116
+ const attrs = new Map([
117
+ ["score", 10],
118
+ ["threshold", "5"],
119
+ ]);
120
+ expect(rule.verify(attrs)).toBe(false);
121
+ });
122
+ // ---------------------------------------------------------------------------
123
+ // Evaluation-time safe-deny: missing attr
124
+ // ---------------------------------------------------------------------------
125
+ it("returns false when attr(a) is missing", () => {
126
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
127
+ const attrs = new Map([["threshold", 5]]);
128
+ expect(rule.verify(attrs)).toBe(false);
129
+ });
130
+ it("returns false when attr(b) is missing", () => {
131
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
132
+ const attrs = new Map([["score", 10]]);
133
+ expect(rule.verify(attrs)).toBe(false);
134
+ });
135
+ // ---------------------------------------------------------------------------
136
+ // Evaluation-time: NaN attribute → false for all ops
137
+ // ---------------------------------------------------------------------------
138
+ it("op=lt: returns false when attr(a) is NaN", () => {
139
+ const rule = new AttrPairCompare({ a: "score", op: "lt", b: "threshold" });
140
+ const attrs = new Map([
141
+ ["score", NaN],
142
+ ["threshold", 10],
143
+ ]);
144
+ expect(rule.verify(attrs)).toBe(false);
145
+ });
146
+ it("op=le: returns false when attr(a) is NaN", () => {
147
+ const rule = new AttrPairCompare({ a: "score", op: "le", b: "threshold" });
148
+ const attrs = new Map([
149
+ ["score", NaN],
150
+ ["threshold", 10],
151
+ ]);
152
+ expect(rule.verify(attrs)).toBe(false);
153
+ });
154
+ it("op=gt: returns false when attr(a) is NaN", () => {
155
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
156
+ const attrs = new Map([
157
+ ["score", NaN],
158
+ ["threshold", 10],
159
+ ]);
160
+ expect(rule.verify(attrs)).toBe(false);
161
+ });
162
+ it("op=ge: returns false when attr(a) is NaN", () => {
163
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
164
+ const attrs = new Map([
165
+ ["score", NaN],
166
+ ["threshold", 10],
167
+ ]);
168
+ expect(rule.verify(attrs)).toBe(false);
169
+ });
170
+ it("op=lt: returns false when attr(b) is NaN", () => {
171
+ const rule = new AttrPairCompare({ a: "score", op: "lt", b: "threshold" });
172
+ const attrs = new Map([
173
+ ["score", 5],
174
+ ["threshold", NaN],
175
+ ]);
176
+ expect(rule.verify(attrs)).toBe(false);
177
+ });
178
+ it("op=le: returns false when attr(b) is NaN", () => {
179
+ const rule = new AttrPairCompare({ a: "score", op: "le", b: "threshold" });
180
+ const attrs = new Map([
181
+ ["score", 5],
182
+ ["threshold", NaN],
183
+ ]);
184
+ expect(rule.verify(attrs)).toBe(false);
185
+ });
186
+ it("op=gt: returns false when attr(b) is NaN", () => {
187
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
188
+ const attrs = new Map([
189
+ ["score", 15],
190
+ ["threshold", NaN],
191
+ ]);
192
+ expect(rule.verify(attrs)).toBe(false);
193
+ });
194
+ it("op=ge: returns false when attr(b) is NaN", () => {
195
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
196
+ const attrs = new Map([
197
+ ["score", 15],
198
+ ["threshold", NaN],
199
+ ]);
200
+ expect(rule.verify(attrs)).toBe(false);
201
+ });
202
+ // ---------------------------------------------------------------------------
203
+ // Evaluation-time: Infinity / -Infinity attributes
204
+ // ---------------------------------------------------------------------------
205
+ it("op=gt: Infinity > 100 returns true", () => {
206
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
207
+ const attrs = new Map([
208
+ ["score", Infinity],
209
+ ["threshold", 100],
210
+ ]);
211
+ expect(rule.verify(attrs)).toBe(true);
212
+ });
213
+ it("op=lt: -Infinity < 0 returns true", () => {
214
+ const rule = new AttrPairCompare({ a: "score", op: "lt", b: "threshold" });
215
+ const attrs = new Map([
216
+ ["score", -Infinity],
217
+ ["threshold", 0],
218
+ ]);
219
+ expect(rule.verify(attrs)).toBe(true);
220
+ });
221
+ it("op=ge: Infinity >= Infinity returns true", () => {
222
+ const rule = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
223
+ const attrs = new Map([
224
+ ["score", Infinity],
225
+ ["threshold", Infinity],
226
+ ]);
227
+ expect(rule.verify(attrs)).toBe(true);
228
+ });
229
+ // ---------------------------------------------------------------------------
230
+ // Construction errors: invalid 'a'
231
+ // ---------------------------------------------------------------------------
232
+ it("throws when 'a' is undefined", () => {
233
+ expect(() => new AttrPairCompare({ a: undefined, op: "gt", b: "threshold" })).toThrow(/AttrPairCompare.*'a'.*(non-empty string|got undefined)/);
234
+ });
235
+ it("throws when 'a' is a non-string (number 42)", () => {
236
+ expect(() => new AttrPairCompare({ a: 42, op: "gt", b: "threshold" })).toThrow(/AttrPairCompare.*'a'.*got number/);
237
+ });
238
+ it("throws when 'a' is an empty string", () => {
239
+ expect(() => new AttrPairCompare({ a: "", op: "gt", b: "threshold" })).toThrow(/AttrPairCompare.*'a'.*empty string/);
240
+ });
241
+ // ---------------------------------------------------------------------------
242
+ // Construction errors: invalid 'b'
243
+ // ---------------------------------------------------------------------------
244
+ it("throws when 'b' is undefined", () => {
245
+ expect(() => new AttrPairCompare({ a: "score", op: "gt", b: undefined })).toThrow(/AttrPairCompare.*'b'.*(non-empty string|got undefined)/);
246
+ });
247
+ it("throws when 'b' is a non-string (number 42)", () => {
248
+ expect(() => new AttrPairCompare({ a: "score", op: "gt", b: 42 })).toThrow(/AttrPairCompare.*'b'.*got number/);
249
+ });
250
+ it("throws when 'b' is an empty string", () => {
251
+ expect(() => new AttrPairCompare({ a: "score", op: "gt", b: "" })).toThrow(/AttrPairCompare.*'b'.*empty string/);
252
+ });
253
+ // ---------------------------------------------------------------------------
254
+ // Construction errors: invalid 'op'
255
+ // ---------------------------------------------------------------------------
256
+ it("throws when 'op' is missing (undefined)", () => {
257
+ expect(() => new AttrPairCompare({ a: "score", op: undefined, b: "threshold" })).toThrow(/AttrPairCompare.*'op'/);
258
+ });
259
+ it("throws when 'op' is not a string (number 42)", () => {
260
+ expect(() => new AttrPairCompare({ a: "score", op: 42, b: "threshold" })).toThrow(/AttrPairCompare.*'op'/);
261
+ });
262
+ it("throws when 'op' is an invalid string ('eq')", () => {
263
+ expect(() => new AttrPairCompare({ a: "score", op: "eq", b: "threshold" })).toThrow(/AttrPairCompare.*'op'/);
264
+ });
265
+ // ---------------------------------------------------------------------------
266
+ // Construction errors: invalid 'group'
267
+ // ---------------------------------------------------------------------------
268
+ it("throws when 'group' is present but is a non-string (number 42)", () => {
269
+ expect(() => new AttrPairCompare({
270
+ a: "score",
271
+ op: "gt",
272
+ b: "threshold",
273
+ group: 42,
274
+ })).toThrow(/AttrPairCompare.*'group'.*got number/);
275
+ });
276
+ it("throws when 'group' is present but is an empty string", () => {
277
+ expect(() => new AttrPairCompare({ a: "score", op: "gt", b: "threshold", group: "" })).toThrow(/AttrPairCompare.*'group'.*empty string/);
278
+ });
279
+ // ---------------------------------------------------------------------------
280
+ // ruleType: default derived from a, op, b
281
+ // ---------------------------------------------------------------------------
282
+ it("derives default ruleType from a, op, and b", () => {
283
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
284
+ expect(rule.ruleType).toBe("attr_pair_compare:score:gt:threshold");
285
+ });
286
+ it("distinct op or b produce distinct ruleTypes (AND semantics)", () => {
287
+ const r1 = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
288
+ const r2 = new AttrPairCompare({ a: "score", op: "ge", b: "threshold" });
289
+ const r3 = new AttrPairCompare({ a: "score", op: "gt", b: "limit" });
290
+ expect(r1.ruleType).not.toBe(r2.ruleType);
291
+ expect(r1.ruleType).not.toBe(r3.ruleType);
292
+ });
293
+ // ---------------------------------------------------------------------------
294
+ // ruleType: group override
295
+ // ---------------------------------------------------------------------------
296
+ it("uses the provided group string as ruleType, overriding the default", () => {
297
+ const r1 = new AttrPairCompare({ a: "score", op: "gt", b: "threshold", group: "score-check" });
298
+ const r2 = new AttrPairCompare({ a: "score", op: "ge", b: "limit", group: "score-check" });
299
+ expect(r1.ruleType).toBe("score-check");
300
+ expect(r2.ruleType).toBe("score-check");
301
+ });
302
+ // ---------------------------------------------------------------------------
303
+ // code and message fields
304
+ // ---------------------------------------------------------------------------
305
+ it("has code 'attr_compare_violated'", () => {
306
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
307
+ expect(rule.code).toBe("attr_compare_violated");
308
+ });
309
+ it("message contains a name, op, and b name", () => {
310
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
311
+ expect(rule.message).toContain("score");
312
+ expect(rule.message).toContain("gt");
313
+ expect(rule.message).toContain("threshold");
314
+ });
315
+ it("message starts with a capital letter", () => {
316
+ const rule = new AttrPairCompare({ a: "score", op: "gt", b: "threshold" });
317
+ expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
318
+ });
319
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AttrPairEqual.test.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AttrPairEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrPairEqual.test.mts"],"names":[],"mappings":""}