@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
package/README.ja.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
auth.policy-verifier 向けの組み込み attribute collector、rule collector、および resource parser です。
|
|
4
4
|
|
|
5
|
+
**Runtime:** Node.js 22+。本パッケージは `ruleType` の安定ハッシュ用に `node:crypto` を利用します。browser / edge ランタイム対応は今後の課題として別 issue で追跡します。
|
|
6
|
+
|
|
5
7
|
## インストール
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -54,6 +56,8 @@ new HasScope(scope: string)
|
|
|
54
56
|
|
|
55
57
|
### AttrMatchRule
|
|
56
58
|
|
|
59
|
+
**非推奨。** 代わりに [`AttrPairEqual`](#attrpairequal) を使用してください。`AttrMatchRule` は `AttrPairEqual` を継承した薄いラッパークラスとして残されており、後方互換のため旧来の `ruleType`(`attr_match:${a}:${b}`)と旧来の `message` 文言を保持します。型 `AttrMatchRuleConfig` は `AttrPairEqualConfig` の型エイリアスです。将来のメジャーバージョンで削除されます。
|
|
60
|
+
|
|
57
61
|
```ts
|
|
58
62
|
new AttrMatchRule({ a: string, b: string, group?: string })
|
|
59
63
|
```
|
|
@@ -63,6 +67,103 @@ new AttrMatchRule({ a: string, b: string, group?: string })
|
|
|
63
67
|
- 純粋な述語です。`CollectorContext` を参照しません。比較対象の値はプロジェクト側の上流 `AttributeCollector` が attrs に格納し、プロジェクト側の `RuleCollector` でこの Rule を構築します。
|
|
64
68
|
- `ruleType` の既定値は `"attr_match:${a}:${b}"` です。評価器は `ruleType` 内で OR、`ruleType` 間で AND を取るので、この既定値により異なる2つの比較は AND(両方必要)として扱われます。2つの比較を OR 結合したい場合(例「DID または email で一致」)は、両方の Rule に同じ `group` を指定してください。その `group` 値が `ruleType` として使われます。
|
|
65
69
|
|
|
70
|
+
## Attribute Comparison Rules
|
|
71
|
+
|
|
72
|
+
属性比較 Rule 群は 2 軸のマトリクスで構成されます: **family**(Literal または Pair)と **operator**(Equal、NotEqual、In、NotIn、Compare)。
|
|
73
|
+
|
|
74
|
+
- **Literal** Rule は、単一の名前付き属性を、コンストラクタで与えられた静的な値(または値集合)と比較します。
|
|
75
|
+
- **Pair** Rule は、評価時に `Attributes` から解決される 2 つの名前付き属性を互いに比較します。
|
|
76
|
+
- `In` / `NotIn` のバリアントは Literal family のみ提供します。集合に対する Pair 比較は有限リストへ自然に一般化できないため、`AttrPairIn` / `AttrPairNotIn` は意図的に存在しません。
|
|
77
|
+
|
|
78
|
+
| Family | Equal | NotEqual | In | NotIn | Compare |
|
|
79
|
+
| ------- | ------------------ | --------------------- | --------------- | ------------------ | -------------------- |
|
|
80
|
+
| Literal | `AttrLiteralEqual` | `AttrLiteralNotEqual` | `AttrLiteralIn` | `AttrLiteralNotIn` | `AttrLiteralCompare` |
|
|
81
|
+
| Pair | `AttrPairEqual` | `AttrPairNotEqual` | — | — | `AttrPairCompare` |
|
|
82
|
+
|
|
83
|
+
### AttrLiteralEqual
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
new AttrLiteralEqual({ a: string, v: string | number | boolean, group?: string })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `code`: `"attr_not_equal"`。
|
|
90
|
+
- 既定の `ruleType`: `` `attr_literal_equal:${a}:${typeof v}:${String(v)}` ``。`typeof v` セグメントは、文字列化すると同じになる異型リテラル(例: `true` と `"true"`)が同じ `ruleType` に畳み込まれるのを防ぎます。
|
|
91
|
+
- `attrs.get(a)` が `v` と同じ型で厳密等価のときに通過します。型の強制変換は行いません。
|
|
92
|
+
|
|
93
|
+
### AttrLiteralNotEqual
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
new AttrLiteralNotEqual({ a: string, v: string | number | boolean, group?: string })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `code`: `"attr_equal"`。
|
|
100
|
+
- 既定の `ruleType`: `` `attr_literal_not_equal:${a}:${typeof v}:${String(v)}` ``。`typeof v` セグメントは異型リテラル間の衝突を防ぎます(`AttrLiteralEqual` と同じ理由)。
|
|
101
|
+
- `attrs.get(a)` が `v` と同じ型で厳密等価でないときに通過します。欠落または型不一致は `false`(safe-deny)を返します。
|
|
102
|
+
|
|
103
|
+
### AttrLiteralIn
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: string })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `code`: `"attr_not_in_set"`。
|
|
110
|
+
- 既定の `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — `count` は重複除去後の要素数、`hashPrefix` は重複除去かつソート済みの `values` を文字列化した内容に対する SHA-256 の先頭 8 桁。同じ `a` と内容上等価な `values`(順序・重複を無視)を持つ 2 つのインスタンスは同じ `ruleType` を持ち、評価器で OR 結合されます。
|
|
111
|
+
- `values` は非空かつ同種の配列(`string[]`、`number[]`、`boolean[]` のいずれか)でなければなりません。`attrs.get(a)` が集合に含まれるときに通過します。`values` 内の重複要素は Rule の挙動に影響しません(内部では `Set` として扱われます)。
|
|
112
|
+
|
|
113
|
+
### AttrLiteralNotIn
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
new AttrLiteralNotIn({ a: string, values: (string | number | boolean)[], group?: string })
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `code`: `"attr_in_set"`。
|
|
120
|
+
- 既定の `ruleType`: `` `attr_literal_not_in:${a}:${type}:${count}:${hashPrefix}` `` — `AttrLiteralIn` と同じ、重複除去済みの安定ハッシュ方式。
|
|
121
|
+
- `values` は非空かつ同種の配列。`attrs.get(a)` が集合に含まれないときに通過します。`values` 内の重複要素は Rule の挙動に影響しません。
|
|
122
|
+
|
|
123
|
+
### AttrLiteralCompare
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
new AttrLiteralCompare({ a: string, op: "lt" | "le" | "gt" | "ge", v: number, group?: string })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `code`: `"attr_compare_violated"`。
|
|
130
|
+
- 既定の `ruleType`: `` `attr_literal_compare:${a}:${op}:${String(v)}` ``。
|
|
131
|
+
- `attrs.get(a)` が数値であり `a op v` を満たすときに通過します。`v` が NaN の場合はコンストラクタ時点で拒否されます。属性側が NaN の場合は常に `false` を返します。
|
|
132
|
+
|
|
133
|
+
### AttrPairEqual
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
new AttrPairEqual({ a: string, b: string, group?: string })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `code`: `"attr_mismatch"`。
|
|
140
|
+
- 既定の `ruleType`: `` `attr_pair_equal:${a}:${b}` ``。
|
|
141
|
+
- `attrs.get(a)` と `attrs.get(b)` がいずれも非空文字列かつ厳密等価のときに通過します。非推奨となった `AttrMatchRule` の後継です。
|
|
142
|
+
|
|
143
|
+
### AttrPairNotEqual
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
new AttrPairNotEqual({ a: string, b: string, group?: string })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `code`: `"attr_match"`。
|
|
150
|
+
- 既定の `ruleType`: `` `attr_pair_not_equal:${a}:${b}` ``。
|
|
151
|
+
- `attrs.get(a)` と `attrs.get(b)` がいずれも非空文字列かつ厳密等価でないときに通過します。欠落・空文字列・非文字列はすべて `false`(safe-deny)を返します。
|
|
152
|
+
|
|
153
|
+
### AttrPairCompare
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group?: string })
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `code`: `"attr_compare_violated"`。
|
|
160
|
+
- 既定の `ruleType`: `` `attr_pair_compare:${a}:${op}:${b}` ``。
|
|
161
|
+
- `attrs.get(a)` と `attrs.get(b)` がいずれも数値で `a op b` を満たすときに通過します。どちらかが NaN の場合は `false` を返します(JS の比較セマンティクス)。
|
|
162
|
+
|
|
163
|
+
### グルーピング: 既定は AND、OR にするには `group` を指定
|
|
164
|
+
|
|
165
|
+
属性比較 Rule はすべて、上記 `AttrMatchRule` と同じグルーピング規則に従います。既定では各 Rule の `ruleType` は識別用パラメータから導出されるため、異なる要件は評価器によって AND 結合されます。2 つの Rule に同じ `group` 文字列を渡すと、両者は同じ `ruleType` を持つようになり、評価器は OR 結合します(どちらか一方が成立すれば要件を満たす)。
|
|
166
|
+
|
|
66
167
|
## Rule Collectors
|
|
67
168
|
|
|
68
169
|
| 名前 | 導出する permission / scope | 返り値 |
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Built-in attribute collectors, rule collectors, and resource parser for auth.policy-verifier.
|
|
4
4
|
|
|
5
|
+
**Runtime:** Node.js 22+. This package uses `node:crypto` for stable rule-type hashing; browser / edge runtime support is tracked as future work.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -54,6 +56,8 @@ new HasScope(scope: string)
|
|
|
54
56
|
|
|
55
57
|
### AttrMatchRule
|
|
56
58
|
|
|
59
|
+
**Deprecated.** Use [`AttrPairEqual`](#attrpairequal) instead. `AttrMatchRule` is kept as a thin wrapper class that extends `AttrPairEqual` and preserves the legacy `ruleType` (`attr_match:${a}:${b}`) and legacy `message` wording for backward compatibility. The type `AttrMatchRuleConfig` is a type alias of `AttrPairEqualConfig`. It will be removed in a future major version.
|
|
60
|
+
|
|
57
61
|
```ts
|
|
58
62
|
new AttrMatchRule({ a: string, b: string, group?: string })
|
|
59
63
|
```
|
|
@@ -63,6 +67,103 @@ new AttrMatchRule({ a: string, b: string, group?: string })
|
|
|
63
67
|
- Pure predicate — does not read `CollectorContext`. Consuming projects provide the two values to compare through upstream `AttributeCollector`s and wire the rule through their own `RuleCollector`.
|
|
64
68
|
- `ruleType` defaults to `"attr_match:${a}:${b}"`. The evaluator ORs rules within a `ruleType` and ANDs across different `ruleType`s, so the default ensures two independent comparisons are AND-combined (required together). Pass `group` explicitly when you want two comparisons to be OR-combined (for example, "identify by DID or by email") — both rules then share the provided `group` as their `ruleType`.
|
|
65
69
|
|
|
70
|
+
## Attribute Comparison Rules
|
|
71
|
+
|
|
72
|
+
The attribute comparison rules form a 2 × 5 matrix over two axes: **family** (Literal vs. Pair) and **operator** (Equal, NotEqual, In, NotIn, Compare).
|
|
73
|
+
|
|
74
|
+
- **Literal** rules compare a single named attribute against a static value (or set of values) supplied at construction time.
|
|
75
|
+
- **Pair** rules compare two named attributes resolved from the `Attributes` map at evaluation time.
|
|
76
|
+
- `In` / `NotIn` variants exist for the Literal family only. A pair-over-set operation does not generalize cleanly to a finite list, so `AttrPairIn` / `AttrPairNotIn` are intentionally absent.
|
|
77
|
+
|
|
78
|
+
| Family | Equal | NotEqual | In | NotIn | Compare |
|
|
79
|
+
| ------- | ------------------ | --------------------- | --------------- | ------------------ | -------------------- |
|
|
80
|
+
| Literal | `AttrLiteralEqual` | `AttrLiteralNotEqual` | `AttrLiteralIn` | `AttrLiteralNotIn` | `AttrLiteralCompare` |
|
|
81
|
+
| Pair | `AttrPairEqual` | `AttrPairNotEqual` | — | — | `AttrPairCompare` |
|
|
82
|
+
|
|
83
|
+
### AttrLiteralEqual
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
new AttrLiteralEqual({ a: string, v: string | number | boolean, group?: string })
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- `code`: `"attr_not_equal"`.
|
|
90
|
+
- Default `ruleType`: `` `attr_literal_equal:${a}:${typeof v}:${String(v)}` ``. The `typeof v` segment prevents silent collisions between distinct-type literals that stringify the same way (e.g. `true` vs `"true"`).
|
|
91
|
+
- Passes when `attrs.get(a)` is the same type and strictly equal to `v`. No type coercion.
|
|
92
|
+
|
|
93
|
+
### AttrLiteralNotEqual
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
new AttrLiteralNotEqual({ a: string, v: string | number | boolean, group?: string })
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- `code`: `"attr_equal"`.
|
|
100
|
+
- Default `ruleType`: `` `attr_literal_not_equal:${a}:${typeof v}:${String(v)}` ``. The `typeof v` segment prevents silent collisions between distinct-type literals (same rationale as `AttrLiteralEqual`).
|
|
101
|
+
- Passes when `attrs.get(a)` is the same type as `v` and strictly not equal to it. Missing or wrong-type attributes return `false` (safe-deny).
|
|
102
|
+
|
|
103
|
+
### AttrLiteralIn
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
new AttrLiteralIn({ a: string, values: (string | number | boolean)[], group?: string })
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- `code`: `"attr_not_in_set"`.
|
|
110
|
+
- Default `ruleType`: `` `attr_literal_in:${a}:${type}:${count}:${hashPrefix}` `` — where `count` is the post-deduplication element count and `hashPrefix` is the first 8 hex characters of a SHA-256 over the deduplicated, sorted, stringified values. Two instances with the same `a` and logically equivalent `values` (duplicates and order do not matter) share the same `ruleType` and are OR-combined by the evaluator.
|
|
111
|
+
- `values` must be a non-empty, homogeneous array (`string[]`, `number[]`, or `boolean[]`). Passes when `attrs.get(a)` is in the set. Duplicate elements in `values` are ignored (the rule uses `Set` semantics internally).
|
|
112
|
+
|
|
113
|
+
### AttrLiteralNotIn
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
new AttrLiteralNotIn({ a: string, values: (string | number | boolean)[], group?: string })
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `code`: `"attr_in_set"`.
|
|
120
|
+
- Default `ruleType`: `` `attr_literal_not_in:${a}:${type}:${count}:${hashPrefix}` `` — same stable, deduplication-aware hash scheme as `AttrLiteralIn`.
|
|
121
|
+
- `values` must be a non-empty, homogeneous array. Passes when `attrs.get(a)` is NOT in the set. Duplicate elements in `values` are ignored.
|
|
122
|
+
|
|
123
|
+
### AttrLiteralCompare
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
new AttrLiteralCompare({ a: string, op: "lt" | "le" | "gt" | "ge", v: number, group?: string })
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `code`: `"attr_compare_violated"`.
|
|
130
|
+
- Default `ruleType`: `` `attr_literal_compare:${a}:${op}:${String(v)}` ``.
|
|
131
|
+
- Passes when `attrs.get(a)` is a number satisfying `a op v`. NaN as `v` is rejected at construction time. NaN attributes always return `false`.
|
|
132
|
+
|
|
133
|
+
### AttrPairEqual
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
new AttrPairEqual({ a: string, b: string, group?: string })
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- `code`: `"attr_mismatch"`.
|
|
140
|
+
- Default `ruleType`: `` `attr_pair_equal:${a}:${b}` ``.
|
|
141
|
+
- Passes when both `attrs.get(a)` and `attrs.get(b)` are non-empty strings and strictly equal. This is the successor to the deprecated `AttrMatchRule`.
|
|
142
|
+
|
|
143
|
+
### AttrPairNotEqual
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
new AttrPairNotEqual({ a: string, b: string, group?: string })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `code`: `"attr_match"`.
|
|
150
|
+
- Default `ruleType`: `` `attr_pair_not_equal:${a}:${b}` ``.
|
|
151
|
+
- Passes when both `attrs.get(a)` and `attrs.get(b)` are non-empty strings and strictly not equal. Missing, empty, or non-string attributes return `false` (safe-deny).
|
|
152
|
+
|
|
153
|
+
### AttrPairCompare
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
new AttrPairCompare({ a: string, op: "lt" | "le" | "gt" | "ge", b: string, group?: string })
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `code`: `"attr_compare_violated"`.
|
|
160
|
+
- Default `ruleType`: `` `attr_pair_compare:${a}:${op}:${b}` ``.
|
|
161
|
+
- Passes when both `attrs.get(a)` and `attrs.get(b)` are numbers satisfying `a op b`. NaN on either side returns `false` (JS comparison semantics).
|
|
162
|
+
|
|
163
|
+
### Grouping: AND by default, `group` for OR
|
|
164
|
+
|
|
165
|
+
All attribute comparison rules follow the same grouping semantics described for `AttrMatchRule` above. By default, each rule's `ruleType` is derived from its distinguishing parameters so that distinct requirements are AND-combined by the evaluator. Pass the same `group` string to two rules to give them the same `ruleType` — the evaluator then OR-combines them (either condition satisfies the requirement).
|
|
166
|
+
|
|
66
167
|
## Rule Collectors
|
|
67
168
|
|
|
68
169
|
| Name | Derives permission/scope | Returns |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralCompare.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralCompare.test.mts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { AttrLiteralCompare } from "#/rules/AttrLiteralCompare.mjs";
|
|
3
|
+
describe("AttrLiteralCompare", () => {
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Happy path: each op at boundary values
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
it("op=lt: returns true when attr < v (5 < 10)", () => {
|
|
8
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
|
|
9
|
+
const attrs = new Map([["score", 5]]);
|
|
10
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("op=lt: returns false when attr === v (10 < 10 is false)", () => {
|
|
13
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
|
|
14
|
+
const attrs = new Map([["score", 10]]);
|
|
15
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it("op=lt: returns false when attr > v (15 < 10 is false)", () => {
|
|
18
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
|
|
19
|
+
const attrs = new Map([["score", 15]]);
|
|
20
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
21
|
+
});
|
|
22
|
+
it("op=le: returns true when attr === v (10 <= 10)", () => {
|
|
23
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
|
|
24
|
+
const attrs = new Map([["score", 10]]);
|
|
25
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it("op=le: returns true when attr < v (5 <= 10)", () => {
|
|
28
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
|
|
29
|
+
const attrs = new Map([["score", 5]]);
|
|
30
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it("op=le: returns false when attr > v (15 <= 10 is false)", () => {
|
|
33
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
|
|
34
|
+
const attrs = new Map([["score", 15]]);
|
|
35
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
it("op=gt: returns true when attr > v (15 > 10)", () => {
|
|
38
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
39
|
+
const attrs = new Map([["score", 15]]);
|
|
40
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it("op=gt: returns false when attr === v (10 > 10 is false)", () => {
|
|
43
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
44
|
+
const attrs = new Map([["score", 10]]);
|
|
45
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("op=gt: returns false when attr < v (5 > 10 is false)", () => {
|
|
48
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
49
|
+
const attrs = new Map([["score", 5]]);
|
|
50
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("op=ge: returns true when attr === v (10 >= 10)", () => {
|
|
53
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
|
|
54
|
+
const attrs = new Map([["score", 10]]);
|
|
55
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it("op=ge: returns true when attr > v (15 >= 10)", () => {
|
|
58
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
|
|
59
|
+
const attrs = new Map([["score", 15]]);
|
|
60
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it("op=ge: returns false when attr < v (5 >= 10 is false)", () => {
|
|
63
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
|
|
64
|
+
const attrs = new Map([["score", 5]]);
|
|
65
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Evaluation-time safe-deny: attr is not a number
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
it("returns false when attr is a string (not a number)", () => {
|
|
71
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 5 });
|
|
72
|
+
const attrs = new Map([["score", "10"]]);
|
|
73
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it("returns false when attr is a boolean (not a number)", () => {
|
|
76
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
|
|
77
|
+
const attrs = new Map([["score", true]]);
|
|
78
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Evaluation-time: NaN attribute → false for all ops
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
it("op=lt: returns false when attr is NaN", () => {
|
|
84
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 10 });
|
|
85
|
+
const attrs = new Map([["score", NaN]]);
|
|
86
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it("op=le: returns false when attr is NaN", () => {
|
|
89
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "le", v: 10 });
|
|
90
|
+
const attrs = new Map([["score", NaN]]);
|
|
91
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("op=gt: returns false when attr is NaN", () => {
|
|
94
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
95
|
+
const attrs = new Map([["score", NaN]]);
|
|
96
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
it("op=ge: returns false when attr is NaN", () => {
|
|
99
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
|
|
100
|
+
const attrs = new Map([["score", NaN]]);
|
|
101
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Evaluation-time: Infinity / -Infinity attributes
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
it("op=gt: Infinity > 100 returns true", () => {
|
|
107
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 100 });
|
|
108
|
+
const attrs = new Map([["score", Infinity]]);
|
|
109
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
it("op=lt: -Infinity < 0 returns true", () => {
|
|
112
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: 0 });
|
|
113
|
+
const attrs = new Map([["score", -Infinity]]);
|
|
114
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it("op=ge: Infinity >= Infinity returns true", () => {
|
|
117
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "ge", v: Infinity });
|
|
118
|
+
const attrs = new Map([["score", Infinity]]);
|
|
119
|
+
expect(rule.verify(attrs)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Evaluation-time safe-deny: missing / null attr
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
it("returns false when the target attribute is missing", () => {
|
|
125
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
|
|
126
|
+
const attrs = new Map();
|
|
127
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it("returns false when the target attribute is null", () => {
|
|
130
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 0 });
|
|
131
|
+
const attrs = new Map([["score", null]]);
|
|
132
|
+
expect(rule.verify(attrs)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Construction errors: invalid 'a'
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
it("throws when 'a' is undefined", () => {
|
|
138
|
+
expect(() => new AttrLiteralCompare({ a: undefined, op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*(non-empty string|got undefined)/);
|
|
139
|
+
});
|
|
140
|
+
it("throws when 'a' is an empty string", () => {
|
|
141
|
+
expect(() => new AttrLiteralCompare({ a: "", op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*empty string/);
|
|
142
|
+
});
|
|
143
|
+
it("throws when 'a' is a non-string (number 42)", () => {
|
|
144
|
+
expect(() => new AttrLiteralCompare({ a: 42, op: "gt", v: 0 })).toThrow(/AttrLiteralCompare.*'a'.*got number/);
|
|
145
|
+
});
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Construction errors: invalid 'v'
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
it("throws when 'v' is missing (undefined)", () => {
|
|
150
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: undefined })).toThrow(/AttrLiteralCompare.*'v'/);
|
|
151
|
+
});
|
|
152
|
+
it("throws when 'v' is null", () => {
|
|
153
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: null })).toThrow(/AttrLiteralCompare.*'v'/);
|
|
154
|
+
});
|
|
155
|
+
it("throws when 'v' is a string", () => {
|
|
156
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: "10" })).toThrow(/AttrLiteralCompare.*'v'/);
|
|
157
|
+
});
|
|
158
|
+
it("throws when 'v' is a boolean", () => {
|
|
159
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: true })).toThrow(/AttrLiteralCompare.*'v'/);
|
|
160
|
+
});
|
|
161
|
+
it("throws when 'v' is a plain object", () => {
|
|
162
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: {} })).toThrow(/AttrLiteralCompare.*'v'/);
|
|
163
|
+
});
|
|
164
|
+
it("throws when 'v' is NaN (fail-fast at construction)", () => {
|
|
165
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: NaN })).toThrow(/AttrLiteralCompare.*'v'.*NaN/);
|
|
166
|
+
});
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Construction errors: invalid 'op'
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
it("throws when 'op' is missing (undefined)", () => {
|
|
171
|
+
expect(() => new AttrLiteralCompare({
|
|
172
|
+
a: "score",
|
|
173
|
+
op: undefined,
|
|
174
|
+
v: 0,
|
|
175
|
+
})).toThrow(/AttrLiteralCompare.*'op'/);
|
|
176
|
+
});
|
|
177
|
+
it("throws when 'op' is not a string (number)", () => {
|
|
178
|
+
expect(() => new AttrLiteralCompare({
|
|
179
|
+
a: "score",
|
|
180
|
+
op: 42,
|
|
181
|
+
v: 0,
|
|
182
|
+
})).toThrow(/AttrLiteralCompare.*'op'/);
|
|
183
|
+
});
|
|
184
|
+
it("throws when 'op' is an invalid string ('eq')", () => {
|
|
185
|
+
expect(() => new AttrLiteralCompare({
|
|
186
|
+
a: "score",
|
|
187
|
+
op: "eq",
|
|
188
|
+
v: 0,
|
|
189
|
+
})).toThrow(/AttrLiteralCompare.*'op'/);
|
|
190
|
+
});
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Construction errors: invalid 'group'
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
it("throws when 'group' is present but is an empty string", () => {
|
|
195
|
+
expect(() => new AttrLiteralCompare({ a: "score", op: "gt", v: 0, group: "" })).toThrow(/AttrLiteralCompare.*'group'.*empty string/);
|
|
196
|
+
});
|
|
197
|
+
it("throws when 'group' is present but is a non-string (number 42)", () => {
|
|
198
|
+
expect(() => new AttrLiteralCompare({
|
|
199
|
+
a: "score",
|
|
200
|
+
op: "gt",
|
|
201
|
+
v: 0,
|
|
202
|
+
group: 42,
|
|
203
|
+
})).toThrow(/AttrLiteralCompare.*'group'.*got number/);
|
|
204
|
+
});
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// ruleType: default derived from a, op, v
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
it("derives default ruleType from a, op, and v", () => {
|
|
209
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
210
|
+
expect(rule.ruleType).toBe("attr_literal_compare:score:gt:10");
|
|
211
|
+
});
|
|
212
|
+
it("derives default ruleType for negative v", () => {
|
|
213
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: -5 });
|
|
214
|
+
expect(rule.ruleType).toBe("attr_literal_compare:score:lt:-5");
|
|
215
|
+
});
|
|
216
|
+
it("derives default ruleType for Infinity v", () => {
|
|
217
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "lt", v: Infinity });
|
|
218
|
+
expect(rule.ruleType).toBe("attr_literal_compare:score:lt:Infinity");
|
|
219
|
+
});
|
|
220
|
+
it("distinct op or v produce distinct ruleTypes (AND semantics)", () => {
|
|
221
|
+
const r1 = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
222
|
+
const r2 = new AttrLiteralCompare({ a: "score", op: "ge", v: 10 });
|
|
223
|
+
const r3 = new AttrLiteralCompare({ a: "score", op: "gt", v: 20 });
|
|
224
|
+
expect(r1.ruleType).not.toBe(r2.ruleType);
|
|
225
|
+
expect(r1.ruleType).not.toBe(r3.ruleType);
|
|
226
|
+
});
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// ruleType: group override
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
it("uses the provided group string as ruleType, overriding the default", () => {
|
|
231
|
+
const r1 = new AttrLiteralCompare({ a: "score", op: "gt", v: 10, group: "score-check" });
|
|
232
|
+
const r2 = new AttrLiteralCompare({ a: "score", op: "ge", v: 11, group: "score-check" });
|
|
233
|
+
expect(r1.ruleType).toBe("score-check");
|
|
234
|
+
expect(r2.ruleType).toBe("score-check");
|
|
235
|
+
});
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// code and message fields
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
it("has code 'attr_compare_violated'", () => {
|
|
240
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
241
|
+
expect(rule.code).toBe("attr_compare_violated");
|
|
242
|
+
});
|
|
243
|
+
it("message contains the attribute name, op, and v", () => {
|
|
244
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
245
|
+
expect(rule.message).toContain("score");
|
|
246
|
+
expect(rule.message).toContain("gt");
|
|
247
|
+
expect(rule.message).toContain("10");
|
|
248
|
+
});
|
|
249
|
+
it("message starts with a capital letter", () => {
|
|
250
|
+
const rule = new AttrLiteralCompare({ a: "score", op: "gt", v: 10 });
|
|
251
|
+
expect(rule.message[0]).toBe(rule.message[0]?.toUpperCase());
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AttrLiteralEqual.test.d.mts","sourceRoot":"","sources":["../../../src/__tests__/rules/AttrLiteralEqual.test.mts"],"names":[],"mappings":""}
|
|
@@ -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
|
+
});
|