@featurevisor/core 2.20.0 → 2.22.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/CHANGELOG.md +19 -0
- package/coverage/clover.xml +1286 -515
- package/coverage/coverage-final.json +17 -16
- package/coverage/lcov-report/builder/allocator.ts.html +1 -1
- package/coverage/lcov-report/builder/buildDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedConditions.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedDatafile.ts.html +1 -1
- package/coverage/lcov-report/builder/buildScopedSegments.ts.html +1 -1
- package/coverage/lcov-report/builder/convertToV1.ts.html +1 -1
- package/coverage/lcov-report/builder/getFeatureRanges.ts.html +1 -1
- package/coverage/lcov-report/builder/hashes.ts.html +1 -1
- package/coverage/lcov-report/builder/index.html +1 -1
- package/coverage/lcov-report/builder/mutateVariables.ts.html +1 -1
- package/coverage/lcov-report/builder/mutator.ts.html +1 -1
- package/coverage/lcov-report/builder/revision.ts.html +1 -1
- package/coverage/lcov-report/builder/traffic.ts.html +1 -1
- package/coverage/lcov-report/config/index.html +1 -1
- package/coverage/lcov-report/config/index.ts.html +1 -1
- package/coverage/lcov-report/config/projectConfig.ts.html +42 -42
- package/coverage/lcov-report/datasource/adapter.ts.html +2 -2
- package/coverage/lcov-report/datasource/datasource.ts.html +43 -43
- package/coverage/lcov-report/datasource/filesystemAdapter.ts.html +48 -48
- package/coverage/lcov-report/datasource/index.html +19 -19
- package/coverage/lcov-report/datasource/index.ts.html +4 -4
- package/coverage/lcov-report/generate-code/index.html +116 -0
- package/coverage/lcov-report/generate-code/typescript.ts.html +3004 -0
- package/coverage/lcov-report/index.html +44 -29
- package/coverage/lcov-report/linter/attributeSchema.ts.html +1815 -30
- package/coverage/lcov-report/linter/checkCircularDependency.ts.html +1 -1
- package/coverage/lcov-report/linter/checkPercentageExceedingSlot.ts.html +1 -1
- package/coverage/lcov-report/linter/conditionSchema.ts.html +1606 -157
- package/coverage/lcov-report/linter/featureSchema.ts.html +56 -56
- package/coverage/lcov-report/linter/groupSchema.ts.html +1 -1
- package/coverage/lcov-report/linter/index.html +39 -39
- package/coverage/lcov-report/linter/lintProject.ts.html +189 -78
- package/coverage/lcov-report/linter/mutationNotation.ts.html +1 -1
- package/coverage/lcov-report/linter/printError.ts.html +1 -1
- package/coverage/lcov-report/linter/schema.ts.html +59 -59
- package/coverage/lcov-report/linter/segmentSchema.ts.html +3 -3
- package/coverage/lcov-report/linter/testSchema.ts.html +6 -6
- package/coverage/lcov-report/list/index.html +1 -1
- package/coverage/lcov-report/list/matrix.ts.html +1 -1
- package/coverage/lcov-report/parsers/index.html +1 -1
- package/coverage/lcov-report/parsers/index.ts.html +4 -4
- package/coverage/lcov-report/parsers/json.ts.html +2 -2
- package/coverage/lcov-report/parsers/yml.ts.html +6 -6
- package/coverage/lcov-report/tester/cliFormat.ts.html +1 -1
- package/coverage/lcov-report/tester/helpers.ts.html +1 -1
- package/coverage/lcov-report/tester/index.html +1 -1
- package/coverage/lcov-report/utils/extractKeys.ts.html +1 -1
- package/coverage/lcov-report/utils/git.ts.html +3 -3
- package/coverage/lcov-report/utils/index.html +1 -1
- package/coverage/lcov-report/utils/index.ts.html +1 -1
- package/coverage/lcov-report/utils/pretty.ts.html +1 -1
- package/coverage/lcov.info +2624 -926
- package/lib/generate-code/typescript.js +57 -29
- package/lib/generate-code/typescript.js.map +1 -1
- package/lib/generate-code/typescript.spec.d.ts +1 -0
- package/lib/generate-code/typescript.spec.js +78 -0
- package/lib/generate-code/typescript.spec.js.map +1 -0
- package/lib/linter/attributeSchema.d.ts +352 -20
- package/lib/linter/attributeSchema.js +426 -16
- package/lib/linter/attributeSchema.js.map +1 -1
- package/lib/linter/attributeSchema.spec.d.ts +1 -0
- package/lib/linter/attributeSchema.spec.js +266 -0
- package/lib/linter/attributeSchema.spec.js.map +1 -0
- package/lib/linter/conditionSchema.d.ts +2 -1
- package/lib/linter/conditionSchema.js +370 -100
- package/lib/linter/conditionSchema.js.map +1 -1
- package/lib/linter/conditionSchema.spec.js +200 -153
- package/lib/linter/conditionSchema.spec.js.map +1 -1
- package/lib/linter/featureSchema.spec.js +114 -3
- package/lib/linter/featureSchema.spec.js.map +1 -1
- package/lib/linter/lintProject.js +45 -14
- package/lib/linter/lintProject.js.map +1 -1
- package/lib/linter/segmentSchema.spec.js +71 -1
- package/lib/linter/segmentSchema.spec.js.map +1 -1
- package/package.json +5 -5
- package/src/generate-code/typescript.spec.ts +108 -0
- package/src/generate-code/typescript.ts +80 -34
- package/src/linter/attributeSchema.spec.ts +342 -0
- package/src/linter/attributeSchema.ts +613 -18
- package/src/linter/conditionSchema.spec.ts +231 -189
- package/src/linter/conditionSchema.ts +582 -99
- package/src/linter/featureSchema.spec.ts +129 -4
- package/src/linter/lintProject.ts +55 -18
- package/src/linter/segmentSchema.spec.ts +82 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for condition schema validation (segment conditions, feature rules, etc.).
|
|
3
|
-
* Covers getConditionsZodSchema: attribute
|
|
4
|
-
* regexFlags,
|
|
3
|
+
* Covers getConditionsZodSchema: attribute path resolution, operator/value compatibility,
|
|
4
|
+
* schema-aware value validation, regexFlags, nesting, and "*" (everyone).
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import type { Attribute } from "@featurevisor/types";
|
|
8
9
|
import type { ProjectConfig } from "../config";
|
|
9
10
|
import { getConditionsZodSchema } from "./conditionSchema";
|
|
10
11
|
|
|
@@ -35,7 +36,80 @@ function minimalProjectConfig(): ProjectConfig {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
const TEST_ATTRIBUTES:
|
|
39
|
+
const TEST_ATTRIBUTES: Record<string, Attribute> = {
|
|
40
|
+
userId: {
|
|
41
|
+
description: "User ID",
|
|
42
|
+
type: "string",
|
|
43
|
+
},
|
|
44
|
+
country: {
|
|
45
|
+
description: "Country",
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: ["de", "fr", "nl", "us"],
|
|
48
|
+
},
|
|
49
|
+
device: {
|
|
50
|
+
description: "Device",
|
|
51
|
+
type: "string",
|
|
52
|
+
},
|
|
53
|
+
email: {
|
|
54
|
+
description: "Email",
|
|
55
|
+
type: "string",
|
|
56
|
+
pattern: "^[^@]+@[^@]+\\.[^@]+$",
|
|
57
|
+
},
|
|
58
|
+
age: {
|
|
59
|
+
description: "Age",
|
|
60
|
+
type: "integer",
|
|
61
|
+
minimum: 18,
|
|
62
|
+
},
|
|
63
|
+
browser: {
|
|
64
|
+
description: "Browser",
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
name: {
|
|
68
|
+
type: "string",
|
|
69
|
+
enum: ["chrome", "firefox", "safari"],
|
|
70
|
+
},
|
|
71
|
+
version: {
|
|
72
|
+
type: "semver",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
account: {
|
|
77
|
+
description: "Account",
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
plan: {
|
|
81
|
+
type: "string",
|
|
82
|
+
enum: ["free", "pro"],
|
|
83
|
+
},
|
|
84
|
+
locale: {
|
|
85
|
+
type: "string",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
labels: {
|
|
90
|
+
description: "Labels",
|
|
91
|
+
type: "object",
|
|
92
|
+
additionalProperties: {
|
|
93
|
+
type: "string",
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
permissions: {
|
|
97
|
+
description: "Permissions",
|
|
98
|
+
type: "array",
|
|
99
|
+
items: {
|
|
100
|
+
type: "string",
|
|
101
|
+
enum: ["read", "write", "admin"],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
version: {
|
|
105
|
+
description: "Version",
|
|
106
|
+
oneOf: [{ type: "string" }, { type: "double" }],
|
|
107
|
+
},
|
|
108
|
+
traits: {
|
|
109
|
+
description: "Traits",
|
|
110
|
+
type: "object",
|
|
111
|
+
},
|
|
112
|
+
};
|
|
39
113
|
|
|
40
114
|
function getConditionsSchema() {
|
|
41
115
|
return getConditionsZodSchema(minimalProjectConfig(), TEST_ATTRIBUTES);
|
|
@@ -69,7 +143,6 @@ function expectConditionsFailure(input: unknown, messageSubstring?: string): z.Z
|
|
|
69
143
|
return err;
|
|
70
144
|
}
|
|
71
145
|
|
|
72
|
-
/** Assert that an intentional mistake produces an error at the expected path with expected message. */
|
|
73
146
|
function expectConditionErrorSurfaces(
|
|
74
147
|
input: unknown,
|
|
75
148
|
opts: { pathContains: string[]; messageContains: string },
|
|
@@ -83,16 +156,36 @@ function expectConditionErrorSurfaces(
|
|
|
83
156
|
}
|
|
84
157
|
|
|
85
158
|
describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
86
|
-
describe("attribute", () => {
|
|
87
|
-
it("accepts
|
|
159
|
+
describe("attribute path resolution", () => {
|
|
160
|
+
it("accepts direct attributes", () => {
|
|
161
|
+
expectConditionsSuccess({ attribute: "userId", operator: "equals", value: "u1" });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("accepts nested attributes from declared object properties", () => {
|
|
88
165
|
expectConditionsSuccess({
|
|
89
|
-
attribute: "
|
|
166
|
+
attribute: "browser.name",
|
|
167
|
+
operator: "equals",
|
|
168
|
+
value: "chrome",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("accepts dynamic nested attributes via additionalProperties", () => {
|
|
173
|
+
expectConditionsSuccess({
|
|
174
|
+
attribute: "labels.locale",
|
|
90
175
|
operator: "equals",
|
|
91
|
-
value: "
|
|
176
|
+
value: "nl-NL",
|
|
92
177
|
});
|
|
93
178
|
});
|
|
94
179
|
|
|
95
|
-
it("
|
|
180
|
+
it("accepts one-level nested attributes on flat objects", () => {
|
|
181
|
+
expectConditionsSuccess({
|
|
182
|
+
attribute: "traits.favoriteColor",
|
|
183
|
+
operator: "equals",
|
|
184
|
+
value: "blue",
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("rejects unknown attributes", () => {
|
|
96
189
|
expectConditionsFailure(
|
|
97
190
|
{
|
|
98
191
|
attribute: "unknownAttr",
|
|
@@ -102,60 +195,70 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
102
195
|
"Unknown attribute",
|
|
103
196
|
);
|
|
104
197
|
});
|
|
198
|
+
|
|
199
|
+
it("rejects paths that are too deep for flat objects", () => {
|
|
200
|
+
expectConditionsFailure(
|
|
201
|
+
{
|
|
202
|
+
attribute: "traits.preferences.theme",
|
|
203
|
+
operator: "equals",
|
|
204
|
+
value: "dark",
|
|
205
|
+
},
|
|
206
|
+
"Unknown attribute",
|
|
207
|
+
);
|
|
208
|
+
});
|
|
105
209
|
});
|
|
106
210
|
|
|
107
|
-
describe("operator and value
|
|
108
|
-
it("accepts
|
|
109
|
-
expectConditionsSuccess({
|
|
211
|
+
describe("operator and value validation", () => {
|
|
212
|
+
it("accepts schema-aware equality checks", () => {
|
|
213
|
+
expectConditionsSuccess({
|
|
214
|
+
attribute: "browser.name",
|
|
215
|
+
operator: "equals",
|
|
216
|
+
value: "firefox",
|
|
217
|
+
});
|
|
110
218
|
});
|
|
111
219
|
|
|
112
|
-
it("
|
|
113
|
-
|
|
220
|
+
it("rejects equality values outside enum constraints", () => {
|
|
221
|
+
expectConditionsFailure(
|
|
222
|
+
{
|
|
223
|
+
attribute: "browser.name",
|
|
224
|
+
operator: "equals",
|
|
225
|
+
value: "edge",
|
|
226
|
+
},
|
|
227
|
+
'Value does not match the schema of attribute "browser.name"',
|
|
228
|
+
);
|
|
114
229
|
});
|
|
115
230
|
|
|
116
|
-
it("accepts
|
|
117
|
-
expectConditionsSuccess({ attribute: "
|
|
231
|
+
it("accepts number equality on numeric attributes", () => {
|
|
232
|
+
expectConditionsSuccess({ attribute: "age", operator: "equals", value: 42 });
|
|
118
233
|
});
|
|
119
234
|
|
|
120
|
-
it("accepts null
|
|
235
|
+
it("accepts null equality checks", () => {
|
|
121
236
|
expectConditionsSuccess({ attribute: "userId", operator: "equals", value: null });
|
|
122
237
|
});
|
|
123
238
|
|
|
124
|
-
it("rejects array
|
|
239
|
+
it("rejects array values with equals", () => {
|
|
125
240
|
expectConditionsFailure(
|
|
126
241
|
{ attribute: "userId", operator: "equals", value: ["a", "b"] },
|
|
127
242
|
"value has to be",
|
|
128
243
|
);
|
|
129
244
|
});
|
|
130
|
-
});
|
|
131
245
|
|
|
132
|
-
|
|
133
|
-
it("accepts number value with greaterThan", () => {
|
|
246
|
+
it("accepts numeric operators on numeric attributes", () => {
|
|
134
247
|
expectConditionsSuccess({
|
|
135
|
-
attribute: "
|
|
248
|
+
attribute: "age",
|
|
136
249
|
operator: "greaterThan",
|
|
137
|
-
value:
|
|
250
|
+
value: 21,
|
|
138
251
|
});
|
|
139
252
|
});
|
|
140
253
|
|
|
141
|
-
it("rejects
|
|
254
|
+
it("rejects numeric operators on string attributes", () => {
|
|
142
255
|
expectConditionsFailure(
|
|
143
|
-
{ attribute: "
|
|
144
|
-
"
|
|
256
|
+
{ attribute: "country", operator: "greaterThan", value: 10 },
|
|
257
|
+
"can only be used with integer or double attributes",
|
|
145
258
|
);
|
|
146
259
|
});
|
|
147
260
|
|
|
148
|
-
it("
|
|
149
|
-
const result = parseConditions({
|
|
150
|
-
attribute: "userId",
|
|
151
|
-
operator: "lessThan",
|
|
152
|
-
});
|
|
153
|
-
expect(result.success).toBe(false);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe("operator and value: string", () => {
|
|
158
|
-
it("accepts string value with contains", () => {
|
|
261
|
+
it("accepts string operators on string-like attributes", () => {
|
|
159
262
|
expectConditionsSuccess({
|
|
160
263
|
attribute: "email",
|
|
161
264
|
operator: "contains",
|
|
@@ -163,40 +266,55 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
163
266
|
});
|
|
164
267
|
});
|
|
165
268
|
|
|
166
|
-
it("rejects
|
|
269
|
+
it("rejects string operators on array attributes", () => {
|
|
167
270
|
expectConditionsFailure(
|
|
168
|
-
{ attribute: "
|
|
169
|
-
"
|
|
271
|
+
{ attribute: "permissions", operator: "contains", value: "admin" },
|
|
272
|
+
"can only be used with string-like attributes",
|
|
170
273
|
);
|
|
171
274
|
});
|
|
172
|
-
});
|
|
173
275
|
|
|
174
|
-
|
|
175
|
-
it("accepts ISO 8601 string with before", () => {
|
|
276
|
+
it("accepts date operators with ISO strings", () => {
|
|
176
277
|
expectConditionsSuccess({
|
|
177
|
-
attribute: "
|
|
278
|
+
attribute: "device",
|
|
178
279
|
operator: "before",
|
|
179
280
|
value: "2025-12-31T23:59:59Z",
|
|
180
281
|
});
|
|
181
282
|
});
|
|
182
283
|
|
|
183
|
-
it("rejects
|
|
284
|
+
it("rejects date operators with invalid strings", () => {
|
|
184
285
|
expectConditionsFailure(
|
|
185
|
-
{ attribute: "
|
|
286
|
+
{ attribute: "device", operator: "after", value: "not-a-date" },
|
|
186
287
|
"ISO 8601",
|
|
187
288
|
);
|
|
188
289
|
});
|
|
189
290
|
|
|
190
|
-
it("
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
291
|
+
it("accepts semver operators on semver attributes", () => {
|
|
292
|
+
expectConditionsSuccess({
|
|
293
|
+
attribute: "browser.version",
|
|
294
|
+
operator: "semverEquals",
|
|
295
|
+
value: "1.0.0",
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("accepts semver operators on oneOf attributes when a string branch supports them", () => {
|
|
300
|
+
expectConditionsSuccess({
|
|
301
|
+
attribute: "version",
|
|
302
|
+
operator: "semverGreaterThan",
|
|
303
|
+
value: "5.0.0",
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("accepts numeric equality on oneOf attributes when a numeric branch matches", () => {
|
|
308
|
+
expectConditionsSuccess({
|
|
309
|
+
attribute: "version",
|
|
310
|
+
operator: "equals",
|
|
311
|
+
value: 5.5,
|
|
312
|
+
});
|
|
195
313
|
});
|
|
196
314
|
});
|
|
197
315
|
|
|
198
|
-
describe("
|
|
199
|
-
it("accepts
|
|
316
|
+
describe("array-aware operators", () => {
|
|
317
|
+
it("accepts in/notIn on scalar attributes", () => {
|
|
200
318
|
expectConditionsSuccess({
|
|
201
319
|
attribute: "country",
|
|
202
320
|
operator: "in",
|
|
@@ -204,28 +322,57 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
204
322
|
});
|
|
205
323
|
});
|
|
206
324
|
|
|
207
|
-
it("rejects non-array
|
|
325
|
+
it("rejects non-array values for in/notIn", () => {
|
|
208
326
|
expectConditionsFailure(
|
|
209
327
|
{ attribute: "country", operator: "notIn", value: "de" },
|
|
210
328
|
"value must be an array",
|
|
211
329
|
);
|
|
212
330
|
});
|
|
213
|
-
});
|
|
214
331
|
|
|
215
|
-
|
|
216
|
-
|
|
332
|
+
it("rejects in/notIn members that violate attribute schema", () => {
|
|
333
|
+
expectConditionsFailure(
|
|
334
|
+
{ attribute: "country", operator: "in", value: ["de", "unknown"] },
|
|
335
|
+
'Value at index 1 does not match the schema of attribute "country"',
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("accepts includes for arrays with typed string items", () => {
|
|
217
340
|
expectConditionsSuccess({
|
|
218
|
-
attribute: "
|
|
219
|
-
operator: "
|
|
220
|
-
value: "
|
|
341
|
+
attribute: "permissions",
|
|
342
|
+
operator: "includes",
|
|
343
|
+
value: "admin",
|
|
221
344
|
});
|
|
222
345
|
});
|
|
223
346
|
|
|
347
|
+
it("rejects includes values outside item enum constraints", () => {
|
|
348
|
+
expectConditionsFailure(
|
|
349
|
+
{
|
|
350
|
+
attribute: "permissions",
|
|
351
|
+
operator: "includes",
|
|
352
|
+
value: "delete",
|
|
353
|
+
},
|
|
354
|
+
'Value does not match the item schema of attribute "permissions"',
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("rejects non-string includes values for string arrays", () => {
|
|
359
|
+
expectConditionsFailure(
|
|
360
|
+
{
|
|
361
|
+
attribute: "permissions",
|
|
362
|
+
operator: "includes",
|
|
363
|
+
value: 42,
|
|
364
|
+
},
|
|
365
|
+
'Value does not match the item schema of attribute "permissions"',
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("regex flags", () => {
|
|
224
371
|
it("accepts regexFlags with matches", () => {
|
|
225
372
|
expectConditionsSuccess({
|
|
226
|
-
attribute: "
|
|
373
|
+
attribute: "browser.version",
|
|
227
374
|
operator: "matches",
|
|
228
|
-
value: "
|
|
375
|
+
value: "1\\.0\\.0",
|
|
229
376
|
regexFlags: "i",
|
|
230
377
|
});
|
|
231
378
|
});
|
|
@@ -233,7 +380,7 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
233
380
|
it("rejects invalid regexFlags", () => {
|
|
234
381
|
expectConditionsFailure(
|
|
235
382
|
{
|
|
236
|
-
attribute: "
|
|
383
|
+
attribute: "browser.version",
|
|
237
384
|
operator: "matches",
|
|
238
385
|
value: "x",
|
|
239
386
|
regexFlags: "invalid",
|
|
@@ -255,15 +402,15 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
255
402
|
});
|
|
256
403
|
});
|
|
257
404
|
|
|
258
|
-
describe("
|
|
259
|
-
it("accepts
|
|
405
|
+
describe("exists/notExists", () => {
|
|
406
|
+
it("accepts exists without value", () => {
|
|
260
407
|
expectConditionsSuccess({
|
|
261
408
|
attribute: "userId",
|
|
262
409
|
operator: "exists",
|
|
263
410
|
});
|
|
264
411
|
});
|
|
265
412
|
|
|
266
|
-
it("rejects
|
|
413
|
+
it("rejects exists with value", () => {
|
|
267
414
|
expectConditionsFailure(
|
|
268
415
|
{ attribute: "userId", operator: "exists", value: "x" },
|
|
269
416
|
"value is not needed",
|
|
@@ -271,94 +418,45 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
271
418
|
});
|
|
272
419
|
});
|
|
273
420
|
|
|
274
|
-
describe("structure
|
|
275
|
-
it("accepts and
|
|
276
|
-
expectConditionsSuccess({
|
|
277
|
-
and: [
|
|
278
|
-
{ attribute: "userId", operator: "equals", value: "u1" },
|
|
279
|
-
{ attribute: "country", operator: "equals", value: "de" },
|
|
280
|
-
],
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("accepts or array of conditions", () => {
|
|
285
|
-
expectConditionsSuccess({
|
|
286
|
-
or: [
|
|
287
|
-
{ attribute: "country", operator: "equals", value: "de" },
|
|
288
|
-
{ attribute: "country", operator: "equals", value: "fr" },
|
|
289
|
-
],
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("accepts not array of conditions", () => {
|
|
294
|
-
expectConditionsSuccess({
|
|
295
|
-
not: [{ attribute: "country", operator: "equals", value: "us" }],
|
|
296
|
-
});
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("accepts nested and inside or", () => {
|
|
421
|
+
describe("structure", () => {
|
|
422
|
+
it("accepts nested and/or/not conditions", () => {
|
|
300
423
|
expectConditionsSuccess({
|
|
301
424
|
or: [
|
|
302
425
|
{
|
|
303
426
|
and: [
|
|
304
|
-
{ attribute: "
|
|
305
|
-
{ attribute: "
|
|
427
|
+
{ attribute: "browser.name", operator: "equals", value: "chrome" },
|
|
428
|
+
{ attribute: "country", operator: "equals", value: "de" },
|
|
306
429
|
],
|
|
307
430
|
},
|
|
308
|
-
{ attribute: "
|
|
431
|
+
{ attribute: "permissions", operator: "includes", value: "admin" },
|
|
309
432
|
],
|
|
310
433
|
});
|
|
311
434
|
});
|
|
312
435
|
|
|
313
|
-
it("rejects
|
|
436
|
+
it("rejects invalid nested conditions", () => {
|
|
314
437
|
expectConditionsFailure(
|
|
315
438
|
{
|
|
316
439
|
and: [
|
|
317
440
|
{ attribute: "userId", operator: "equals", value: "u1" },
|
|
318
|
-
{ attribute: "
|
|
441
|
+
{ attribute: "browser.name", operator: "equals", value: "edge" },
|
|
319
442
|
],
|
|
320
443
|
},
|
|
321
|
-
|
|
322
|
-
);
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("rejects invalid value type inside nested condition", () => {
|
|
326
|
-
expectConditionsFailure(
|
|
327
|
-
{
|
|
328
|
-
or: [{ attribute: "country", operator: "greaterThan", value: "string" }],
|
|
329
|
-
},
|
|
330
|
-
"value must be a number",
|
|
444
|
+
'Value does not match the schema of attribute "browser.name"',
|
|
331
445
|
);
|
|
332
446
|
});
|
|
333
|
-
});
|
|
334
447
|
|
|
335
|
-
describe("everyone (*)", () => {
|
|
336
448
|
it("accepts literal * as conditions", () => {
|
|
337
449
|
expectConditionsSuccess("*");
|
|
338
450
|
});
|
|
339
|
-
});
|
|
340
451
|
|
|
341
|
-
|
|
342
|
-
it("accepts array of plain conditions", () => {
|
|
452
|
+
it("accepts arrays of plain conditions", () => {
|
|
343
453
|
expectConditionsSuccess([
|
|
344
454
|
{ attribute: "userId", operator: "equals", value: "u1" },
|
|
345
455
|
{ attribute: "country", operator: "equals", value: "de" },
|
|
346
456
|
]);
|
|
347
457
|
});
|
|
348
458
|
|
|
349
|
-
it("rejects
|
|
350
|
-
expectConditionsFailure(
|
|
351
|
-
[
|
|
352
|
-
{ attribute: "userId", operator: "equals", value: "u1" },
|
|
353
|
-
{ attribute: "unknown", operator: "equals", value: "x" },
|
|
354
|
-
],
|
|
355
|
-
"Unknown attribute",
|
|
356
|
-
);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe("strict: no extra keys on and/or/not", () => {
|
|
361
|
-
it("rejects and/or/not with extra key", () => {
|
|
459
|
+
it("rejects and/or/not objects with extra keys", () => {
|
|
362
460
|
const result = parseConditions({
|
|
363
461
|
and: [{ attribute: "userId", operator: "equals", value: "u1" }],
|
|
364
462
|
extraKey: true,
|
|
@@ -367,81 +465,25 @@ describe("conditionSchema.ts :: getConditionsZodSchema", () => {
|
|
|
367
465
|
});
|
|
368
466
|
});
|
|
369
467
|
|
|
370
|
-
describe("
|
|
371
|
-
it("
|
|
372
|
-
const result = parseConditions({
|
|
373
|
-
attribute: "userId",
|
|
374
|
-
operator: "unknownOp",
|
|
375
|
-
value: "x",
|
|
376
|
-
});
|
|
377
|
-
expect(result.success).toBe(false);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("accepts all common and numeric operators", () => {
|
|
381
|
-
expectConditionsSuccess({ attribute: "userId", operator: "notEquals", value: "x" });
|
|
382
|
-
expectConditionsSuccess({ attribute: "userId", operator: "greaterThanOrEquals", value: 0 });
|
|
383
|
-
expectConditionsSuccess({ attribute: "userId", operator: "lessThanOrEquals", value: 100 });
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it("accepts semver operators with string value", () => {
|
|
387
|
-
expectConditionsSuccess({
|
|
388
|
-
attribute: "userId",
|
|
389
|
-
operator: "semverEquals",
|
|
390
|
-
value: "1.0.0",
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
describe("errors surface properly: intentional mistakes produce correct path and message", () => {
|
|
396
|
-
it("unknown attribute: error path includes attribute, message says Unknown attribute", () => {
|
|
468
|
+
describe("error surfacing", () => {
|
|
469
|
+
it("reports unknown attributes at the attribute path", () => {
|
|
397
470
|
expectConditionErrorSurfaces(
|
|
398
471
|
{ attribute: "typoAttr", operator: "equals", value: "x" },
|
|
399
472
|
{ pathContains: ["attribute"], messageContains: "Unknown attribute" },
|
|
400
473
|
);
|
|
401
474
|
});
|
|
402
475
|
|
|
403
|
-
it("
|
|
476
|
+
it("reports schema mismatches at the value path", () => {
|
|
404
477
|
expectConditionErrorSurfaces(
|
|
405
|
-
{ attribute: "
|
|
406
|
-
{ pathContains: ["value"], messageContains: "
|
|
478
|
+
{ attribute: "browser.name", operator: "equals", value: "edge" },
|
|
479
|
+
{ pathContains: ["value"], messageContains: "Value does not match the schema" },
|
|
407
480
|
);
|
|
408
481
|
});
|
|
409
482
|
|
|
410
|
-
it("
|
|
483
|
+
it("reports array item schema mismatches at the value path", () => {
|
|
411
484
|
expectConditionErrorSurfaces(
|
|
412
|
-
{ attribute: "
|
|
413
|
-
{ pathContains: ["value"], messageContains: "
|
|
414
|
-
);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("exists operator with value set: error path points to value, message says not needed", () => {
|
|
418
|
-
expectConditionErrorSurfaces(
|
|
419
|
-
{ attribute: "userId", operator: "exists", value: "x" },
|
|
420
|
-
{ pathContains: ["value"], messageContains: "not needed" },
|
|
421
|
-
);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("regexFlags when operator is not matches: error path points to regexFlags", () => {
|
|
425
|
-
expectConditionErrorSurfaces(
|
|
426
|
-
{
|
|
427
|
-
attribute: "userId",
|
|
428
|
-
operator: "equals",
|
|
429
|
-
value: "u1",
|
|
430
|
-
regexFlags: "i",
|
|
431
|
-
},
|
|
432
|
-
{ pathContains: ["regexFlags"], messageContains: "not needed" },
|
|
433
|
-
);
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it("nested condition with unknown attribute: error path goes into and.*.attribute", () => {
|
|
437
|
-
expectConditionErrorSurfaces(
|
|
438
|
-
{
|
|
439
|
-
and: [
|
|
440
|
-
{ attribute: "userId", operator: "equals", value: "u1" },
|
|
441
|
-
{ attribute: "badAttr", operator: "equals", value: "x" },
|
|
442
|
-
],
|
|
443
|
-
},
|
|
444
|
-
{ pathContains: ["attribute"], messageContains: "Unknown attribute" },
|
|
485
|
+
{ attribute: "country", operator: "in", value: ["de", "edge"] },
|
|
486
|
+
{ pathContains: ["value"], messageContains: "Value at index 1 does not match" },
|
|
445
487
|
);
|
|
446
488
|
});
|
|
447
489
|
});
|