@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.
- package/README.ja.md +104 -2
- package/README.md +102 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralCompare.test.mjs +253 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralEqual.test.mjs +138 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralIn.test.mjs +160 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralNotEqual.test.mjs +157 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrLiteralNotIn.test.mjs +168 -0
- package/dist/__tests__/rules/AttrMatchRule.test.mjs +8 -0
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairCompare.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairCompare.test.mjs +319 -0
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairEqual.test.mjs +155 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts +2 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.d.mts.map +1 -0
- package/dist/__tests__/rules/AttrPairNotEqual.test.mjs +155 -0
- package/dist/__tests__/rules/_sharedValidation.test.d.mts +2 -0
- package/dist/__tests__/rules/_sharedValidation.test.d.mts.map +1 -0
- package/dist/__tests__/rules/_sharedValidation.test.mjs +325 -0
- package/dist/__tests__/rules/collectors/ResourceActionPermissionRuleCollector.test.d.mts.map +1 -0
- package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.mjs → collectors/ResourceActionPermissionRuleCollector.test.mjs} +1 -1
- package/dist/__tests__/rules/collectors/ResourceActionScopeRuleCollector.test.d.mts.map +1 -0
- package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.mjs → collectors/ResourceActionScopeRuleCollector.test.mjs} +1 -1
- package/dist/index.d.mts +10 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/module.mjs +2 -2
- package/dist/rules/AttrLiteralCompare.d.mts +31 -0
- package/dist/rules/AttrLiteralCompare.d.mts.map +1 -0
- package/dist/rules/AttrLiteralCompare.mjs +36 -0
- package/dist/rules/AttrLiteralEqual.d.mts +37 -0
- package/dist/rules/AttrLiteralEqual.d.mts.map +1 -0
- package/dist/rules/AttrLiteralEqual.mjs +43 -0
- package/dist/rules/AttrLiteralIn.d.mts +38 -0
- package/dist/rules/AttrLiteralIn.d.mts.map +1 -0
- package/dist/rules/AttrLiteralIn.mjs +46 -0
- package/dist/rules/AttrLiteralNotEqual.d.mts +36 -0
- package/dist/rules/AttrLiteralNotEqual.d.mts.map +1 -0
- package/dist/rules/AttrLiteralNotEqual.mjs +42 -0
- package/dist/rules/AttrLiteralNotIn.d.mts +33 -0
- package/dist/rules/AttrLiteralNotIn.d.mts.map +1 -0
- package/dist/rules/AttrLiteralNotIn.mjs +41 -0
- package/dist/rules/AttrMatchRule.d.mts +7 -34
- package/dist/rules/AttrMatchRule.d.mts.map +1 -1
- package/dist/rules/AttrMatchRule.mjs +7 -32
- package/dist/rules/AttrPairCompare.d.mts +35 -0
- package/dist/rules/AttrPairCompare.d.mts.map +1 -0
- package/dist/rules/AttrPairCompare.mjs +43 -0
- package/dist/rules/AttrPairEqual.d.mts +35 -0
- package/dist/rules/AttrPairEqual.d.mts.map +1 -0
- package/dist/rules/AttrPairEqual.mjs +44 -0
- package/dist/rules/AttrPairNotEqual.d.mts +34 -0
- package/dist/rules/AttrPairNotEqual.d.mts.map +1 -0
- package/dist/rules/AttrPairNotEqual.mjs +43 -0
- package/dist/rules/_sharedValidation.d.mts +87 -0
- package/dist/rules/_sharedValidation.d.mts.map +1 -0
- package/dist/rules/_sharedValidation.mjs +189 -0
- package/dist/rules/collectors/ResourceActionPermissionRuleCollector.d.mts.map +1 -0
- package/dist/rules/{ResourceActionPermissionRuleCollector.mjs → collectors/ResourceActionPermissionRuleCollector.mjs} +1 -1
- package/dist/rules/collectors/ResourceActionScopeRuleCollector.d.mts.map +1 -0
- package/dist/rules/{ResourceActionScopeRuleCollector.mjs → collectors/ResourceActionScopeRuleCollector.mjs} +1 -1
- package/package.json +5 -2
- package/dist/__tests__/rules/ResourceActionPermissionRuleCollector.test.d.mts.map +0 -1
- package/dist/__tests__/rules/ResourceActionScopeRuleCollector.test.d.mts.map +0 -1
- package/dist/rules/ResourceActionPermissionRuleCollector.d.mts.map +0 -1
- package/dist/rules/ResourceActionScopeRuleCollector.d.mts.map +0 -1
- /package/dist/__tests__/rules/{ResourceActionPermissionRuleCollector.test.d.mts → collectors/ResourceActionPermissionRuleCollector.test.d.mts} +0 -0
- /package/dist/__tests__/rules/{ResourceActionScopeRuleCollector.test.d.mts → collectors/ResourceActionScopeRuleCollector.test.d.mts} +0 -0
- /package/dist/rules/{ResourceActionPermissionRuleCollector.d.mts → collectors/ResourceActionPermissionRuleCollector.d.mts} +0 -0
- /package/dist/rules/{ResourceActionScopeRuleCollector.d.mts → collectors/ResourceActionScopeRuleCollector.d.mts} +0 -0
|
@@ -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
|
+
});
|
|
@@ -48,6 +48,14 @@ describe("AttrMatchRule", () => {
|
|
|
48
48
|
expect(r1.ruleType).toContain("userId");
|
|
49
49
|
expect(r1.ruleType).toContain("sub");
|
|
50
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
|
+
});
|
|
51
59
|
it("uses the provided group override when set, to enable explicit grouping", () => {
|
|
52
60
|
// When callers want two comparisons to be OR'd together (e.g. match by
|
|
53
61
|
// either DID or email), they opt in by passing the same `group`.
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"AttrPairEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrPairEqual.test.mts"],"names":[],"mappings":""}
|