Improve YangInstanceIdentifierDeserializerTest
[netconf.git] / restconf / restconf-nb-rfc8040 / src / test / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierDeserializerTest.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.nb.rfc8040.utils.parser;
10
11 import static org.hamcrest.CoreMatchers.instanceOf;
12 import static org.hamcrest.MatcherAssert.assertThat;
13 import static org.junit.Assert.assertEquals;
14 import static org.junit.Assert.assertThrows;
15
16 import com.google.common.collect.ImmutableMap;
17 import java.io.FileNotFoundException;
18 import java.util.Iterator;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Set;
23 import org.junit.AfterClass;
24 import org.junit.BeforeClass;
25 import org.junit.Test;
26 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
27 import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
28 import org.opendaylight.yangtools.yang.common.ErrorTag;
29 import org.opendaylight.yangtools.yang.common.ErrorType;
30 import org.opendaylight.yangtools.yang.common.QName;
31 import org.opendaylight.yangtools.yang.common.Revision;
32 import org.opendaylight.yangtools.yang.common.Uint16;
33 import org.opendaylight.yangtools.yang.common.Uint8;
34 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
35 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
40 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
41 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
42
43 /**
44  * Unit tests for {@link YangInstanceIdentifierDeserializer}.
45  */
46 public class YangInstanceIdentifierDeserializerTest {
47     // schema context
48     private static EffectiveModelContext SCHEMA_CONTEXT;
49
50     @BeforeClass
51     public static void beforeClass() throws FileNotFoundException {
52         SCHEMA_CONTEXT =
53                 YangParserTestUtils.parseYangFiles(TestRestconfUtils.loadFiles("/restconf/parser/deserializer"));
54     }
55
56     @AfterClass
57     public static void afterClass() {
58         SCHEMA_CONTEXT = null;
59     }
60
61     /**
62      * Test of deserialization <code>String</code> URI with container to
63      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
64      */
65     @Test
66     public void deserializeContainerTest() {
67         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:contA");
68         assertEquals(1, result.size());
69         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
70     }
71
72     /**
73      * Test of deserialization <code>String</code> URI with container containing leaf to
74      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
75      */
76     @Test
77     public void deserializeContainerWithLeafTest() {
78         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:contA/leaf-A");
79         assertEquals(2, result.size());
80         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
81         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "leaf-A")), result.get(1));
82     }
83
84     /**
85      * Test of deserialization <code>String</code> URI with container containing list with leaf list to
86      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
87      */
88     @Test
89     public void deserializeContainerWithListWithLeafListTest() {
90         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
91             "deserializer-test:contA/list-A=100/leaf-list-AA=instance");
92         assertEquals(5, result.size());
93
94         // container
95         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
96         // list
97         final QName list = QName.create("deserializer:test", "2016-06-06", "list-A");
98         assertEquals(NodeIdentifier.create(list), result.get(1));
99         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint8.valueOf(100)),
100             result.get(2));
101         // leaf list
102         final QName leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-AA");
103         assertEquals(NodeIdentifier.create(leafList), result.get(3));
104         assertEquals(new NodeWithValue<>(leafList, "instance"), result.get(4));
105     }
106
107     /**
108      * Test of deserialization <code>String</code> URI with container containing list with Action to
109      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
110      */
111     @Test
112     public void deserializeContainerWithListWithActionTest() {
113         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
114             "example-actions:interfaces/interface=eth0/reset");
115         assertEquals(4, result.size());
116         // container
117         assertEquals(NodeIdentifier.create(
118             QName.create("https://example.com/ns/example-actions", "2016-07-07", "interfaces")), result.get(0));
119         // list
120         final QName list = QName.create("https://example.com/ns/example-actions", "2016-07-07", "interface");
121         assertEquals(NodeIdentifier.create(list), result.get(1));
122         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "eth0"), result.get(2));
123         // action
124         assertEquals(NodeIdentifier.create(
125             QName.create("https://example.com/ns/example-actions", "2016-07-07", "reset")), result.get(3));
126     }
127
128     /**
129      * Test of deserialization <code>String</code> URI containing list with no keys to
130      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
131      */
132     @Test
133     public void deserializeListWithNoKeysTest() {
134         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:list-no-key");
135         assertEquals(2, result.size());
136         final QName list = QName.create("deserializer:test", "2016-06-06", "list-no-key");
137         assertEquals(NodeIdentifier.create(list), result.get(0));
138         assertEquals(NodeIdentifier.create(list), result.get(1));
139     }
140
141     /**
142      * Test of deserialization <code>String</code> URI containing list with one key to
143      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
144      */
145     @Test
146     public void deserializeListWithOneKeyTest() {
147         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
148             "deserializer-test:list-one-key=value");
149         assertEquals(2, result.size());
150         final QName list = QName.create("deserializer:test", "2016-06-06", "list-one-key");
151         assertEquals(NodeIdentifier.create(list), result.get(0));
152         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "value"), result.get(1));
153     }
154
155     /**
156      * Test of deserialization <code>String</code> URI containing list with multiple keys to
157      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
158      */
159     @Test
160     public void deserializeListWithMultipleKeysTest() {
161         final QName list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
162         final Map<QName, Object> values = ImmutableMap.of(
163             QName.create(list, "name"), "value",
164             QName.create(list, "number"), Uint8.valueOf(100),
165             QName.create(list, "enabled"), false);
166
167         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
168             "deserializer-test:list-multiple-keys=value,100,false");
169         assertEquals(2, result.size());
170         assertEquals(NodeIdentifier.create(list), result.get(0));
171         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
172     }
173
174     /**
175      * Test of deserialization <code>String</code> URI containing leaf list to
176      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
177      */
178     @Test
179     public void deserializeLeafListTest() {
180         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
181             "deserializer-test:leaf-list-0=true");
182         assertEquals(2, result.size());
183
184         final QName leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-0");
185         assertEquals(new NodeIdentifier(leafList), result.get(0));
186         assertEquals(new NodeWithValue<>(leafList, true), result.get(1));
187     }
188
189     /**
190      * Test when empty <code>String</code> is supplied as an input. Test is expected to return empty result.
191      */
192     @Test
193     public void deserializeEmptyDataTest() {
194         assertEquals(List.of(), YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, ""));
195     }
196
197     /**
198      * Test of deserialization <code>String</code> URI with identifiers separated by multiple slashes to
199      * {@code Iterable<YangInstanceIdentifier.PathArgument>}.
200      */
201     @Test
202     public void deserializeMultipleSlashesTest() {
203         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
204             "deserializer-test:contA////list-A=40//list-key");
205         assertEquals(4, result.size());
206
207         // container
208         assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
209         // list
210         final QName list = QName.create("deserializer:test", "2016-06-06", "list-A");
211         assertEquals(NodeIdentifier.create(list), result.get(1));
212         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint8.valueOf(40)),
213             result.get(2));
214         // leaf
215         assertEquals(new NodeIdentifier(QName.create("deserializer:test", "2016-06-06", "list-key")), result.get(3));
216     }
217
218     /**
219      * Negative test when supplied <code>SchemaContext</code> is null. Test is expected to fail with
220      * <code>NullPointerException</code>.
221      */
222     @Test
223     public void deserializeNullSchemaContextNegativeTest() {
224         assertThrows(NullPointerException.class,
225             () -> YangInstanceIdentifierDeserializer.create(null, "deserializer-test:contA"));
226     }
227
228     /**
229      * Negative test when supplied <code>String</code> data to deserialize is null. Test is expected to fail with
230      * <code>NullPointerException</code>.
231      */
232     @Test
233     public void nullDataNegativeNegativeTest() {
234         assertThrows(NullPointerException.class,
235             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, null));
236     }
237
238     /**
239      * Negative test when identifier is not followed by slash or equals. Test is expected to fail with
240      * <code>RestconfDocumentedException</code>.
241      */
242     @Test
243     public void deserializeBadCharMissingSlashOrEqualNegativeTest() {
244         final var ex = assertThrows(RestconfDocumentedException.class,
245             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:cont*leaf-A"));
246         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
247             + "error-message: Could not parse Instance Identifier 'deserializer-test:cont*leaf-A'. Offset: '22' : "
248             + "Reason: Bad char '*' on the current position.]]", ex.getMessage());
249         final var errors = ex.getErrors();
250         assertEquals(1, errors.size());
251         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
252         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
253     }
254
255     /**
256      * Negative test of validating identifier when there is a slash after container without next identifier. Test
257      * is expected to fail with <code>RestconfDocumentedException</code>.
258      */
259     @Test
260     public void validArgIdentifierContainerEndsWithSlashNegativeTest() {
261         final var ex = assertThrows(RestconfDocumentedException.class,
262             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:contA/"));
263         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
264             + "error-message: Could not parse Instance Identifier 'deserializer-test:contA/'. Offset: '24' : "
265             + "Reason: Identifier cannot end with '/'.]]", ex.getMessage());
266         final var errors = ex.getErrors();
267         assertEquals(1, errors.size());
268         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
269         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
270     }
271
272     /**
273      * Negative test of validating identifier when there are multiple slashes after container without next identifier.
274      * Test is expected to fail with <code>RestconfDocumentedException</code>.
275      */
276     @Test
277     public void validArgIdentifierContainerEndsWithMultipleSlashesNegativeTest() {
278         final var ex = assertThrows(RestconfDocumentedException.class,
279             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:contA///"));
280         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
281             + "error-message: Could not parse Instance Identifier 'deserializer-test:contA///'. Offset: '26' : "
282             + "Reason: Identifier cannot end with '/'.]]", ex.getMessage());
283         final var errors = ex.getErrors();
284         assertEquals(1, errors.size());
285         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
286         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
287     }
288
289     /**
290      * Negative test of validating identifier when there is a slash after list key values without next identifier. Test
291      * is expected to fail with <code>RestconfDocumentedException</code>.
292      */
293     @Test
294     public void validArgIdentifierListEndsWithSlashLNegativeTest() {
295         final var ex = assertThrows(RestconfDocumentedException.class,
296             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:list-one-key=value/"));
297         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
298             + "error-message: Could not parse Instance Identifier 'deserializer-test:list-one-key=value/'. "
299             + "Offset: '37' : Reason: Identifier cannot end with '/'.]]", ex.getMessage());
300         final var errors = ex.getErrors();
301         assertEquals(1, errors.size());
302         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
303         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
304     }
305
306     /**
307      * Negative test of validating identifier when there are multiple slashes after list key values without next
308      * identifier. Test is expected to fail with <code>RestconfDocumentedException</code>.
309      */
310     @Test
311     public void validArgIdentifierListEndsWithSlashesNegativeTest() {
312         final var ex = assertThrows(RestconfDocumentedException.class,
313             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:list-one-key=value//"));
314         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
315             + "error-message: Could not parse Instance Identifier 'deserializer-test:list-one-key=value//'. "
316             + "Offset: '38' : Reason: Identifier cannot end with '/'.]]", ex.getMessage());
317         final var errors = ex.getErrors();
318         assertEquals(1, errors.size());
319         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
320         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
321     }
322
323     /**
324      * Negative test of creating <code>QName</code> when identifier is empty (example: '/'). Test is expected to fail
325      * with <code>RestconfDocumentedException</code>.
326      */
327     @Test
328     public void prepareQnameEmptyIdentifierNegativeTest() {
329         final var ex = assertThrows(RestconfDocumentedException.class,
330             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "/"));
331         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
332             + "error-message: Could not parse Instance Identifier '/'. Offset: '0' : "
333             + "Reason: Identifier must start with character from set 'a-zA-Z_']]", ex.getMessage());
334         final var errors = ex.getErrors();
335         assertEquals(1, errors.size());
336         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
337         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
338     }
339
340     /**
341      * Negative test of creating <code>QName</code> when in identifier there is another sign than colon or equals.
342      * Test is expected to fail with <code>RestconfDocumentedException</code>.
343      */
344     @Test
345     public void prepareQnameBuildPathNegativeTest() {
346         final var ex = assertThrows(RestconfDocumentedException.class,
347             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test*contA"));
348         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
349             + "error-message: Could not parse Instance Identifier 'deserializer-test*contA'. Offset: '17' : "
350             + "Reason: Bad char '*' on the current position.]]", ex.getMessage());
351         final var errors = ex.getErrors();
352         assertEquals(1, errors.size());
353         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
354         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
355     }
356
357     /**
358      * Negative test of creating <code>QName</code> when it is not possible to find module for specified prefix. Test is
359      * expected to fail with <code>RestconfDocumentedException</code>.
360      */
361     @Test
362     public void prepareQnameNotExistingPrefixNegativeTest() {
363         final var ex = assertThrows(RestconfDocumentedException.class,
364             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "not-existing:contA"));
365         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: unknown-element, "
366             + "error-message: Failed to lookup for module with name 'not-existing'.]]", ex.getMessage());
367         final var errors = ex.getErrors();
368         assertEquals(1, errors.size());
369         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
370         assertEquals(ErrorTag.UNKNOWN_ELEMENT, errors.get(0).getErrorTag());
371     }
372
373     /**
374      * Negative test of creating <code>QName</code> when after prefix and colon there is not parsable identifier as
375      * local name. Test is expected to fail with <code>RestconfDocumentedException</code>.
376      */
377     @Test
378     public void prepareQnameNotValidPrefixAndLocalNameNegativeTest() {
379         final var ex = assertThrows(RestconfDocumentedException.class, () ->
380             YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:*not-parsable-identifier"));
381         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, "
382             + "error-message: Could not parse Instance Identifier 'deserializer-test:*not-parsable-identifier'. "
383             + "Offset: '18' : Reason: Identifier must start with character from set 'a-zA-Z_']]", ex.getMessage());
384         final var errors = ex.getErrors();
385         assertEquals(1, errors.size());
386         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
387         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
388     }
389
390     /**
391      * Negative test of creating <code>QName</code> when data ends after prefix and colon. Test is expected to fail
392      * with <code>StringIndexOutOfBoundsException</code>.
393      */
394     @Test
395     public void prepareQnameErrorParsingNegativeTest() {
396         // FIXME: this is just wrong
397         assertThrows(StringIndexOutOfBoundsException.class,
398             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:"));
399     }
400
401     /**
402      * Negative test of creating <code>QName</code> when after identifier and colon there is node name of unknown
403      * node in current container. Test is expected to fail with <code>RestconfDocumentedException</code> and error
404      * type, error tag and error status code are compared to expected values.
405      */
406     @Test
407     public void prepareQnameNotValidContainerNameNegativeTest() {
408         final var ex = assertThrows(RestconfDocumentedException.class,
409             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:contA/leafB"));
410         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: data-missing, "
411             + "error-message: Schema node leafB does not exist in module.]]", ex.getMessage());
412         final var errors = ex.getErrors();
413         assertEquals(1, errors.size());
414         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
415         assertEquals(ErrorTag.DATA_MISSING, errors.get(0).getErrorTag());
416     }
417
418     /**
419      * Negative test of creating <code>QName</code> when after identifier and equals there is node name of unknown
420      * node in current list. Test is expected to fail with <code>RestconfDocumentedException</code> and error
421      * type, error tag and error status code are compared to expected values.
422      */
423     @Test
424     public void prepareQnameNotValidListNameNegativeTest() {
425         final var ex = assertThrows(RestconfDocumentedException.class,
426             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
427                 "deserializer-test:list-no-key/disabled=false"));
428         assertEquals(ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
429         assertEquals(ErrorTag.DATA_MISSING, ex.getErrors().get(0).getErrorTag());
430     }
431
432     /**
433      * Negative test of getting next identifier when current node is keyed entry. Test is expected to
434      * fail with <code>RestconfDocumentedException</code>.
435      */
436     @Test
437     public void prepareIdentifierNotKeyedEntryNegativeTest() {
438         final var ex = assertThrows(RestconfDocumentedException.class,
439             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:list-one-key"));
440         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: missing-attribute, "
441             + "error-message: Could not parse Instance Identifier 'deserializer-test:list-one-key'. Offset: '30' : "
442             + "Reason: Entry '(deserializer:test?revision=2016-06-06)list-one-key' requires key or value predicate "
443             + "to be present.]]", ex.getMessage());
444         final var errors = ex.getErrors();
445         assertEquals(1, errors.size());
446         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
447         assertEquals(ErrorTag.MISSING_ATTRIBUTE, errors.get(0).getErrorTag());
448     }
449
450     /**
451      * Negative test when there is a comma also after the last key. Test is expected to fail with
452      * <code>RestconfDocumentedException</code>.
453      */
454     @Test
455     public void deserializeKeysEndsWithComaNegativeTest() {
456         final var ex = assertThrows(RestconfDocumentedException.class,
457             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
458             "deserializer-test:list-multiple-keys=value,100,false,"));
459         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: malformed-message, error-message: "
460             + "Could not parse Instance Identifier 'deserializer-test:list-multiple-keys=value,100,false,'. "
461             + "Offset: '52' : Reason: Identifier must start with '/'.]]", ex.getMessage());
462         final var errors = ex.getErrors();
463         assertEquals(1, errors.size());
464         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
465         assertEquals(ErrorTag.MALFORMED_MESSAGE, errors.get(0).getErrorTag());
466     }
467
468     /**
469      * Positive when not all keys of list are encoded. The missing keys should be considered to has empty
470      * <code>String</code> values. Also value of next leaf must not be considered to be missing key value.
471      */
472     @Test
473     public void notAllListKeysEncodedPositiveTest() {
474         final QName list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
475         final Map<QName, Object> values = ImmutableMap.of(
476             QName.create(list, "name"), ":foo",
477             QName.create(list, "number"), "",
478             QName.create(list, "enabled"), "");
479
480         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
481             "deserializer-test:list-multiple-keys=%3Afoo,,/string-value");
482         assertEquals(3, result.size());
483         // list
484         assertEquals(NodeIdentifier.create(list), result.get(0));
485         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
486         // leaf
487         assertEquals(new NodeIdentifier(QName.create("deserializer:test", "2016-06-06", "string-value")),
488             result.get(2));
489     }
490
491     /**
492      * Negative test when not all keys of list are encoded and it is not possible to consider missing keys to be empty.
493      * Test is expected to fail with <code>RestconfDocumentedException</code> and error type, error tag and error
494      * status code are compared to expected values.
495      */
496     @Test
497     public void notAllListKeysEncodedNegativeTest() {
498         final var ex = assertThrows(RestconfDocumentedException.class,
499             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
500                     "deserializer-test:list-multiple-keys=%3Afoo/string-value"));
501         assertEquals(ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
502         assertEquals(ErrorTag.MISSING_ATTRIBUTE, ex.getErrors().get(0).getErrorTag());
503     }
504
505     /**
506      * Test URI with list where key value starts with, ends with or contains percent encoded characters.The encoded
507      * value should be complete also with not percent-encoded parts.
508      */
509     @Test
510     public void percentEncodedKeyEndsWithNoPercentEncodedChars() {
511         final String URI = "deserializer-test:list-multiple-keys=%3Afoo,1,true";
512         final YangInstanceIdentifier result = YangInstanceIdentifier.create(
513                 YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, URI));
514
515         final Iterator<Entry<QName, Object>> resultListKeys =
516                 ((NodeIdentifierWithPredicates)result.getLastPathArgument()).entrySet().iterator();
517
518         assertEquals(":foo", resultListKeys.next().getValue());
519         assertEquals(Uint8.ONE, resultListKeys.next().getValue());
520         assertEquals(true, resultListKeys.next().getValue());
521     }
522
523     /**
524      * Positive test when all keys of list can be considered to be empty <code>String</code>.
525      */
526     @Test
527     public void deserializeAllKeysEmptyTest() {
528         final QName list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
529         final Map<QName, Object> values = ImmutableMap.of(
530             QName.create(list, "name"), "",
531             QName.create(list, "number"), "",
532             QName.create(list, "enabled"), "");
533
534         final var result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
535             "deserializer-test:list-multiple-keys=,,");
536         assertEquals(2, result.size());
537         assertEquals(NodeIdentifier.create(list), result.get(0));
538         assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
539     }
540
541     /**
542      * Negative test of deserialization when for leaf list there is no specified instance value.
543      * <code>RestconfDocumentedException</code> is expected and error type, error tag and error status code are
544      * compared to expected values.
545      */
546     @Test
547     public void leafListMissingKeyNegativeTest() {
548         final var ex = assertThrows(RestconfDocumentedException.class,
549             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT, "deserializer-test:leaf-list-0="));
550         assertEquals(ErrorType.PROTOCOL, ex.getErrors().get(0).getErrorType());
551         assertEquals(ErrorTag.MISSING_ATTRIBUTE, ex.getErrors().get(0).getErrorTag());
552     }
553
554     /**
555      * Positive test of deserialization when parts of input URI <code>String</code> are defined in another module.
556      */
557     @Test
558     public void deserializePartInOtherModuleTest() {
559         final List<PathArgument> result = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
560             "deserializer-test-included:augmented-list=100/augmented-leaf");
561
562         assertEquals(4, result.size());
563
564         final QName list = QName.create("deserializer:test:included", "2016-06-06", "augmented-list");
565         final QName child = QName.create("deserializer:test", "2016-06-06", "augmented-leaf");
566
567         // list
568         assertEquals(NodeIdentifier.create(list), result.get(0));
569         assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint16.valueOf(100)),
570             result.get(1));
571
572         // augmented leaf
573         assertEquals(new AugmentationIdentifier(Set.of(child)), result.get(2));
574         assertEquals(NodeIdentifier.create(child), result.get(3));
575     }
576
577     /**
578      * Deserialization of path that contains list entry with key which value is described by leaflef to identityref.
579      */
580     @Test
581     public void deserializePathWithIdentityrefKeyValueTest() {
582         final var pathArgs = YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
583             "deserializer-test-included:refs/list-with-identityref=deserializer-test%3Aderived-identity/foo");
584         assertEquals(4, pathArgs.size());
585
586         assertEquals("refs", pathArgs.get(0).getNodeType().getLocalName());
587         assertEquals("list-with-identityref", pathArgs.get(1).getNodeType().getLocalName());
588
589         final PathArgument listEntryArg = pathArgs.get(2);
590         assertThat(listEntryArg, instanceOf(NodeIdentifierWithPredicates.class));
591         assertEquals("list-with-identityref", listEntryArg.getNodeType().getLocalName());
592         final Set<QName> keys = ((NodeIdentifierWithPredicates) listEntryArg).keySet();
593         assertEquals(1, keys.size());
594         assertEquals("id", keys.iterator().next().getLocalName());
595         final Object keyValue = ((NodeIdentifierWithPredicates) listEntryArg).values().iterator().next();
596         assertEquals(QName.create("deserializer:test", "derived-identity", Revision.of("2016-06-06")), keyValue);
597
598         assertEquals("foo", pathArgs.get(3).getNodeType().getLocalName());
599     }
600
601     /**
602      * Identityref key value is not encoded correctly - ':' character must be encoded as '%3A'.
603      */
604     @Test
605     public void deserializePathWithInvalidIdentityrefKeyValueTest() {
606         final var ex = assertThrows(RestconfDocumentedException.class,
607             () -> YangInstanceIdentifierDeserializer.create(SCHEMA_CONTEXT,
608             "deserializer-test-included:refs/list-with-identityref=deserializer-test:derived-identity/foo"));
609         assertEquals("errors: [RestconfError [error-type: protocol, error-tag: bad-element, "
610             + "error-message: Cannot decode value 'deserializer-test' for identityref type in "
611             + "(deserializer:test:included?revision=2016-06-06)list-with-identityref. Make sure reserved characters "
612             + "such as comma, single-quote, double-quote, colon, double-quote, space, and forward slash (,'\":\" /) "
613             + "are percent-encoded, for example ':' is '%3A']]", ex.getMessage());
614         final var errors = ex.getErrors();
615         assertEquals(1, errors.size());
616         assertEquals(ErrorType.PROTOCOL, errors.get(0).getErrorType());
617         assertEquals(ErrorTag.BAD_ELEMENT, errors.get(0).getErrorTag());
618     }
619 }