@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,138 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AttrLiteralEqual } from "#/rules/AttrLiteralEqual.mjs";
|
|
3
|
+
describe("AttrLiteralEqual", () => {
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Happy path: verify() returns true
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
it("returns true when attr equals the configured string literal", () => {
|
|
8
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
9
|
+
const attrs = new Map([["role", "admin"]]);
|
|
10
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("returns true when attr equals the configured number literal", () => {
|
|
13
|
+
const rule = new AttrLiteralEqual({ a: "age", v: 30 });
|
|
14
|
+
const attrs = new Map([["age", 30]]);
|
|
15
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("returns true when attr equals the configured boolean literal", () => {
|
|
18
|
+
const rule = new AttrLiteralEqual({ a: "verified", v: true });
|
|
19
|
+
const attrs = new Map([["verified", true]]);
|
|
20
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Type mismatch: no coercion
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
it("returns false when number literal v=30 but attr is the string '30' (no coercion)", () => {
|
|
26
|
+
const rule = new AttrLiteralEqual({ a: "age", v: 30 });
|
|
27
|
+
const attrs = new Map([["age", "30"]]);
|
|
28
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("returns false when string literal v='true' but attr is the boolean true (no coercion)", () => {
|
|
31
|
+
const rule = new AttrLiteralEqual({ a: "verified", v: "true" });
|
|
32
|
+
const attrs = new Map([["verified", true]]);
|
|
33
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Evaluation-time safe-deny: missing / null attr
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
it("returns false when the target attribute is missing", () => {
|
|
39
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
40
|
+
const attrs = new Map();
|
|
41
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it("returns false when the target attribute is null", () => {
|
|
44
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
45
|
+
const attrs = new Map([["role", null]]);
|
|
46
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Construction errors: invalid 'a'
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
it("throws when 'a' is undefined", () => {
|
|
52
|
+
expect(() => new AttrLiteralEqual({ a: undefined, v: "x" })).toThrow(/AttrLiteralEqual.*'a'.*(non-empty string|got undefined)/);
|
|
53
|
+
});
|
|
54
|
+
it("throws when 'a' is an empty string", () => {
|
|
55
|
+
expect(() => new AttrLiteralEqual({ a: "", v: "x" })).toThrow(/AttrLiteralEqual.*'a'.*empty string/);
|
|
56
|
+
});
|
|
57
|
+
it("throws when 'a' is a non-string (number 42)", () => {
|
|
58
|
+
expect(() => new AttrLiteralEqual({ a: 42, v: "x" })).toThrow(/AttrLiteralEqual.*'a'.*got number/);
|
|
59
|
+
});
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Construction errors: invalid 'v'
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
it("throws when 'v' is null", () => {
|
|
64
|
+
expect(() => new AttrLiteralEqual({ a: "role", v: null })).toThrow(/AttrLiteralEqual.*'v'.*got null/);
|
|
65
|
+
});
|
|
66
|
+
it("throws when 'v' is undefined", () => {
|
|
67
|
+
expect(() => new AttrLiteralEqual({ a: "role", v: undefined })).toThrow(/AttrLiteralEqual.*'v'.*got undefined/);
|
|
68
|
+
});
|
|
69
|
+
it("throws when 'v' is a plain object ({})", () => {
|
|
70
|
+
expect(() => new AttrLiteralEqual({ a: "role", v: {} })).toThrow(/AttrLiteralEqual.*'v'.*got object/);
|
|
71
|
+
});
|
|
72
|
+
it("throws when 'v' is NaN", () => {
|
|
73
|
+
// NaN === NaN is always false, so AttrLiteralEqual({ v: NaN }) would be
|
|
74
|
+
// an always-deny rule. Must fail-fast at construction rather than ship
|
|
75
|
+
// a silently-broken predicate.
|
|
76
|
+
expect(() => new AttrLiteralEqual({ a: "score", v: Number.NaN })).toThrow(/must not be NaN/);
|
|
77
|
+
});
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Construction errors: invalid 'group'
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
it("throws when 'group' is present but is an empty string", () => {
|
|
82
|
+
expect(() => new AttrLiteralEqual({ a: "role", v: "admin", group: "" })).toThrow(/AttrLiteralEqual.*'group'.*empty string/);
|
|
83
|
+
});
|
|
84
|
+
it("throws when 'group' is present but is a non-string (number 42)", () => {
|
|
85
|
+
expect(() => new AttrLiteralEqual({ a: "role", v: "admin", group: 42 })).toThrow(/AttrLiteralEqual.*'group'.*got number/);
|
|
86
|
+
});
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// ruleType: default derived from 'a', 'typeof v', and 'String(v)'
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
it("derives default ruleType including the type segment for a string literal", () => {
|
|
91
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
92
|
+
expect(rule.ruleType).toBe("attr_literal_equal:role:string:admin");
|
|
93
|
+
});
|
|
94
|
+
it("derives default ruleType including the type segment for a number literal", () => {
|
|
95
|
+
const rule = new AttrLiteralEqual({ a: "age", v: 30 });
|
|
96
|
+
expect(rule.ruleType).toBe("attr_literal_equal:age:number:30");
|
|
97
|
+
});
|
|
98
|
+
it("derives default ruleType including the type segment for a boolean literal", () => {
|
|
99
|
+
const rule = new AttrLiteralEqual({ a: "verified", v: true });
|
|
100
|
+
expect(rule.ruleType).toBe("attr_literal_equal:verified:boolean:true");
|
|
101
|
+
});
|
|
102
|
+
it("distinct configs produce distinct default ruleTypes (AND semantics)", () => {
|
|
103
|
+
const r1 = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
104
|
+
const r2 = new AttrLiteralEqual({ a: "role", v: "editor" });
|
|
105
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
106
|
+
});
|
|
107
|
+
it("distinguishes literals by type in ruleType (e.g. boolean true vs string 'true')", () => {
|
|
108
|
+
// Regression guard: without the typeof-segment in ruleType, these two
|
|
109
|
+
// rules would share the same default ruleType and the evaluator would
|
|
110
|
+
// silently OR them together, weakening authorization. The type tag
|
|
111
|
+
// keeps them AND-combined as intended.
|
|
112
|
+
const r1 = new AttrLiteralEqual({ a: "status", v: true });
|
|
113
|
+
const r2 = new AttrLiteralEqual({ a: "status", v: "true" });
|
|
114
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
115
|
+
});
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// ruleType: group override
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
it("uses the provided group string as ruleType, overriding the default", () => {
|
|
120
|
+
const r1 = new AttrLiteralEqual({ a: "role", v: "admin", group: "any-admin-role" });
|
|
121
|
+
const r2 = new AttrLiteralEqual({ a: "role", v: "superuser", group: "any-admin-role" });
|
|
122
|
+
expect(r1.ruleType).toBe("any-admin-role");
|
|
123
|
+
expect(r2.ruleType).toBe("any-admin-role");
|
|
124
|
+
});
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// code and message fields
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
it("has code 'attr_not_equal'", () => {
|
|
129
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
130
|
+
expect(rule.code).toBe("attr_not_equal");
|
|
131
|
+
});
|
|
132
|
+
it("message contains the attribute name and literal value, and starts with a capital letter", () => {
|
|
133
|
+
const rule = new AttrLiteralEqual({ a: "role", v: "admin" });
|
|
134
|
+
expect(rule.message).toContain("role");
|
|
135
|
+
expect(rule.message).toContain("admin");
|
|
136
|
+
expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralIn.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralIn.test.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AttrLiteralIn } from "#/rules/AttrLiteralIn.mjs";
|
|
3
|
+
describe("AttrLiteralIn", () => {
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Happy path: attr in set → true
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
it("returns true when attr is a string in the configured values set", () => {
|
|
8
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin", "editor", "viewer"] });
|
|
9
|
+
const attrs = new Map([["role", "editor"]]);
|
|
10
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("returns true when attr is a number in the configured values set", () => {
|
|
13
|
+
const rule = new AttrLiteralIn({ a: "level", values: [1, 2, 3] });
|
|
14
|
+
const attrs = new Map([["level", 2]]);
|
|
15
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("returns true when attr is a boolean in the configured values set", () => {
|
|
18
|
+
const rule = new AttrLiteralIn({ a: "active", values: [true] });
|
|
19
|
+
const attrs = new Map([["active", true]]);
|
|
20
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Happy path: attr not in set → false
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
it("returns false when attr is a string not in the configured values set", () => {
|
|
26
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
27
|
+
const attrs = new Map([["role", "viewer"]]);
|
|
28
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("returns false when attr is a number not in the configured values set", () => {
|
|
31
|
+
const rule = new AttrLiteralIn({ a: "level", values: [1, 2, 3] });
|
|
32
|
+
const attrs = new Map([["level", 5]]);
|
|
33
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Type mismatch: values type != attr type → safe-deny (no throw)
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
it("returns false when values are strings but attr is a number (type mismatch)", () => {
|
|
39
|
+
const rule = new AttrLiteralIn({ a: "x", values: ["a", "b"] });
|
|
40
|
+
const attrs = new Map([["x", 42]]);
|
|
41
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it("returns false when values are numbers but attr is a string (type mismatch)", () => {
|
|
44
|
+
const rule = new AttrLiteralIn({ a: "x", values: [1, 2, 3] });
|
|
45
|
+
const attrs = new Map([["x", "1"]]);
|
|
46
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Evaluation-time safe-deny: missing / null attr
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
it("returns false when the target attribute is missing", () => {
|
|
52
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin"] });
|
|
53
|
+
const attrs = new Map();
|
|
54
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("returns false when the target attribute is null", () => {
|
|
57
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin"] });
|
|
58
|
+
const attrs = new Map([["role", null]]);
|
|
59
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Construction errors: invalid 'a'
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
it("throws when 'a' is undefined", () => {
|
|
65
|
+
expect(() => new AttrLiteralIn({ a: undefined, values: ["x"] })).toThrow(/AttrLiteralIn.*'a'.*(non-empty string|got undefined)/);
|
|
66
|
+
});
|
|
67
|
+
it("throws when 'a' is an empty string", () => {
|
|
68
|
+
expect(() => new AttrLiteralIn({ a: "", values: ["x"] })).toThrow(/AttrLiteralIn.*'a'.*empty string/);
|
|
69
|
+
});
|
|
70
|
+
it("throws when 'a' is a non-string (number 42)", () => {
|
|
71
|
+
expect(() => new AttrLiteralIn({ a: 42, values: ["x"] })).toThrow(/AttrLiteralIn.*'a'.*got number/);
|
|
72
|
+
});
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Construction errors: invalid 'values'
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
it("throws when 'values' is missing (undefined)", () => {
|
|
77
|
+
expect(() => new AttrLiteralIn({ a: "role", values: undefined })).toThrow(/AttrLiteralIn.*'values'/);
|
|
78
|
+
});
|
|
79
|
+
it("throws when 'values' is not an array (a string)", () => {
|
|
80
|
+
expect(() => new AttrLiteralIn({ a: "role", values: "admin" })).toThrow(/AttrLiteralIn.*'values'/);
|
|
81
|
+
});
|
|
82
|
+
it("throws when 'values' is an empty array", () => {
|
|
83
|
+
expect(() => new AttrLiteralIn({ a: "role", values: [] })).toThrow(/AttrLiteralIn.*'values'/);
|
|
84
|
+
});
|
|
85
|
+
it("throws when 'values' contains null", () => {
|
|
86
|
+
expect(() => new AttrLiteralIn({ a: "role", values: [null] })).toThrow(/AttrLiteralIn.*'values'/);
|
|
87
|
+
});
|
|
88
|
+
it("throws when 'values' contains undefined", () => {
|
|
89
|
+
expect(() => new AttrLiteralIn({ a: "role", values: [undefined] })).toThrow(/AttrLiteralIn.*'values'/);
|
|
90
|
+
});
|
|
91
|
+
it("throws when 'values' contains mixed types (string and number)", () => {
|
|
92
|
+
expect(() => new AttrLiteralIn({ a: "role", values: ["a", 1] })).toThrow(/AttrLiteralIn.*'values'.*mixed/);
|
|
93
|
+
});
|
|
94
|
+
it("throws when 'values' contains an object element", () => {
|
|
95
|
+
expect(() => new AttrLiteralIn({ a: "role", values: [{}] })).toThrow(/AttrLiteralIn.*'values'/);
|
|
96
|
+
});
|
|
97
|
+
it("throws when 'values' contains NaN", () => {
|
|
98
|
+
// A NaN element could never match any numeric attribute (NaN !== NaN)
|
|
99
|
+
// and would silently be a dead element in the set. Must fail-fast.
|
|
100
|
+
expect(() => new AttrLiteralIn({ a: "score", values: [1, Number.NaN, 3] })).toThrow(/AttrLiteralIn.*must not contain NaN/);
|
|
101
|
+
});
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Construction errors: invalid 'group'
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
it("throws when 'group' is present but is an empty string", () => {
|
|
106
|
+
expect(() => new AttrLiteralIn({ a: "role", values: ["admin"], group: "" })).toThrow(/AttrLiteralIn.*'group'.*empty string/);
|
|
107
|
+
});
|
|
108
|
+
it("throws when 'group' is present but is a non-string (number 42)", () => {
|
|
109
|
+
expect(() => new AttrLiteralIn({ a: "role", values: ["admin"], group: 42 })).toThrow(/AttrLiteralIn.*'group'.*got number/);
|
|
110
|
+
});
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// ruleType: default includes valuesKey
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
it("derives default ruleType containing the attribute name and valuesKey", () => {
|
|
115
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
116
|
+
expect(rule.ruleType).toMatch(/^attr_literal_in:role:string:2:[0-9a-f]{8}$/);
|
|
117
|
+
});
|
|
118
|
+
it("two instances with same 'a' but different values produce different ruleTypes", () => {
|
|
119
|
+
const r1 = new AttrLiteralIn({ a: "role", values: ["admin"] });
|
|
120
|
+
const r2 = new AttrLiteralIn({ a: "role", values: ["editor"] });
|
|
121
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
122
|
+
});
|
|
123
|
+
it("two instances with same 'a' and identical values (same order) produce same ruleType", () => {
|
|
124
|
+
const r1 = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
125
|
+
const r2 = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
126
|
+
expect(r1.ruleType).toBe(r2.ruleType);
|
|
127
|
+
});
|
|
128
|
+
it("two instances with same 'a' and same values in different order produce same ruleType (content-based sort)", () => {
|
|
129
|
+
const r1 = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
130
|
+
const r2 = new AttrLiteralIn({ a: "role", values: ["editor", "admin"] });
|
|
131
|
+
expect(r1.ruleType).toBe(r2.ruleType);
|
|
132
|
+
});
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// ruleType: group override
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
it("uses the provided group string as ruleType, overriding the valuesKey-based default", () => {
|
|
137
|
+
const r1 = new AttrLiteralIn({ a: "role", values: ["admin"], group: "any-role" });
|
|
138
|
+
const r2 = new AttrLiteralIn({ a: "role", values: ["editor"], group: "any-role" });
|
|
139
|
+
expect(r1.ruleType).toBe("any-role");
|
|
140
|
+
expect(r2.ruleType).toBe("any-role");
|
|
141
|
+
});
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// code and message fields
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
it("has code 'attr_not_in_set'", () => {
|
|
146
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin"] });
|
|
147
|
+
expect(rule.code).toBe("attr_not_in_set");
|
|
148
|
+
});
|
|
149
|
+
it("message contains the attribute name, the 'must be one of' phrase, and lists all values", () => {
|
|
150
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin", "editor"] });
|
|
151
|
+
expect(rule.message).toContain("role");
|
|
152
|
+
expect(rule.message).toContain("must be one of");
|
|
153
|
+
expect(rule.message).toContain("admin");
|
|
154
|
+
expect(rule.message).toContain("editor");
|
|
155
|
+
});
|
|
156
|
+
it("message starts with a capital letter", () => {
|
|
157
|
+
const rule = new AttrLiteralIn({ a: "role", values: ["admin"] });
|
|
158
|
+
expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralNotEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralNotEqual.test.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AttrLiteralNotEqual } from "#/rules/AttrLiteralNotEqual.mjs";
|
|
3
|
+
describe("AttrLiteralNotEqual", () => {
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Happy path: verify() returns true (attr present, type matches, not equal)
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
it("returns true when attr is a different string than the configured literal", () => {
|
|
8
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
9
|
+
const attrs = new Map([["role", "editor"]]);
|
|
10
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("returns true when attr is a different number than the configured literal", () => {
|
|
13
|
+
const rule = new AttrLiteralNotEqual({ a: "age", v: 30 });
|
|
14
|
+
const attrs = new Map([["age", 25]]);
|
|
15
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("returns true when attr is a different boolean than the configured literal", () => {
|
|
18
|
+
const rule = new AttrLiteralNotEqual({ a: "verified", v: true });
|
|
19
|
+
const attrs = new Map([["verified", false]]);
|
|
20
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// verify() returns false when attr equals the literal
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
it("returns false when attr equals the configured string literal", () => {
|
|
26
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
27
|
+
const attrs = new Map([["role", "admin"]]);
|
|
28
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("returns false when attr equals the configured number literal", () => {
|
|
31
|
+
const rule = new AttrLiteralNotEqual({ a: "age", v: 30 });
|
|
32
|
+
const attrs = new Map([["age", 30]]);
|
|
33
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it("returns false when attr equals the configured boolean literal", () => {
|
|
36
|
+
const rule = new AttrLiteralNotEqual({ a: "verified", v: false });
|
|
37
|
+
const attrs = new Map([["verified", false]]);
|
|
38
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Type mismatch: safe-deny (no coercion)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
it("returns false when number literal v=30 but attr is the string '30' (no coercion)", () => {
|
|
44
|
+
const rule = new AttrLiteralNotEqual({ a: "age", v: 30 });
|
|
45
|
+
const attrs = new Map([["age", "30"]]);
|
|
46
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
it("returns false when string literal v='true' but attr is the boolean true (no coercion)", () => {
|
|
49
|
+
const rule = new AttrLiteralNotEqual({ a: "verified", v: "true" });
|
|
50
|
+
const attrs = new Map([["verified", true]]);
|
|
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 AttrLiteralNotEqual({ a: "role", v: "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 AttrLiteralNotEqual({ a: "role", v: "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 AttrLiteralNotEqual({ a: undefined, v: "x" })).toThrow(/AttrLiteralNotEqual.*'a'.*(non-empty string|got undefined)/);
|
|
71
|
+
});
|
|
72
|
+
it("throws when 'a' is an empty string", () => {
|
|
73
|
+
expect(() => new AttrLiteralNotEqual({ a: "", v: "x" })).toThrow(/AttrLiteralNotEqual.*'a'.*empty string/);
|
|
74
|
+
});
|
|
75
|
+
it("throws when 'a' is a non-string (number 42)", () => {
|
|
76
|
+
expect(() => new AttrLiteralNotEqual({ a: 42, v: "x" })).toThrow(/AttrLiteralNotEqual.*'a'.*got number/);
|
|
77
|
+
});
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Construction errors: invalid 'v'
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
it("throws when 'v' is null", () => {
|
|
82
|
+
expect(() => new AttrLiteralNotEqual({ a: "role", v: null })).toThrow(/AttrLiteralNotEqual.*'v'.*got null/);
|
|
83
|
+
});
|
|
84
|
+
it("throws when 'v' is undefined", () => {
|
|
85
|
+
expect(() => new AttrLiteralNotEqual({ a: "role", v: undefined })).toThrow(/AttrLiteralNotEqual.*'v'.*got undefined/);
|
|
86
|
+
});
|
|
87
|
+
it("throws when 'v' is a plain object ({})", () => {
|
|
88
|
+
expect(() => new AttrLiteralNotEqual({ a: "role", v: {} })).toThrow(/AttrLiteralNotEqual.*'v'.*got object/);
|
|
89
|
+
});
|
|
90
|
+
it("throws when 'v' is NaN", () => {
|
|
91
|
+
// x !== NaN is always true, so AttrLiteralNotEqual({ v: NaN }) would be
|
|
92
|
+
// an always-allow rule against any numeric attribute — silently
|
|
93
|
+
// weakening authorization. Must fail-fast at construction.
|
|
94
|
+
expect(() => new AttrLiteralNotEqual({ a: "score", v: Number.NaN })).toThrow(/must not be NaN/);
|
|
95
|
+
});
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Construction errors: invalid 'group'
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
it("throws when 'group' is present but is an empty string", () => {
|
|
100
|
+
expect(() => new AttrLiteralNotEqual({ a: "role", v: "admin", group: "" })).toThrow(/AttrLiteralNotEqual.*'group'.*empty string/);
|
|
101
|
+
});
|
|
102
|
+
it("throws when 'group' is present but is a non-string (number 42)", () => {
|
|
103
|
+
expect(() => new AttrLiteralNotEqual({ a: "role", v: "admin", group: 42 })).toThrow(/AttrLiteralNotEqual.*'group'.*got number/);
|
|
104
|
+
});
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// ruleType: default derived from 'a', 'typeof v', and 'String(v)'
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
it("derives default ruleType including the type segment for a string literal", () => {
|
|
109
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
110
|
+
expect(rule.ruleType).toBe("attr_literal_not_equal:role:string:admin");
|
|
111
|
+
});
|
|
112
|
+
it("derives default ruleType including the type segment for a number literal", () => {
|
|
113
|
+
const rule = new AttrLiteralNotEqual({ a: "age", v: 30 });
|
|
114
|
+
expect(rule.ruleType).toBe("attr_literal_not_equal:age:number:30");
|
|
115
|
+
});
|
|
116
|
+
it("derives default ruleType including the type segment for a boolean literal", () => {
|
|
117
|
+
const rule = new AttrLiteralNotEqual({ a: "verified", v: true });
|
|
118
|
+
expect(rule.ruleType).toBe("attr_literal_not_equal:verified:boolean:true");
|
|
119
|
+
});
|
|
120
|
+
it("distinct configs produce distinct default ruleTypes (AND semantics)", () => {
|
|
121
|
+
const r1 = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
122
|
+
const r2 = new AttrLiteralNotEqual({ a: "role", v: "editor" });
|
|
123
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
124
|
+
});
|
|
125
|
+
it("distinguishes literals by type in ruleType (e.g. number 1 vs string '1')", () => {
|
|
126
|
+
// Regression guard: see AttrLiteralEqual test for the same rationale.
|
|
127
|
+
const r1 = new AttrLiteralNotEqual({ a: "flag", v: 1 });
|
|
128
|
+
const r2 = new AttrLiteralNotEqual({ a: "flag", v: "1" });
|
|
129
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
130
|
+
});
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// ruleType: group override
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
it("uses the provided group string as ruleType, overriding the default", () => {
|
|
135
|
+
const r1 = new AttrLiteralNotEqual({ a: "role", v: "banned", group: "not-banned" });
|
|
136
|
+
const r2 = new AttrLiteralNotEqual({ a: "role", v: "blocked", group: "not-banned" });
|
|
137
|
+
expect(r1.ruleType).toBe("not-banned");
|
|
138
|
+
expect(r2.ruleType).toBe("not-banned");
|
|
139
|
+
});
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// code and message fields
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
it("has code 'attr_equal'", () => {
|
|
144
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
145
|
+
expect(rule.code).toBe("attr_equal");
|
|
146
|
+
});
|
|
147
|
+
it("message contains the attribute name and literal value, and starts with a capital letter", () => {
|
|
148
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
149
|
+
expect(rule.message).toContain("role");
|
|
150
|
+
expect(rule.message).toContain("admin");
|
|
151
|
+
expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
|
|
152
|
+
});
|
|
153
|
+
it("message contains 'must not equal'", () => {
|
|
154
|
+
const rule = new AttrLiteralNotEqual({ a: "role", v: "admin" });
|
|
155
|
+
expect(rule.message).toContain("must not equal");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralNotIn.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralNotIn.test.mts"],"names":[],"mappings":""}
|