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