1
2
3
4
5
6 package gov.nist.secauto.metaschema.core.model.constraint;
7
8 import static org.hamcrest.MatcherAssert.assertThat;
9 import static org.hamcrest.Matchers.hasItem;
10 import static org.hamcrest.Matchers.hasProperty;
11 import static org.hamcrest.Matchers.hasSize;
12 import static org.hamcrest.Matchers.is;
13 import static org.junit.jupiter.api.Assertions.assertAll;
14 import static org.junit.jupiter.api.Assertions.assertFalse;
15 import static org.junit.jupiter.api.Assertions.assertTrue;
16 import static org.mockito.ArgumentMatchers.any;
17 import static org.mockito.Mockito.doReturn;
18 import static org.mockito.Mockito.mock;
19
20 import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
21 import gov.nist.secauto.metaschema.core.metapath.DynamicContext;
22 import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression;
23 import gov.nist.secauto.metaschema.core.metapath.StaticContext;
24 import gov.nist.secauto.metaschema.core.mdm.IDMFieldNodeItem;
25 import gov.nist.secauto.metaschema.core.metapath.format.IPathFormatter;
26 import gov.nist.secauto.metaschema.core.metapath.item.atomic.IStringItem;
27 import gov.nist.secauto.metaschema.core.metapath.item.node.IFieldNodeItem;
28 import gov.nist.secauto.metaschema.core.metapath.item.node.IFlagNodeItem;
29 import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
30 import gov.nist.secauto.metaschema.core.model.IFlagDefinition;
31 import gov.nist.secauto.metaschema.core.model.IFlagInstance;
32 import gov.nist.secauto.metaschema.core.model.ISource;
33 import gov.nist.secauto.metaschema.core.model.constraint.IConstraint.Level;
34 import gov.nist.secauto.metaschema.core.qname.IEnhancedQName;
35 import gov.nist.secauto.metaschema.core.testsupport.MockedModelTestSupport;
36 import gov.nist.secauto.metaschema.core.testsupport.mocking.MockNodeItemFactory;
37 import gov.nist.secauto.metaschema.core.util.CollectionUtil;
38 import gov.nist.secauto.metaschema.core.util.ObjectUtils;
39
40 import org.junit.jupiter.api.Test;
41
42 import java.net.URI;
43
44 import edu.umd.cs.findbugs.annotations.NonNull;
45
46
47
48
49 @SuppressWarnings("PMD.TooManyStaticImports")
50 class ExpectConstraintTest {
51 @NonNull
52 private static final String NS = ObjectUtils.notNull(URI.create("http://example.com/ns").toASCIIString());
53
54 @NonNull
55 private static IEnhancedQName qname(@NonNull String name) {
56 return IEnhancedQName.of(NS, name);
57 }
58
59
60
61
62
63
64
65
66 @SuppressWarnings("null")
67 private static void mockFlagDefinitionConstraints(@NonNull IFlagNodeItem flag) {
68 IFlagDefinition definition = flag.getDefinition();
69 doReturn(CollectionUtil.emptyMap()).when(definition).getLetExpressions();
70 doReturn(CollectionUtil.emptyList()).when(definition).getAllowedValuesConstraints();
71 doReturn(CollectionUtil.emptyList()).when(definition).getExpectConstraints();
72 doReturn(CollectionUtil.emptyList()).when(definition).getMatchesConstraints();
73 doReturn(CollectionUtil.emptyList()).when(definition).getIndexHasKeyConstraints();
74 }
75
76
77
78
79
80
81
82 @SuppressWarnings("null")
83 @Test
84 void testExpectConstraintPasses() throws ConstraintValidationException {
85 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
86
87 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test-value"));
88
89 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
90
91 ISource source = mock(ISource.class);
92
93
94 IExpectConstraint expectConstraint = IExpectConstraint.builder()
95 .source(source)
96 .test(IMetapathExpression.compile("string-length(.) > 0"))
97 .build();
98
99 doReturn(flagDefinition).when(flag).getDefinition();
100 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
101
102 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
103 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
104 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
105 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
106 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
107
108 StaticContext staticContext = StaticContext.instance();
109 doReturn(staticContext).when(source).getStaticContext();
110
111 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
112 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
113 DynamicContext dynamicContext = new DynamicContext(staticContext);
114 validator.validate(flag, dynamicContext);
115 validator.finalizeValidation(dynamicContext);
116
117 assertTrue(handler.isPassing(), "constraint should pass when test expression evaluates to true");
118 }
119
120
121
122
123
124
125
126 @SuppressWarnings("null")
127 @Test
128 void testExpectConstraintFails() throws ConstraintValidationException {
129 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
130
131 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test"));
132
133 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
134
135 ISource source = mock(ISource.class);
136
137
138 IExpectConstraint expectConstraint = IExpectConstraint.builder()
139 .source(source)
140 .test(IMetapathExpression.compile("string-length(.) > 10"))
141 .build();
142
143 doReturn(flagDefinition).when(flag).getDefinition();
144 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
145
146 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
147 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
148 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
149 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
150 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
151
152 StaticContext staticContext = StaticContext.instance();
153 doReturn(staticContext).when(source).getStaticContext();
154
155 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
156 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
157 DynamicContext dynamicContext = new DynamicContext(staticContext);
158 validator.validate(flag, dynamicContext);
159 validator.finalizeValidation(dynamicContext);
160
161 assertAll(
162 () -> assertFalse(handler.isPassing(), "constraint should fail when test expression evaluates to false"),
163 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
164 () -> assertThat("finding should be for the flag node", handler.getFindings(),
165 hasItem(hasProperty("node", is(flag)))));
166 }
167
168
169
170
171
172
173
174 @SuppressWarnings("null")
175 @Test
176 void testExpectConstraintWithCustomMessage() throws ConstraintValidationException {
177 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
178
179 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("short"));
180
181 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
182
183 ISource source = mock(ISource.class);
184
185 String customMessage = "Value must be at least 10 characters long";
186
187
188 IExpectConstraint expectConstraint = IExpectConstraint.builder()
189 .source(source)
190 .test(IMetapathExpression.compile("string-length(.) >= 10"))
191 .message(customMessage)
192 .build();
193
194 doReturn(flagDefinition).when(flag).getDefinition();
195 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
196
197 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
198 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
199 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
200 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
201 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
202
203 StaticContext staticContext = StaticContext.instance();
204 doReturn(staticContext).when(source).getStaticContext();
205
206 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
207 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
208 DynamicContext dynamicContext = new DynamicContext(staticContext);
209 validator.validate(flag, dynamicContext);
210 validator.finalizeValidation(dynamicContext);
211
212 assertAll(
213 () -> assertFalse(handler.isPassing(), "constraint should fail"),
214 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
215 () -> assertThat("finding should contain custom message", handler.getFindings(),
216 hasItem(hasProperty("message", is(customMessage)))));
217 }
218
219
220
221
222
223
224
225 @Test
226 void testExpectConstraintWithTargetMetapath() throws ConstraintValidationException {
227 MockedModelTestSupport mocking = new MockedModelTestSupport();
228 ISource source = ISource.externalSource("https://example.com/module");
229
230
231 IFieldDefinition fieldDef = mocking.field()
232 .qname(qname("item"))
233 .source(source)
234 .toDefinition();
235
236
237 IFlagInstance statusFlagInstance = mocking.flag()
238 .qname(IEnhancedQName.of("status"))
239 .source(source)
240 .toInstance(fieldDef);
241
242
243 StaticContext staticContext = StaticContext.builder()
244 .defaultModelNamespace(NS)
245 .build();
246
247
248 IDMFieldNodeItem field = IDMFieldNodeItem.newInstance(fieldDef, IStringItem.valueOf("value"), staticContext);
249 field.newFlag(statusFlagInstance, IStringItem.valueOf("active"));
250
251
252 IExpectConstraint expectConstraint = IExpectConstraint.builder()
253 .source(source)
254 .target(IMetapathExpression.compile("@status", staticContext))
255 .test(IMetapathExpression.compile(". = 'inactive'", staticContext))
256 .build();
257 fieldDef.addConstraint(expectConstraint);
258
259 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
260 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
261 DynamicContext dynamicContext = new DynamicContext(staticContext);
262 validator.validate(field, dynamicContext);
263 validator.finalizeValidation(dynamicContext);
264
265 assertAll(
266 () -> assertFalse(handler.isPassing(), "constraint should fail when target test fails"),
267 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)));
268 }
269
270
271
272
273
274
275
276 @SuppressWarnings("null")
277 @Test
278 void testExpectConstraintWithWarningSeverity() throws ConstraintValidationException {
279 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
280
281 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test"));
282
283 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
284
285 ISource source = mock(ISource.class);
286
287
288 IExpectConstraint expectConstraint = IExpectConstraint.builder()
289 .source(source)
290 .level(Level.WARNING)
291 .test(IMetapathExpression.compile("string-length(.) > 10"))
292 .build();
293
294 doReturn(flagDefinition).when(flag).getDefinition();
295 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
296
297 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
298 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
299 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
300 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
301 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
302
303 StaticContext staticContext = StaticContext.instance();
304 doReturn(staticContext).when(source).getStaticContext();
305
306 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
307 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
308 DynamicContext dynamicContext = new DynamicContext(staticContext);
309 validator.validate(flag, dynamicContext);
310 validator.finalizeValidation(dynamicContext);
311
312 assertAll(
313 () -> assertTrue(handler.isPassing(), "validation should pass with WARNING level violation"),
314 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
315 () -> assertThat("finding should have WARNING severity", handler.getFindings(),
316 hasItem(hasProperty("severity", is(Level.WARNING)))));
317 }
318
319
320
321
322
323
324
325 @SuppressWarnings("null")
326 @Test
327 void testExpectConstraintWithErrorSeverity() throws ConstraintValidationException {
328 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
329
330 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test"));
331
332 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
333
334 ISource source = mock(ISource.class);
335
336
337 IExpectConstraint expectConstraint = IExpectConstraint.builder()
338 .source(source)
339 .level(Level.ERROR)
340 .test(IMetapathExpression.compile("string-length(.) > 10"))
341 .build();
342
343 doReturn(flagDefinition).when(flag).getDefinition();
344 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
345
346 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
347 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
348 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
349 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
350 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
351
352 StaticContext staticContext = StaticContext.instance();
353 doReturn(staticContext).when(source).getStaticContext();
354
355 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
356 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
357 DynamicContext dynamicContext = new DynamicContext(staticContext);
358 validator.validate(flag, dynamicContext);
359 validator.finalizeValidation(dynamicContext);
360
361 assertAll(
362 () -> assertFalse(handler.isPassing(), "validation should fail with ERROR level violation"),
363 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
364 () -> assertThat("finding should have ERROR severity", handler.getFindings(),
365 hasItem(hasProperty("severity", is(Level.ERROR)))));
366 }
367
368
369
370
371
372
373
374 @SuppressWarnings("null")
375 @Test
376 void testExpectConstraintWithCriticalSeverity() throws ConstraintValidationException {
377 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
378
379 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test"));
380
381 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
382
383 ISource source = mock(ISource.class);
384
385
386 IExpectConstraint expectConstraint = IExpectConstraint.builder()
387 .source(source)
388 .level(Level.CRITICAL)
389 .test(IMetapathExpression.compile("string-length(.) > 10"))
390 .build();
391
392 doReturn(flagDefinition).when(flag).getDefinition();
393 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
394
395 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
396 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
397 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
398 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
399 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
400
401 StaticContext staticContext = StaticContext.instance();
402 doReturn(staticContext).when(source).getStaticContext();
403
404 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
405 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
406 DynamicContext dynamicContext = new DynamicContext(staticContext);
407 validator.validate(flag, dynamicContext);
408 validator.finalizeValidation(dynamicContext);
409
410 assertAll(
411 () -> assertFalse(handler.isPassing(), "validation should fail with CRITICAL level violation"),
412 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
413 () -> assertThat("finding should have CRITICAL severity", handler.getFindings(),
414 hasItem(hasProperty("severity", is(Level.CRITICAL)))));
415 }
416
417
418
419
420
421
422
423 @SuppressWarnings("null")
424 @Test
425 void testExpectConstraintWithComplexExpression() throws ConstraintValidationException {
426 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
427
428 IFlagNodeItem flag = itemFactory.flag(qname("email"), IStringItem.valueOf("user@example.com"));
429
430 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
431
432 ISource source = mock(ISource.class);
433
434
435 IExpectConstraint expectConstraint = IExpectConstraint.builder()
436 .source(source)
437 .test(IMetapathExpression.compile("contains(., '@') and string-length(.) > 5"))
438 .build();
439
440 doReturn(flagDefinition).when(flag).getDefinition();
441 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
442
443 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
444 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
445 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
446 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
447 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
448
449 StaticContext staticContext = StaticContext.instance();
450 doReturn(staticContext).when(source).getStaticContext();
451
452 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
453 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
454 DynamicContext dynamicContext = new DynamicContext(staticContext);
455 validator.validate(flag, dynamicContext);
456 validator.finalizeValidation(dynamicContext);
457
458 assertTrue(handler.isPassing(), "constraint should pass when complex expression evaluates to true");
459 }
460
461
462
463
464
465
466
467 @SuppressWarnings("null")
468 @Test
469 void testExpectConstraintWithMetadata() throws ConstraintValidationException {
470 MockNodeItemFactory itemFactory = new MockNodeItemFactory();
471
472 IFlagNodeItem flag = itemFactory.flag(qname("value"), IStringItem.valueOf("test"));
473
474 IFlagDefinition flagDefinition = mock(IFlagDefinition.class);
475
476 ISource source = mock(ISource.class);
477
478
479 IExpectConstraint expectConstraint = IExpectConstraint.builder()
480 .source(source)
481 .identifier("expect-001")
482 .formalName("Minimum Length Constraint")
483 .description(MarkupLine.fromMarkdown("Ensures value has minimum length of 10 characters"))
484 .test(IMetapathExpression.compile("string-length(.) >= 10"))
485 .message("Value is too short")
486 .build();
487
488 doReturn(flagDefinition).when(flag).getDefinition();
489 doReturn("flag/path").when(flag).toPath(any(IPathFormatter.class));
490
491 doReturn(CollectionUtil.emptyMap()).when(flagDefinition).getLetExpressions();
492 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getAllowedValuesConstraints();
493 doReturn(CollectionUtil.singletonList(expectConstraint)).when(flagDefinition).getExpectConstraints();
494 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getMatchesConstraints();
495 doReturn(CollectionUtil.emptyList()).when(flagDefinition).getIndexHasKeyConstraints();
496
497 StaticContext staticContext = StaticContext.instance();
498 doReturn(staticContext).when(source).getStaticContext();
499
500 FindingCollectingConstraintValidationHandler handler = new FindingCollectingConstraintValidationHandler();
501 DefaultConstraintValidator validator = new DefaultConstraintValidator(handler);
502 DynamicContext dynamicContext = new DynamicContext(staticContext);
503 validator.validate(flag, dynamicContext);
504 validator.finalizeValidation(dynamicContext);
505
506 assertAll(
507 () -> assertFalse(handler.isPassing(), "constraint should fail"),
508 () -> assertThat("should have 1 finding", handler.getFindings(), hasSize(1)),
509 () -> assertThat("constraint should have id", expectConstraint.getId(), is("expect-001")),
510 () -> assertThat("constraint should have formal name", expectConstraint.getFormalName(),
511 is("Minimum Length Constraint")));
512 }
513 }