@o3co/auth.policy-verifier.builtins 0.2.0 → 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.
- package/README.ja.md +101 -0
- package/README.md +101 -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/index.d.mts +8 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +8 -0
- 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/package.json +5 -2
|
@@ -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":""}
|
|
@@ -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":""}
|