Refactor YangInstanceIdentifierDeserializer
[netconf.git] / restconf / restconf-nb / src / test / java / org / opendaylight / restconf / server / spi / ApiPathNormalizerTest.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  * Copyright (c) 2021 PANTHEON.tech, s.r.o.
4  *
5  * This program and the accompanying materials are made available under the
6  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
7  * and is available at http://www.eclipse.org/legal/epl-v10.html
8  */
9 package org.opendaylight.restconf.server.spi;
10
11 import static org.junit.jupiter.api.Assertions.assertEquals;
12 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
13 import static org.junit.jupiter.api.Assertions.assertNotNull;
14 import static org.junit.jupiter.api.Assertions.assertThrows;
15
16 import com.google.common.collect.ImmutableMap;
17 import java.text.ParseException;
18 import org.junit.jupiter.api.Test;
19 import org.opendaylight.restconf.api.ApiPath;
20 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
21 import org.opendaylight.restconf.common.errors.RestconfError;
22 import org.opendaylight.restconf.server.api.DatabindContext;
23 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Result;
24 import org.opendaylight.yangtools.yang.common.ErrorTag;
25 import org.opendaylight.yangtools.yang.common.ErrorType;
26 import org.opendaylight.yangtools.yang.common.QName;
27 import org.opendaylight.yangtools.yang.common.Revision;
28 import org.opendaylight.yangtools.yang.common.Uint16;
29 import org.opendaylight.yangtools.yang.common.Uint8;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
34 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
35
36 /**
37  * Unit tests for {@link ApiPathNormalizer}.
38  */
39 class ApiPathNormalizerTest {
40     private static final QName ACTIONS_INTERFACES =
41         QName.create("https://example.com/ns/example-actions", "2016-07-07", "interfaces");
42
43     private static final ApiPathNormalizer NORMALIZER = new ApiPathNormalizer(DatabindContext.ofModel(
44         YangParserTestUtils.parseYangResourceDirectory("/restconf/parser/deserializer")));
45
46     /**
47      * Test of deserialization <code>String</code> URI with container to
48      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
49      */
50     @Test
51     void deserializeContainerTest() {
52         final var result = assertNormalizedPath("deserializer-test:contA").path.getPathArguments();
53         assertEquals(1, result.size());
54         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
55     }
56
57     /**
58      * Test of deserialization <code>String</code> URI with container containing leaf to
59      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
60      */
61     @Test
62     void deserializeContainerWithLeafTest() {
63         final var result = assertNormalizedPath("deserializer-test:contA/leaf-A").path.getPathArguments();
64         assertEquals(2, result.size());
65         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
66         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "leaf-A")), result.get(1));
67     }
68
69     /**
70      * Test of deserialization <code>String</code> URI with container containing list with leaf list to
71      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
72      */
73     @Test
74     void deserializeContainerWithListWithLeafListTest() {
75         final var result = assertNormalizedPath("deserializer-test:contA/list-A=100/leaf-list-AA=instance").path
76             .getPathArguments();
77         assertEquals(5, result.size());
78
79         // container
80         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
81         // list
82         final var list = QName.create("deserializer:test", "2016-06-06", "list-A");
83         assertEquals(NodeIdentifier.create(list), result.get(1));
84         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint8.valueOf(100)),
85             result.get(2));
86         // leaf list
87         final var leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-AA");
88         assertEquals(NodeIdentifier.create(leafList), result.get(3));
89         assertEquals(new NodeWithValue<>(leafList, "instance"), result.get(4));
90     }
91
92     /**
93      * Test of deserialization <code>String</code> URI with container containing list with Action to
94      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
95      */
96     @Test
97     void deserializeContainerWithListWithActionTest() {
98         final var result = assertNormalizedPath("example-actions:interfaces/interface=eth0/reset").path
99             .getPathArguments();
100         assertEquals(4, result.size());
101         // container
102         assertEquals(NodeIdentifier.create(ACTIONS_INTERFACES), result.get(0));
103         // list
104         final var list = QName.create(ACTIONS_INTERFACES, "interface");
105         assertEquals(NodeIdentifier.create(list), result.get(1));
106         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "eth0"), result.get(2));
107         // action
108         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "reset")), result.get(3));
109     }
110
111     /**
112      * Test of deserialization <code>String</code> URI with container containing choice node with Action to
113      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
114      */
115     @Test
116     void deserializeContainerWithChoiceSchemaNodeWithActionTest() {
117         final var result = assertNormalizedPath("example-actions:interfaces/typeA-gigabyte/interface=eth0/reboot").path
118             .getPathArguments();
119         assertEquals(6, result.size());
120
121         // container
122         assertEquals(NodeIdentifier.create(ACTIONS_INTERFACES), result.get(0));
123         // choice
124         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "interface-type")), result.get(1));
125         // container
126         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "typeA-gigabyte")), result.get(2));
127
128         // list
129         final var list = QName.create(ACTIONS_INTERFACES, "interface");
130         assertEquals(NodeIdentifier.create(list), result.get(3));
131         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "eth0"), result.get(4));
132
133         // action QName
134         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "reboot")), result.get(5));
135     }
136
137     /**
138      * Test of deserialization <code>String</code> URI with container containing choice node with Action to
139      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
140      */
141     @Test
142     void deserializeContainerWithChoiceCaseSchemaNodeWithActionTest() {
143         final var result = assertNormalizedPath("example-actions:interfaces/udp/reboot").path.getPathArguments();
144         assertEquals(4, result.size());
145         // container
146         assertEquals(NodeIdentifier.create(ACTIONS_INTERFACES), result.get(0));
147         // choice
148         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "protocol")), result.get(1));
149         // choice container
150         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "udp")), result.get(2));
151         // action QName
152         assertEquals(NodeIdentifier.create(QName.create(ACTIONS_INTERFACES, "reboot")), result.get(3));
153     }
154
155     /**
156      * Test of deserialization <code>String</code> URI containing list with no keys to
157      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
158      */
159     @Test
160     void deserializeListWithNoKeysTest() {
161         final var result = assertNormalizedPath("deserializer-test:list-no-key").path.getPathArguments();
162         assertEquals(2, result.size());
163         final var list = QName.create("deserializer:test", "2016-06-06", "list-no-key");
164         assertEquals(NodeIdentifier.create(list), result.get(0));
165         assertEquals(NodeIdentifier.create(list), result.get(1));
166     }
167
168     /**
169      * Test of deserialization <code>String</code> URI containing list with one key to
170      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
171      */
172     @Test
173     void deserializeListWithOneKeyTest() {
174         final var result = assertNormalizedPath("deserializer-test:list-one-key=value").path.getPathArguments();
175         assertEquals(2, result.size());
176         final QName list = QName.create("deserializer:test", "2016-06-06", "list-one-key");
177         assertEquals(NodeIdentifier.create(list), result.get(0));
178         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "value"), result.get(1));
179     }
180
181     /**
182      * Test of deserialization <code>String</code> URI containing list with multiple keys to
183      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
184      */
185     @Test
186     void deserializeListWithMultipleKeysTest() {
187         final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
188         final var values = ImmutableMap.<QName, Object>of(
189             QName.create(list, "name"), "value",
190             QName.create(list, "number"), Uint8.valueOf(100),
191             QName.create(list, "enabled"), false);
192
193         final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=value,100,false").path
194             .getPathArguments();
195         assertEquals(2, result.size());
196         assertEquals(NodeIdentifier.create(list), result.get(0));
197         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
198     }
199
200     /**
201      * Test of deserialization <code>String</code> URI containing leaf list to
202      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
203      */
204     @Test
205     void deserializeLeafListTest() {
206         final var result = assertNormalizedPath("deserializer-test:leaf-list-0=true").path.getPathArguments();
207         assertEquals(2, result.size());
208
209         final QName leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-0");
210         assertEquals(new NodeIdentifier(leafList), result.get(0));
211         assertEquals(new NodeWithValue<>(leafList, true), result.get(1));
212     }
213
214     /**
215      * Test when empty <code>String</code> is supplied as an input. Test is expected to return empty result.
216      */
217     @Test
218     void deserializeEmptyDataTest() {
219         assertEquals(YangInstanceIdentifier.of(), assertNormalizedPath("").path);
220     }
221
222     /**
223      * Negative test when supplied <code>String</code> data to deserialize is null.
224      */
225     @Test
226     void nullDataNegativeNegativeTest() {
227         assertThrows(NullPointerException.class, () -> assertNormalizedPath(null));
228     }
229
230     /**
231      * Negative test of creating <code>QName</code> when it is not possible to find module for specified prefix. Test is
232      * expected to fail with <code>RestconfDocumentedException</code>.
233      */
234     @Test
235     void prepareQnameNotExistingPrefixNegativeTest() {
236         final var error = assertErrorPath("not-existing:contA");
237         assertEquals("Failed to lookup for module with name 'not-existing'.", error.getErrorMessage());
238         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
239         assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.getErrorTag());
240     }
241
242     /**
243      * Negative test of creating <code>QName</code> when after identifier and colon there is node name of unknown
244      * node in current container. Test is expected to fail with <code>RestconfDocumentedException</code> and error
245      * type, error tag and error status code are compared to expected values.
246      */
247     @Test
248     public void prepareQnameNotValidContainerNameNegativeTest() {
249         final var error = assertErrorPath("deserializer-test:contA/leafB");
250         assertEquals("Schema for '(deserializer:test?revision=2016-06-06)leafB' not found", error.getErrorMessage());
251         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
252         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
253     }
254
255     /**
256      * Negative test of creating <code>QName</code> when after identifier and equals there is node name of unknown
257      * node in current list. Test is expected to fail with <code>RestconfDocumentedException</code> and error
258      * type, error tag and error status code are compared to expected values.
259      */
260     @Test
261     void prepareQnameNotValidListNameNegativeTest() {
262         final var error = assertErrorPath("deserializer-test:list-no-key/disabled=false");
263         assertEquals("Schema for '(deserializer:test?revision=2016-06-06)disabled' not found", error.getErrorMessage());
264         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
265         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
266     }
267
268     /**
269      * Negative test of getting next identifier when current node is keyed entry. Test is expected to
270      * fail with <code>RestconfDocumentedException</code>.
271      */
272     @Test
273     void prepareIdentifierNotKeyedEntryNegativeTest() {
274         final var error = assertErrorPath("deserializer-test:list-one-key");
275         assertEquals("""
276             Entry '(deserializer:test?revision=2016-06-06)list-one-key' requires key or value predicate to be \
277             present.""", error.getErrorMessage());
278         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
279         assertEquals(ErrorTag.MISSING_ATTRIBUTE, error.getErrorTag());
280     }
281
282     /**
283      * Negative test when there is a comma also after the last key. Test is expected to fail with
284      * <code>RestconfDocumentedException</code>. Last comma indicates a fourth key, which is a mismatch with schema.
285      */
286     @Test
287     void deserializeKeysEndsWithCommaTooManyNegativeTest() {
288         final var error = assertErrorPath("deserializer-test:list-multiple-keys=value,100,false,");
289         assertEquals("""
290             Schema for (deserializer:test?revision=2016-06-06)list-multiple-keys requires 3 key values, 4 supplied""",
291             error.getErrorMessage());
292         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
293         assertEquals(ErrorTag.UNKNOWN_ATTRIBUTE, error.getErrorTag());
294     }
295
296     /**
297      * Negative test when there is a comma also after the last key. Test is expected to fail with
298      * <code>RestconfDocumentedException</code>. Last comma indicates a third key, whose is a mismatch with schema.
299      */
300     @Test
301     void deserializeKeysEndsWithCommaIllegalNegativeTest() {
302         final var error = assertErrorPath("deserializer-test:list-multiple-keys=value,100,");
303         assertEquals("Invalid value '' for (deserializer:test?revision=2016-06-06)enabled", error.getErrorMessage());
304         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
305         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
306         assertEquals("Invalid value '' for boolean type. Allowed values are 'true' and 'false'", error.getErrorInfo());
307     }
308
309     /**
310      * Positive when not all keys of list are encoded. The missing keys should be considered to has empty
311      * <code>String</code> values. Also value of next leaf must not be considered to be missing key value.
312      */
313     @Test
314     void notAllListKeysEncodedPositiveTest() {
315         final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
316         final var values = ImmutableMap.<QName, Object>of(
317             QName.create(list, "name"), ":foo",
318             QName.create(list, "number"), Uint8.ONE,
319             QName.create(list, "enabled"), false);
320
321         final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=%3Afoo,1,false/string-value")
322             .path.getPathArguments();
323         assertEquals(3, result.size());
324         // list
325         assertEquals(NodeIdentifier.create(list), result.get(0));
326         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
327         // leaf
328         assertEquals(new NodeIdentifier(QName.create("deserializer:test", "2016-06-06", "string-value")),
329             result.get(2));
330     }
331
332     /**
333      * Negative test when not all keys of list are encoded and it is not possible to consider missing keys to be empty.
334      * Test is expected to fail with <code>RestconfDocumentedException</code> and error type, error tag and error
335      * status code are compared to expected values.
336      */
337     @Test
338     void notAllListKeysEncodedNegativeTest() {
339         final var error = assertErrorPath("deserializer-test:list-multiple-keys=%3Afoo/string-value");
340         assertEquals("""
341             Schema for (deserializer:test?revision=2016-06-06)list-multiple-keys requires 3 key values, 1 supplied""",
342             error.getErrorMessage());
343         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
344         assertEquals(ErrorTag.MISSING_ATTRIBUTE, error.getErrorTag());
345     }
346
347     /**
348      * Test URI with list where key value starts with, ends with or contains percent encoded characters.The encoded
349      * value should be complete also with not percent-encoded parts.
350      */
351     @Test
352     void percentEncodedKeyEndsWithNoPercentEncodedChars() {
353         final var URI = "deserializer-test:list-multiple-keys=%3Afoo,1,true";
354         final var result = assertNormalizedPath(URI).path;
355
356         final var resultListKeys = assertInstanceOf(NodeIdentifierWithPredicates.class, result.getLastPathArgument())
357             .entrySet().iterator();
358         assertEquals(":foo", resultListKeys.next().getValue());
359         assertEquals(Uint8.ONE, resultListKeys.next().getValue());
360         assertEquals(true, resultListKeys.next().getValue());
361     }
362
363     /**
364      * Positive test when all keys of list can be considered to be empty <code>String</code>.
365      */
366     @Test
367     void deserializeAllKeysEmptyTest() {
368         final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
369         final var values = ImmutableMap.<QName, Object>of(
370             QName.create(list, "name"), "",
371             QName.create(list, "number"), Uint8.ZERO,
372             QName.create(list, "enabled"), true);
373
374         final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=,0,true").path.getPathArguments();
375         assertEquals(2, result.size());
376         assertEquals(NodeIdentifier.create(list), result.get(0));
377         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
378     }
379
380     /**
381      * Negative test of deserialization when for leaf list there is no specified instance value.
382      * <code>RestconfDocumentedException</code> is expected and error type, error tag and error status code are
383      * compared to expected values.
384      */
385     @Test
386     void leafListMissingKeyNegativeTest() {
387         final var error = assertErrorPath("deserializer-test:leaf-list-0=");
388         assertEquals("Invalid value '' for (deserializer:test?revision=2016-06-06)leaf-list-0",
389             error.getErrorMessage());
390         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
391         assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
392     }
393
394     /**
395      * Positive test of deserialization when parts of input URI <code>String</code> are defined in another module.
396      */
397     @Test
398     void deserializePartInOtherModuleTest() {
399         final var result = assertNormalizedPath(
400             "deserializer-test-included:augmented-list=100/deserializer-test:augmented-leaf").path.getPathArguments();
401         assertEquals(3, result.size());
402
403         // list
404         final var list = QName.create("deserializer:test:included", "2016-06-06", "augmented-list");
405         assertEquals(NodeIdentifier.create(list), result.get(0));
406         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint16.valueOf(100)),
407             result.get(1));
408
409         // augmented leaf
410         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "augmented-leaf")),
411             result.get(2));
412     }
413
414     @Test
415     void deserializeListInOtherModuleTest() {
416         final var result = assertNormalizedPath(
417             "deserializer-test-included:augmented-list=100/deserializer-test:augmenting-list=0")
418             .path.getPathArguments();
419         assertEquals(4, result.size());
420
421         // list
422         final var list = QName.create("deserializer:test:included", "2016-06-06", "augmented-list");
423         assertEquals(NodeIdentifier.create(list), result.get(0));
424         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint16.valueOf(100)),
425             result.get(1));
426
427         // augmented list
428         final var augList = QName.create("deserializer:test", "2016-06-06", "augmenting-list");
429         assertEquals(NodeIdentifier.create(augList), result.get(2));
430         assertEquals(NodeIdentifierWithPredicates.of(augList, QName.create(augList, "id"), 0), result.get(3));
431     }
432
433     /**
434      * Deserialization of path that contains list entry with key which value is described by leaflef to identityref.
435      */
436     @Test
437     void deserializePathWithIdentityrefKeyValueTest() {
438         assertIdentityrefKeyValue(
439             "deserializer-test-included:refs/list-with-identityref=deserializer-test%3Aderived-identity/foo");
440     }
441
442     /**
443      * Identityref key value is not encoded correctly - ':' character must be encoded as '%3A'.
444      */
445     @Test
446     void deserializePathWithInvalidIdentityrefKeyValueTest() {
447         assertIdentityrefKeyValue(
448             "deserializer-test-included:refs/list-with-identityref=deserializer-test:derived-identity/foo");
449     }
450
451     private static RestconfError assertErrorPath(final String path) {
452         final var apiPath = assertApiPath(path);
453         final var ex = assertThrows(RestconfDocumentedException.class, () -> NORMALIZER.normalizePath(apiPath));
454         final var errors = ex.getErrors();
455         assertEquals(1, errors.size());
456         return errors.get(0);
457     }
458
459     private static Result assertNormalizedPath(final String path) {
460         final var result = NORMALIZER.normalizePath(assertApiPath(path));
461         assertNotNull(result);
462         return result;
463     }
464
465     private static ApiPath assertApiPath(final String path) {
466         try {
467             return ApiPath.parse(path);
468         } catch (ParseException e) {
469             throw new AssertionError(e);
470         }
471     }
472
473     private static void assertIdentityrefKeyValue(final String path) {
474         final var pathArgs = assertNormalizedPath(path).path.getPathArguments();
475         assertEquals(4, pathArgs.size());
476
477         assertEquals("refs", pathArgs.get(0).getNodeType().getLocalName());
478         assertEquals("list-with-identityref", pathArgs.get(1).getNodeType().getLocalName());
479
480         final var listEntryArg = assertInstanceOf(NodeIdentifierWithPredicates.class, pathArgs.get(2));
481         assertEquals("list-with-identityref", listEntryArg.getNodeType().getLocalName());
482         final var keys = listEntryArg.keySet();
483         assertEquals(1, keys.size());
484         assertEquals("id", keys.iterator().next().getLocalName());
485         final var keyValue = listEntryArg.values().iterator().next();
486         assertEquals(QName.create("deserializer:test", "derived-identity", Revision.of("2016-06-06")), keyValue);
487
488         assertEquals("foo", pathArgs.get(3).getNodeType().getLocalName());
489     }
490 }