/*
* Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
* Copyright (c) 2021 PANTHEON.tech, s.r.o.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.restconf.server.spi;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.collect.ImmutableMap;
import java.text.ParseException;
import org.junit.jupiter.api.Test;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.api.ErrorMessage;
import org.opendaylight.restconf.server.api.DatabindContext;
import org.opendaylight.restconf.server.api.DatabindPath.Action;
import org.opendaylight.restconf.server.api.DatabindPath.Data;
import org.opendaylight.restconf.server.api.ServerError;
import org.opendaylight.restconf.server.api.ServerErrorInfo;
import org.opendaylight.restconf.server.api.ServerException;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.common.Revision;
import org.opendaylight.yangtools.yang.common.Uint16;
import org.opendaylight.yangtools.yang.common.Uint8;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
/**
* Unit tests for {@link ApiPathNormalizer}.
*/
class ApiPathNormalizerTest {
private static final QName ACTIONS_INTERFACES =
QName.create("https://example.com/ns/example-actions", "2016-07-07", "interfaces");
private static final ApiPathNormalizer NORMALIZER = new ApiPathNormalizer(DatabindContext.ofModel(
YangParserTestUtils.parseYangResourceDirectory("/restconf/parser/deserializer")));
/**
* Test of deserialization String
URI with container to
* {@code Iterable}.
*/
@Test
void deserializeContainerTest() {
final var result = assertNormalizedPath("deserializer-test:contA").getPathArguments();
assertEquals(1, result.size());
assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
}
/**
* Test of deserialization String
URI with container containing leaf to
* {@code Iterable}.
*/
@Test
void deserializeContainerWithLeafTest() {
final var result = assertNormalizedPath("deserializer-test:contA/leaf-A").getPathArguments();
assertEquals(2, result.size());
assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "leaf-A")), result.get(1));
}
/**
* Test of deserialization String
URI with container containing list with leaf list to
* {@code Iterable}.
*/
@Test
void deserializeContainerWithListWithLeafListTest() {
final var result = assertNormalizedPath("deserializer-test:contA/list-A=100/leaf-list-AA=instance")
.getPathArguments();
assertEquals(5, result.size());
// container
assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "contA")), result.get(0));
// list
final var list = QName.create("deserializer:test", "2016-06-06", "list-A");
assertEquals(NodeIdentifier.create(list), result.get(1));
assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint8.valueOf(100)),
result.get(2));
// leaf list
final var leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-AA");
assertEquals(NodeIdentifier.create(leafList), result.get(3));
assertEquals(new NodeWithValue<>(leafList, "instance"), result.get(4));
}
/**
* Test of deserialization String
URI with container containing list with Action to
* {@code Iterable}.
*/
@Test
void deserializeContainerWithListWithActionTest() {
final var result = assertNormalizedAction("example-actions:interfaces/interface=eth0/reset");
final var list = QName.create(ACTIONS_INTERFACES, "interface");
assertEquals(YangInstanceIdentifier.builder()
// container
.node(ACTIONS_INTERFACES)
// list
.node(list)
.nodeWithKey(list, QName.create(list, "name"), "eth0")
.build(), result.instance());
assertEquals(QName.create(ACTIONS_INTERFACES, "reset"), result.action().argument());
}
/**
* Test of deserialization String
URI with container containing choice node with Action to
* {@code Iterable}.
*/
@Test
void deserializeContainerWithChoiceSchemaNodeWithActionTest() {
final var result = assertNormalizedAction("example-actions:interfaces/typeA-gigabyte/interface=eth0/reboot");
final var list = QName.create(ACTIONS_INTERFACES, "interface");
assertEquals(YangInstanceIdentifier.builder()
// container
.node(ACTIONS_INTERFACES)
// choice
.node(QName.create(ACTIONS_INTERFACES, "interface-type"))
// container
.node(QName.create(ACTIONS_INTERFACES, "typeA-gigabyte"))
// list
.node(list)
.nodeWithKey(list, QName.create(list, "name"), "eth0")
.build(), result.instance());
assertEquals(QName.create(ACTIONS_INTERFACES, "reboot"), result.action().argument());
}
/**
* Test of deserialization String
URI with container containing choice node with Action to
* {@code Iterable}.
*/
@Test
void deserializeContainerWithChoiceCaseSchemaNodeWithActionTest() {
final var result = assertNormalizedAction("example-actions:interfaces/udp/reboot");
assertEquals(YangInstanceIdentifier.of(
// container
ACTIONS_INTERFACES,
// choice
QName.create(ACTIONS_INTERFACES, "protocol"),
// choice container
QName.create(ACTIONS_INTERFACES, "udp")),
result.instance());
}
/**
* Test of deserialization String
URI containing list with no keys to
* {@code Iterable}.
*/
@Test
void deserializeListWithNoKeysTest() {
final var result = assertNormalizedPath("deserializer-test:list-no-key").getPathArguments();
assertEquals(2, result.size());
final var list = QName.create("deserializer:test", "2016-06-06", "list-no-key");
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifier.create(list), result.get(1));
}
/**
* Test of deserialization String
URI containing list with one key to
* {@code Iterable}.
*/
@Test
void deserializeListWithOneKeyTest() {
final var result = assertNormalizedPath("deserializer-test:list-one-key=value").getPathArguments();
assertEquals(2, result.size());
final QName list = QName.create("deserializer:test", "2016-06-06", "list-one-key");
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "name"), "value"), result.get(1));
}
/**
* Test of deserialization String
URI containing list with multiple keys to
* {@code Iterable}.
*/
@Test
void deserializeListWithMultipleKeysTest() {
final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
final var values = ImmutableMap.of(
QName.create(list, "name"), "value",
QName.create(list, "number"), Uint8.valueOf(100),
QName.create(list, "enabled"), false);
final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=value,100,false")
.getPathArguments();
assertEquals(2, result.size());
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
}
/**
* Test of deserialization String
URI containing leaf list to
* {@code Iterable}.
*/
@Test
void deserializeLeafListTest() {
final var result = assertNormalizedPath("deserializer-test:leaf-list-0=true").getPathArguments();
assertEquals(2, result.size());
final QName leafList = QName.create("deserializer:test", "2016-06-06", "leaf-list-0");
assertEquals(new NodeIdentifier(leafList), result.get(0));
assertEquals(new NodeWithValue<>(leafList, true), result.get(1));
}
/**
* Test when empty String
is supplied as an input. Test is expected to return empty result.
*/
@Test
void deserializeEmptyDataTest() {
assertEquals(YangInstanceIdentifier.of(), assertNormalizedPath(""));
}
/**
* Negative test when supplied String
data to deserialize is null.
*/
@Test
void nullDataNegativeNegativeTest() {
assertThrows(NullPointerException.class, () -> assertNormalizedPath(null));
}
/**
* Negative test of creating QName
when it is not possible to find module for specified prefix. Test is
* expected to fail with RestconfDocumentedException
.
*/
@Test
void prepareQnameNotExistingPrefixNegativeTest() {
final var error = assertErrorPath("not-existing:contA");
assertEquals(new ErrorMessage("Failed to lookup for module with name 'not-existing'."), error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.tag());
}
/**
* Negative test of creating QName
when after identifier and colon there is node name of unknown
* node in current container. Test is expected to fail with RestconfDocumentedException
and error
* type, error tag and error status code are compared to expected values.
*/
@Test
public void prepareQnameNotValidContainerNameNegativeTest() {
final var error = assertErrorPath("deserializer-test:contA/leafB");
assertEquals(new ErrorMessage("Schema for '(deserializer:test?revision=2016-06-06)leafB' not found"),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.DATA_MISSING, error.tag());
}
/**
* Negative test of creating QName
when after identifier and equals there is node name of unknown
* node in current list. Test is expected to fail with RestconfDocumentedException
and error
* type, error tag and error status code are compared to expected values.
*/
@Test
void prepareQnameNotValidListNameNegativeTest() {
final var error = assertErrorPath("deserializer-test:list-no-key/disabled=false");
assertEquals(new ErrorMessage("Schema for '(deserializer:test?revision=2016-06-06)disabled' not found"),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.DATA_MISSING, error.tag());
}
/**
* Negative test of getting next identifier when current node is keyed entry. Test is expected to
* fail with RestconfDocumentedException
.
*/
@Test
void prepareIdentifierNotKeyedEntryNegativeTest() {
final var error = assertErrorPath("deserializer-test:list-one-key");
assertEquals(new ErrorMessage("""
Entry '(deserializer:test?revision=2016-06-06)list-one-key' requires key or value predicate to be \
present."""), error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.MISSING_ATTRIBUTE, error.tag());
}
/**
* Negative test when there is a comma also after the last key. Test is expected to fail with
* RestconfDocumentedException
. Last comma indicates a fourth key, which is a mismatch with schema.
*/
@Test
void deserializeKeysEndsWithCommaTooManyNegativeTest() {
final var error = assertErrorPath("deserializer-test:list-multiple-keys=value,100,false,");
assertEquals(new ErrorMessage("""
Schema for (deserializer:test?revision=2016-06-06)list-multiple-keys requires 3 key values, 4 supplied"""),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.UNKNOWN_ATTRIBUTE, error.tag());
}
/**
* Negative test when there is a comma also after the last key. Test is expected to fail with
* RestconfDocumentedException
. Last comma indicates a third key, whose is a mismatch with schema.
*/
@Test
void deserializeKeysEndsWithCommaIllegalNegativeTest() {
final var error = assertErrorPath("deserializer-test:list-multiple-keys=value,100,");
assertEquals(new ErrorMessage("Invalid value '' for (deserializer:test?revision=2016-06-06)enabled"),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.INVALID_VALUE, error.tag());
assertEquals(new ServerErrorInfo("Invalid value '' for boolean type. Allowed values are 'true' and 'false'"),
error.info());
}
/**
* Positive when not all keys of list are encoded. The missing keys should be considered to has empty
* String
values. Also value of next leaf must not be considered to be missing key value.
*/
@Test
void notAllListKeysEncodedPositiveTest() {
final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
final var values = ImmutableMap.of(
QName.create(list, "name"), ":foo",
QName.create(list, "number"), Uint8.ONE,
QName.create(list, "enabled"), false);
final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=%3Afoo,1,false/string-value")
.getPathArguments();
assertEquals(3, result.size());
// list
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
// leaf
assertEquals(new NodeIdentifier(QName.create("deserializer:test", "2016-06-06", "string-value")),
result.get(2));
}
/**
* Negative test when not all keys of list are encoded and it is not possible to consider missing keys to be empty.
* Test is expected to fail with RestconfDocumentedException
and error type, error tag and error
* status code are compared to expected values.
*/
@Test
void notAllListKeysEncodedNegativeTest() {
final var error = assertErrorPath("deserializer-test:list-multiple-keys=%3Afoo/string-value");
assertEquals(new ErrorMessage("""
Schema for (deserializer:test?revision=2016-06-06)list-multiple-keys requires 3 key values, 1 supplied"""),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.MISSING_ATTRIBUTE, error.tag());
}
/**
* Test URI with list where key value starts with, ends with or contains percent encoded characters.The encoded
* value should be complete also with not percent-encoded parts.
*/
@Test
void percentEncodedKeyEndsWithNoPercentEncodedChars() {
final var URI = "deserializer-test:list-multiple-keys=%3Afoo,1,true";
final var result = assertNormalizedPath(URI);
final var resultListKeys = assertInstanceOf(NodeIdentifierWithPredicates.class, result.getLastPathArgument())
.entrySet().iterator();
assertEquals(":foo", resultListKeys.next().getValue());
assertEquals(Uint8.ONE, resultListKeys.next().getValue());
assertEquals(true, resultListKeys.next().getValue());
}
/**
* Positive test when all keys of list can be considered to be empty String
.
*/
@Test
void deserializeAllKeysEmptyTest() {
final var list = QName.create("deserializer:test", "2016-06-06", "list-multiple-keys");
final var values = ImmutableMap.of(
QName.create(list, "name"), "",
QName.create(list, "number"), Uint8.ZERO,
QName.create(list, "enabled"), true);
final var result = assertNormalizedPath("deserializer-test:list-multiple-keys=,0,true").getPathArguments();
assertEquals(2, result.size());
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, values), result.get(1));
}
/**
* Negative test of deserialization when for leaf list there is no specified instance value.
* RestconfDocumentedException
is expected and error type, error tag and error status code are
* compared to expected values.
*/
@Test
void leafListMissingKeyNegativeTest() {
final var error = assertErrorPath("deserializer-test:leaf-list-0=");
assertEquals(new ErrorMessage("Invalid value '' for (deserializer:test?revision=2016-06-06)leaf-list-0"),
error.message());
assertEquals(ErrorType.PROTOCOL, error.type());
assertEquals(ErrorTag.INVALID_VALUE, error.tag());
}
/**
* Positive test of deserialization when parts of input URI String
are defined in another module.
*/
@Test
void deserializePartInOtherModuleTest() {
final var result = assertNormalizedPath(
"deserializer-test-included:augmented-list=100/deserializer-test:augmented-leaf").getPathArguments();
assertEquals(3, result.size());
// list
final var list = QName.create("deserializer:test:included", "2016-06-06", "augmented-list");
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint16.valueOf(100)),
result.get(1));
// augmented leaf
assertEquals(NodeIdentifier.create(QName.create("deserializer:test", "2016-06-06", "augmented-leaf")),
result.get(2));
}
@Test
void deserializeListInOtherModuleTest() {
final var result = assertNormalizedPath(
"deserializer-test-included:augmented-list=100/deserializer-test:augmenting-list=0").getPathArguments();
assertEquals(4, result.size());
// list
final var list = QName.create("deserializer:test:included", "2016-06-06", "augmented-list");
assertEquals(NodeIdentifier.create(list), result.get(0));
assertEquals(NodeIdentifierWithPredicates.of(list, QName.create(list, "list-key"), Uint16.valueOf(100)),
result.get(1));
// augmented list
final var augList = QName.create("deserializer:test", "2016-06-06", "augmenting-list");
assertEquals(NodeIdentifier.create(augList), result.get(2));
assertEquals(NodeIdentifierWithPredicates.of(augList, QName.create(augList, "id"), 0), result.get(3));
}
/**
* Deserialization of path that contains list entry with key which value is described by leaflef to identityref.
*/
@Test
void deserializePathWithIdentityrefKeyValueTest() {
assertIdentityrefKeyValue(
"deserializer-test-included:refs/list-with-identityref=deserializer-test%3Aderived-identity/foo");
}
/**
* Identityref key value is not encoded correctly - ':' character must be encoded as '%3A'.
*/
@Test
void deserializePathWithInvalidIdentityrefKeyValueTest() {
assertIdentityrefKeyValue(
"deserializer-test-included:refs/list-with-identityref=deserializer-test:derived-identity/foo");
}
private static ServerError assertErrorPath(final String path) {
final var apiPath = assertApiPath(path);
return assertThrows(ServerException.class, () -> NORMALIZER.normalizePath(apiPath)).error();
}
private static Action assertNormalizedAction(final String path) {
try {
return assertInstanceOf(Action.class, NORMALIZER.normalizePath(assertApiPath(path)));
} catch (ServerException e) {
throw new AssertionError(e);
}
}
private static YangInstanceIdentifier assertNormalizedPath(final String path) {
try {
return assertInstanceOf(Data.class, NORMALIZER.normalizePath(assertApiPath(path))).instance();
} catch (ServerException e) {
throw new AssertionError(e);
}
}
private static ApiPath assertApiPath(final String path) {
try {
return ApiPath.parse(path);
} catch (ParseException e) {
throw new AssertionError(e);
}
}
private static void assertIdentityrefKeyValue(final String path) {
final var pathArgs = assertNormalizedPath(path).getPathArguments();
assertEquals(4, pathArgs.size());
assertEquals("refs", pathArgs.get(0).getNodeType().getLocalName());
assertEquals("list-with-identityref", pathArgs.get(1).getNodeType().getLocalName());
final var listEntryArg = assertInstanceOf(NodeIdentifierWithPredicates.class, pathArgs.get(2));
assertEquals("list-with-identityref", listEntryArg.getNodeType().getLocalName());
final var keys = listEntryArg.keySet();
assertEquals(1, keys.size());
assertEquals("id", keys.iterator().next().getLocalName());
final var keyValue = listEntryArg.values().iterator().next();
assertEquals(QName.create("deserializer:test", "derived-identity", Revision.of("2016-06-06")), keyValue);
assertEquals("foo", pathArgs.get(3).getNodeType().getLocalName());
}
}