From: Ivan Hrasko Date: Thu, 6 Oct 2016 09:19:59 +0000 (+0200) Subject: Bug 6895 - Implement Query parameters - depth X-Git-Tag: release/carbon~133^2 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=c03c0893f46c57db090a0b88556715b19fefd1ec;p=netconf.git Bug 6895 - Implement Query parameters - depth - depth URI query parameter for new Restconf - parse and verify all GET parameters in one method - added tests for depth parameter writer - added unit tests for content parameter Change-Id: If9fece9c995dd52bb8ad00d29b6addb66fa4e32b Signed-off-by: Ivan Hrasko --- diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/WriterParameters.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/WriterParameters.java index 11227d0a04..07978083dd 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/WriterParameters.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/netconf/sal/restconf/impl/WriterParameters.java @@ -11,12 +11,18 @@ package org.opendaylight.netconf.sal.restconf.impl; import com.google.common.base.Optional; public class WriterParameters { + private final String content; private final Optional depth; private final boolean prettyPrint; private WriterParameters(final WriterParametersBuilder builder) { - this.prettyPrint = builder.prettyPrint; + this.content = builder.content; this.depth = builder.depth; + this.prettyPrint = builder.prettyPrint; + } + + public String getContent() { + return content; } public Optional getDepth() { @@ -28,14 +34,15 @@ public class WriterParameters { } public static class WriterParametersBuilder { + private String content; private Optional depth = Optional.absent(); private boolean prettyPrint; - public WriterParametersBuilder() { - } + public WriterParametersBuilder() {} - public Optional getDepth() { - return depth; + public WriterParametersBuilder setContent(final String content) { + this.content = content; + return this; } public WriterParametersBuilder setDepth(final int depth) { @@ -43,10 +50,6 @@ public class WriterParameters { return this; } - public boolean isPrettyPrint() { - return prettyPrint; - } - public WriterParametersBuilder setPrettyPrint(final boolean prettyPrint) { this.prettyPrint = prettyPrint; return this; @@ -57,4 +60,3 @@ public class WriterParameters { } } } - diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImpl.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImpl.java index cb17434392..e75e78371b 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImpl.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImpl.java @@ -24,6 +24,7 @@ import org.opendaylight.netconf.sal.restconf.impl.PATCHContext; import org.opendaylight.netconf.sal.restconf.impl.PATCHStatusContext; import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; import org.opendaylight.netconf.sal.restconf.impl.RestconfError; +import org.opendaylight.netconf.sal.restconf.impl.WriterParameters; import org.opendaylight.restconf.RestConnectorProvider; import org.opendaylight.restconf.common.references.SchemaContextRef; import org.opendaylight.restconf.handlers.DOMMountPointServiceHandler; @@ -65,12 +66,13 @@ public class RestconfDataServiceImpl implements RestconfDataService { @Override public Response readData(final String identifier, final UriInfo uriInfo) { Preconditions.checkNotNull(identifier); + + final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters(uriInfo); final SchemaContextRef schemaContextRef = new SchemaContextRef(this.schemaContextHandler.get()); final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier( identifier, schemaContextRef.get(), Optional.of(this.mountPointServiceHandler.get())); final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint(); - final String value = uriInfo.getQueryParameters().getFirst(RestconfDataServiceConstant.CONTENT); final DOMTransactionChain transactionChain; if (mountPoint == null) { @@ -79,9 +81,9 @@ public class RestconfDataServiceImpl implements RestconfDataService { transactionChain = transactionChainOfMountPoint(mountPoint); } - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(instanceIdentifier, mountPoint, - transactionChain); - final NormalizedNode node = ReadDataTransactionUtil.readData(value, transactionNode); + final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper( + instanceIdentifier, mountPoint, transactionChain); + final NormalizedNode node = ReadDataTransactionUtil.readData(parameters.getContent(), transactionNode); if (node == null) { throw new RestconfDocumentedException( "Request could not be completed because the relevant data model content does not exist", @@ -94,12 +96,19 @@ public class RestconfDataServiceImpl implements RestconfDataService { + node.getNodeType().getLocalName() + '"'; final Response resp; - if ((value == null) || value.contains(RestconfDataServiceConstant.ReadData.CONFIG)) { - resp = Response.status(200).entity(new NormalizedNodeContext(instanceIdentifier, node)).header("ETag", etag) - .header("Last-Modified", dateFormatGmt.format(new Date())).build(); + if ((parameters.getContent().equals(RestconfDataServiceConstant.ReadData.ALL)) + || parameters.getContent().equals(RestconfDataServiceConstant.ReadData.CONFIG)) { + resp = Response.status(200) + .entity(new NormalizedNodeContext(instanceIdentifier, node, parameters)) + .header("ETag", etag) + .header("Last-Modified", dateFormatGmt.format(new Date())) + .build(); } else { - resp = Response.status(200).entity(new NormalizedNodeContext(instanceIdentifier, node)).build(); + resp = Response.status(200) + .entity(new NormalizedNodeContext(instanceIdentifier, node, parameters)) + .build(); } + return resp; } diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ParametersUtil.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ParametersUtil.java new file mode 100644 index 0000000000..bd85e54e48 --- /dev/null +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ParametersUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * 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.restful.utils; + +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType; + +class ParametersUtil { + + private ParametersUtil() { + throw new UnsupportedOperationException("Util class."); + } + + /** + * Check if URI does not contain not allowed parameters for specified operation + * + * @param operationType + * - type of operation (READ, POST, PUT, DELETE...) + * @param usedParameters + * - parameters used in URI request + * @param allowedParameters + * - allowed parameters for operation + */ + static void checkParametersTypes(@Nonnull final String operationType, + @Nonnull final Set usedParameters, + @Nonnull final String... allowedParameters) { + final Set notAllowedParameters = Sets.newHashSet(usedParameters); + notAllowedParameters.removeAll(Sets.newHashSet(allowedParameters)); + + if (!notAllowedParameters.isEmpty()) { + throw new RestconfDocumentedException( + "Not allowed parameters for " + operationType + " operation: " + notAllowedParameters, + RestconfError.ErrorType.PROTOCOL, + RestconfError.ErrorTag.INVALID_VALUE); + } + } + + /** + * Check if URI does not contain value for the same parameter more than once + * + * @param parameterValues + * - URI parameter values + * @param parameterName + * - URI parameter name + */ + static void checkParameterCount(@Nonnull final List parameterValues, @Nonnull final String parameterName) { + if (parameterValues.size() > 1) { + throw new RestconfDocumentedException( + "Parameter " + parameterName + " can appear at most once in request URI", + ErrorType.PROTOCOL, + ErrorTag.INVALID_VALUE); + } + } +} diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtil.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtil.java index fe840990ed..cf4ced08b1 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtil.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtil.java @@ -8,18 +8,23 @@ package org.opendaylight.restconf.restful.utils; import com.google.common.base.Optional; +import com.google.common.primitives.Ints; import com.google.common.util.concurrent.CheckedFuture; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.ws.rs.core.UriInfo; import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException; import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; -import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag; -import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError; +import org.opendaylight.netconf.sal.restconf.impl.WriterParameters; +import org.opendaylight.netconf.sal.restconf.impl.WriterParameters.WriterParametersBuilder; import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper; import org.opendaylight.yangtools.yang.common.QNameModule; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; @@ -56,6 +61,72 @@ public final class ReadDataTransactionUtil { throw new UnsupportedOperationException("Util class."); } + /** + * Parse parameters from URI request and check their types and values. + * + * @param uriInfo + * - URI info + * @return {@link WriterParameters} + */ + @Nonnull public static WriterParameters parseUriParameters(@Nullable final UriInfo uriInfo) { + final WriterParametersBuilder builder = new WriterParametersBuilder(); + + if (uriInfo == null) { + return builder.build(); + } + + // check only allowed parameters + ParametersUtil.checkParametersTypes( + RestconfDataServiceConstant.ReadData.READ_TYPE_TX, + uriInfo.getQueryParameters().keySet(), + RestconfDataServiceConstant.ReadData.CONTENT, + RestconfDataServiceConstant.ReadData.DEPTH); + + // read parameters from URI or set default values + final List content = uriInfo.getQueryParameters().getOrDefault( + RestconfDataServiceConstant.ReadData.CONTENT, + Collections.singletonList(RestconfDataServiceConstant.ReadData.ALL)); + final List depth = uriInfo.getQueryParameters().getOrDefault( + RestconfDataServiceConstant.ReadData.DEPTH, + Collections.singletonList(RestconfDataServiceConstant.ReadData.UNBOUNDED)); + + // parameter can be in URI at most once + ParametersUtil.checkParameterCount(content, RestconfDataServiceConstant.ReadData.CONTENT); + ParametersUtil.checkParameterCount(depth, RestconfDataServiceConstant.ReadData.DEPTH); + + // check and set content + final String contentValue = content.get(0); + if (!contentValue.equals(RestconfDataServiceConstant.ReadData.ALL)) { + if (!contentValue.equals(RestconfDataServiceConstant.ReadData.CONFIG) + && !contentValue.equals(RestconfDataServiceConstant.ReadData.NONCONFIG)) { + throw new RestconfDocumentedException( + new RestconfError(RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.INVALID_VALUE, + "Invalid content parameter: " + contentValue, null, + "The content parameter value must be either config, nonconfig or all (default)")); + } + } + + builder.setContent(content.get(0)); + + // check and set depth + if (!depth.get(0).equals(RestconfDataServiceConstant.ReadData.UNBOUNDED)) { + final Integer value = Ints.tryParse(depth.get(0)); + + if (value == null + || (!(value >= RestconfDataServiceConstant.ReadData.MIN_DEPTH + && value <= RestconfDataServiceConstant.ReadData.MAX_DEPTH))) { + throw new RestconfDocumentedException( + new RestconfError(RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.INVALID_VALUE, + "Invalid depth parameter: " + depth, null, + "The depth parameter must be an integer between 1 and 65535 or \"unbounded\"")); + } else { + builder.setDepth(value); + } + } + + return builder.build(); + } + /** * Read specific type of data from data store via transaction. * @@ -65,31 +136,26 @@ public final class ReadDataTransactionUtil { * - {@link TransactionVarsWrapper} - wrapper for variables * @return {@link NormalizedNode} */ - public static @Nullable NormalizedNode readData(@Nullable final String valueOfContent, + public static @Nullable NormalizedNode readData(@Nonnull final String valueOfContent, @Nonnull final TransactionVarsWrapper transactionNode) { - final NormalizedNode data; - if (valueOfContent != null) { - switch (valueOfContent) { - case RestconfDataServiceConstant.ReadData.CONFIG: - transactionNode.setLogicalDatastoreType(LogicalDatastoreType.CONFIGURATION); - data = readDataViaTransaction(transactionNode); - break; - case RestconfDataServiceConstant.ReadData.NONCONFIG: - transactionNode.setLogicalDatastoreType(LogicalDatastoreType.OPERATIONAL); - data = readDataViaTransaction(transactionNode); - break; - case RestconfDataServiceConstant.ReadData.ALL: - data = readAllData(transactionNode); - break; - default: - throw new RestconfDocumentedException("Bad query parameter for content.", ErrorType.APPLICATION, - ErrorTag.INVALID_VALUE); - } - } else { - data = readAllData(transactionNode); + switch (valueOfContent) { + case RestconfDataServiceConstant.ReadData.CONFIG: + transactionNode.setLogicalDatastoreType(LogicalDatastoreType.CONFIGURATION); + return readDataViaTransaction(transactionNode); + + case RestconfDataServiceConstant.ReadData.NONCONFIG: + transactionNode.setLogicalDatastoreType(LogicalDatastoreType.OPERATIONAL); + return readDataViaTransaction(transactionNode); + + case RestconfDataServiceConstant.ReadData.ALL: + return readAllData(transactionNode); + + default: + throw new RestconfDocumentedException( + new RestconfError(RestconfError.ErrorType.PROTOCOL, RestconfError.ErrorTag.INVALID_VALUE, + "Invalid content parameter: " + valueOfContent, null, + "The content parameter value must be either config, nonconfig or all (default)")); } - - return data; } /** diff --git a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/RestconfDataServiceConstant.java b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/RestconfDataServiceConstant.java index bc9f4d6463..fd43a4a834 100644 --- a/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/RestconfDataServiceConstant.java +++ b/restconf/sal-rest-connector/src/main/java/org/opendaylight/restconf/restful/utils/RestconfDataServiceConstant.java @@ -21,7 +21,6 @@ import org.opendaylight.yangtools.yang.common.QNameModule; */ public final class RestconfDataServiceConstant { - public static final String CONTENT = "content"; public static final QName NETCONF_BASE_QNAME; static { try { @@ -43,10 +42,20 @@ public final class RestconfDataServiceConstant { * */ public final class ReadData { + // URI parameters + public static final String CONTENT = "content"; + public static final String DEPTH = "depth"; + // content values public static final String CONFIG = "config"; - public static final String NONCONFIG = "nonconfig"; public static final String ALL = "all"; + public static final String NONCONFIG = "nonconfig"; + + // depth values + public static final String UNBOUNDED = "unbounded"; + public static final int MIN_DEPTH = 1; + public static final int MAX_DEPTH = 65535; + public static final String READ_TYPE_TX = "READ"; private ReadData() { diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriterTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriterTest.java new file mode 100644 index 0000000000..ba8aee833e --- /dev/null +++ b/restconf/sal-rest-connector/src/test/java/org/opendaylight/netconf/sal/rest/impl/DepthAwareNormalizedNodeWriterTest.java @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * 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.netconf.sal.rest.impl; + +import com.google.common.base.Optional; +import com.google.common.collect.Sets; +import java.util.Collection; +import java.util.Collections; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.opendaylight.yangtools.yang.common.QName; +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.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter; + +public class DepthAwareNormalizedNodeWriterTest { + + @Mock + private NormalizedNodeStreamWriter writer; + @Mock + private ContainerNode containerNodeData; + @Mock + private MapNode mapNodeData; + @Mock + private MapEntryNode mapEntryNodeData; + @Mock + private LeafSetNode leafSetNodeData; + @Mock + private LeafSetEntryNode leafSetEntryNodeData; + @Mock + private LeafNode keyLeafNodeData; + @Mock + private LeafNode anotherLeafNodeData; + + private NodeIdentifier containerNodeIdentifier; + private NodeIdentifier mapNodeIdentifier; + private NodeIdentifierWithPredicates mapEntryNodeIdentifier; + private NodeIdentifier leafSetNodeIdentifier; + private NodeWithValue leafSetEntryNodeIdentifier; + private NodeIdentifier keyLeafNodeIdentifier; + private NodeIdentifier anotherLeafNodeIdentifier; + + private Collection> containerNodeValue; + private Collection mapNodeValue; + private Collection> mapEntryNodeValue; + private Collection> leafSetNodeValue; + private String leafSetEntryNodeValue; + private String keyLeafNodeValue; + private String anotherLeafNodeValue; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + // identifiers + containerNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "container")); + Mockito.when(containerNodeData.getIdentifier()).thenReturn(containerNodeIdentifier); + + mapNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "list")); + Mockito.when(mapNodeData.getIdentifier()).thenReturn(mapNodeIdentifier); + + final QName leafSetEntryNodeQName = QName.create("namespace", "leaf-set-entry"); + leafSetEntryNodeValue = "leaf-set-value"; + leafSetEntryNodeIdentifier = new NodeWithValue<>(leafSetEntryNodeQName, leafSetEntryNodeValue); + Mockito.when(leafSetEntryNodeData.getIdentifier()).thenReturn(leafSetEntryNodeIdentifier); + Mockito.when(leafSetEntryNodeData.getNodeType()).thenReturn(leafSetEntryNodeQName); + + leafSetNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "leaf-set")); + Mockito.when(leafSetNodeData.getIdentifier()).thenReturn(leafSetNodeIdentifier); + + final QName mapEntryNodeKey = QName.create("namespace", "key-field"); + keyLeafNodeIdentifier = NodeIdentifier.create(mapEntryNodeKey); + keyLeafNodeValue = "key-value"; + + mapEntryNodeIdentifier = new YangInstanceIdentifier.NodeIdentifierWithPredicates( + QName.create("namespace", "list-entry"), Collections.singletonMap(mapEntryNodeKey, keyLeafNodeValue)); + Mockito.when(mapEntryNodeData.getIdentifier()).thenReturn(mapEntryNodeIdentifier); + Mockito.when(mapEntryNodeData.getChild(keyLeafNodeIdentifier)).thenReturn(Optional.of(keyLeafNodeData)); + + Mockito.when(keyLeafNodeData.getValue()).thenReturn(keyLeafNodeValue); + Mockito.when(keyLeafNodeData.getIdentifier()).thenReturn(keyLeafNodeIdentifier); + + anotherLeafNodeIdentifier = NodeIdentifier.create(QName.create("namespace", "another-field")); + anotherLeafNodeValue = "another-value"; + + Mockito.when(anotherLeafNodeData.getValue()).thenReturn(anotherLeafNodeValue); + Mockito.when(anotherLeafNodeData.getIdentifier()).thenReturn(anotherLeafNodeIdentifier); + + // values + Mockito.when(leafSetEntryNodeData.getValue()).thenReturn(leafSetEntryNodeValue); + + leafSetNodeValue = Collections.singletonList(leafSetEntryNodeData); + Mockito.when(leafSetNodeData.getValue()).thenReturn(leafSetNodeValue); + + containerNodeValue = Collections.singleton(leafSetNodeData); + Mockito.when(containerNodeData.getValue()).thenReturn(containerNodeValue); + + mapEntryNodeValue = Sets.newHashSet(keyLeafNodeData, anotherLeafNodeData); + Mockito.when(mapEntryNodeData.getValue()).thenReturn(mapEntryNodeValue); + + mapNodeValue = Collections.singleton(mapEntryNodeData); + Mockito.when(mapNodeData.getValue()).thenReturn(mapNodeValue); + } + + /** + * Test write {@link ContainerNode} with children but write data only to depth 1 (children will not be written). + */ + @Test + public void writeContainerWithoutChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter(writer, 1); + + depthWriter.write(containerNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write {@link ContainerNode} with children and write also all its children. + */ + @Test + public void writeContainerWithChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, Integer.MAX_VALUE); + + depthWriter.write(containerNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startContainerNode(containerNodeIdentifier, containerNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode( + leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue); + inOrder.verify(writer, Mockito.times(2)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link MapNode} with children but write data only to depth 1 (children will not be written). + */ + @Test + public void writeMapNodeWithoutChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter(writer, 1); + + depthWriter.write(mapNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + inOrder.verify(writer, Mockito.times(2)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write {@link MapNode} with children and write also all its children. + * + * FIXME + * Although ordered writer is used leaves are not written in expected order. + * + */ + @Ignore + @Test + public void writeMapNodeWithChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, Integer.MAX_VALUE); + + depthWriter.write(mapNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startMapNode(mapNodeIdentifier, mapNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + // FIXME this assertion is not working because leaves are not written in expected order + inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue); + inOrder.verify(writer, Mockito.times(2)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link LeafSetNode} with depth 1 (children will not be written). + */ + @Test + public void writeLeafSetNodeWithoutChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, 1); + + depthWriter.write(leafSetNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link LeafSetNode} when all its children will be written. + */ + @Test + public void writeLeafSetNodeWithChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, Integer.MAX_VALUE); + + depthWriter.write(leafSetNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startLeafSet(leafSetNodeIdentifier, leafSetNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode( + leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link LeafSetEntryNode}. + */ + @Test + public void writeLeafSetEntryNodeTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, Integer.MAX_VALUE); + + depthWriter.write(leafSetEntryNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).leafSetEntryNode( + leafSetEntryNodeIdentifier.getNodeType(), leafSetEntryNodeValue); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link MapEntryNode} unordered to depth 1 to write only keys. + */ + @Test + public void writeMapEntryNodeUnorderedOnlyKeysTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, false, 1); + + depthWriter.write(mapEntryNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + // write only the key + inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link MapEntryNode} unordered with full depth. + */ + @Test + public void writeMapEntryNodeUnorderedTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, false, Integer.MAX_VALUE); + + depthWriter.write(mapEntryNodeData); + + // unordered + Mockito.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + Mockito.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + Mockito.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue); + Mockito.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link MapEntryNode} ordered with depth 1 (children will not be written). + */ + @Test + public void writeMapEntryNodeOrderedWithoutChildrenTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, true, 1); + + depthWriter.write(mapEntryNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + inOrder.verify(writer, Mockito.times(1)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } + + /** + * Test write with {@link MapEntryNode} ordered and write also all its children. + * + * FIXME + * Although ordered writer is used leaves are not written in expected order. + * + */ + @Ignore + @Test + public void writeMapEntryNodeOrderedTest() throws Exception { + final DepthAwareNormalizedNodeWriter depthWriter = DepthAwareNormalizedNodeWriter.forStreamWriter( + writer, true, Integer.MAX_VALUE); + + depthWriter.write(mapEntryNodeData); + + final InOrder inOrder = Mockito.inOrder(writer); + inOrder.verify(writer, Mockito.times(1)).startMapEntryNode(mapEntryNodeIdentifier, mapEntryNodeValue.size()); + inOrder.verify(writer, Mockito.times(2)).leafNode(keyLeafNodeIdentifier, keyLeafNodeValue); + // FIXME this assertion is not working because leaves are not written in expected order + inOrder.verify(writer, Mockito.times(1)).leafNode(anotherLeafNodeIdentifier, anotherLeafNodeValue); + inOrder.verify(writer, Mockito.times(1)).endNode(); + Mockito.verifyNoMoreInteractions(writer); + } +} \ No newline at end of file diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImplTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImplTest.java index 485b522cf2..5248586943 100644 --- a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImplTest.java +++ b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/services/impl/RestconfDataServiceImplTest.java @@ -22,6 +22,7 @@ import com.google.common.base.Optional; import com.google.common.util.concurrent.Futures; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.Response; @@ -227,6 +228,66 @@ public class RestconfDataServiceImplTest { dataService.readData("example-jukebox:jukebox", uriInfo); } + /** + * Read data from config datastore according to content parameter + */ + @Test + public void testReadDataConfigTest() { + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + parameters.put("content", Collections.singletonList("config")); + + doReturn(parameters).when(uriInfo).getQueryParameters(); + doReturn(Futures.immediateCheckedFuture(Optional.of(buildBaseContConfig))).when(read) + .read(LogicalDatastoreType.CONFIGURATION, iidBase); + doReturn(Futures.immediateCheckedFuture(Optional.of(buildBaseContOperational))).when(read) + .read(LogicalDatastoreType.OPERATIONAL, iidBase); + + final Response response = dataService.readData("example-jukebox:jukebox", uriInfo); + + assertNotNull(response); + assertEquals(200, response.getStatus()); + + // response must contain only config data + final NormalizedNode data = ((NormalizedNodeContext) response.getEntity()).getData(); + + // config data present + assertTrue(((ContainerNode) data).getChild(buildPlayerCont.getIdentifier()).isPresent()); + assertTrue(((ContainerNode) data).getChild(buildLibraryCont.getIdentifier()).isPresent()); + + // state data absent + assertFalse(((ContainerNode) data).getChild(buildPlaylistList.getIdentifier()).isPresent()); + } + + /** + * Read data from operational datastore according to content parameter + */ + @Test + public void testReadDataOperationalTest() { + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + parameters.put("content", Collections.singletonList("nonconfig")); + + doReturn(parameters).when(uriInfo).getQueryParameters(); + doReturn(Futures.immediateCheckedFuture(Optional.of(buildBaseContConfig))).when(read) + .read(LogicalDatastoreType.CONFIGURATION, iidBase); + doReturn(Futures.immediateCheckedFuture(Optional.of(buildBaseContOperational))).when(read) + .read(LogicalDatastoreType.OPERATIONAL, iidBase); + + final Response response = dataService.readData("example-jukebox:jukebox", uriInfo); + + assertNotNull(response); + assertEquals(200, response.getStatus()); + + // response must contain only operational data + final NormalizedNode data = ((NormalizedNodeContext) response.getEntity()).getData(); + + // state data present + assertTrue(((ContainerNode) data).getChild(buildPlayerCont.getIdentifier()).isPresent()); + assertTrue(((ContainerNode) data).getChild(buildPlaylistList.getIdentifier()).isPresent()); + + // config data absent + assertFalse(((ContainerNode) data).getChild(buildLibraryCont.getIdentifier()).isPresent()); + } + @Test public void testPutData() { final InstanceIdentifierContext iidContext = new InstanceIdentifierContext<>(iidBase, schemaNode, null, contextRef.get()); diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ParametersUtilTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ParametersUtilTest.java new file mode 100644 index 0000000000..b55ffd970b --- /dev/null +++ b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ParametersUtilTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * 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.restful.utils; + +import static org.junit.Assert.assertEquals; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.junit.Assert; +import org.junit.Test; +import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType; + +/** + * Unit test for {@link ParametersUtil} + */ +public class ParametersUtilTest { + + /** + * Test when all parameters are allowed + */ + @Test + public void checkParametersTypesTest() { + ParametersUtil.checkParametersTypes( + RestconfDataServiceConstant.ReadData.READ_TYPE_TX, + Sets.newHashSet("content"), + RestconfDataServiceConstant.ReadData.CONTENT, RestconfDataServiceConstant.ReadData.DEPTH); + } + + /** + * Test when not allowed parameter type is used + */ + @Test + public void checkParametersTypesNegativeTest() { + try { + ParametersUtil.checkParametersTypes( + RestconfDataServiceConstant.ReadData.READ_TYPE_TX, + Sets.newHashSet("not-allowed-parameter"), + RestconfDataServiceConstant.ReadData.CONTENT, RestconfDataServiceConstant.ReadData.DEPTH); + + Assert.fail("Test expected to fail due to not allowed parameter used with operation"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Test when parameter is present at most once + */ + @Test + public void checkParameterCountTest() { + ParametersUtil.checkParameterCount(Lists.newArrayList("all"), RestconfDataServiceConstant.ReadData.CONTENT); + } + + /** + * Test when parameter is present more than once + */ + @Test + public void checkParameterCountNegativeTest() { + try { + ParametersUtil.checkParameterCount(Lists.newArrayList("config", "nonconfig", "all"), + RestconfDataServiceConstant.ReadData.CONTENT); + + Assert.fail("Test expected to fail due to multiple values of the same parameter"); + } catch (final RestconfDocumentedException e) { + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } +} \ No newline at end of file diff --git a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtilTest.java b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtilTest.java index c06cc4532f..acc543b24e 100644 --- a/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtilTest.java +++ b/restconf/sal-rest-connector/src/test/java/org/opendaylight/restconf/restful/utils/ReadDataTransactionUtilTest.java @@ -9,20 +9,31 @@ package org.opendaylight.restconf.restful.utils; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; import com.google.common.base.Optional; import com.google.common.util.concurrent.Futures; +import java.util.Collections; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.UriInfo; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType; import org.opendaylight.controller.md.sal.dom.api.DOMDataReadOnlyTransaction; import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain; import org.opendaylight.netconf.sal.restconf.impl.InstanceIdentifierContext; import org.opendaylight.netconf.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.netconf.sal.restconf.impl.RestconfError.ErrorType; +import org.opendaylight.netconf.sal.restconf.impl.WriterParameters; import org.opendaylight.restconf.restful.transaction.TransactionVarsWrapper; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; @@ -122,7 +133,8 @@ public class ReadDataTransactionUtilTest { doReturn(Futures.immediateCheckedFuture(Optional.of(data.data4))).when(read) .read(LogicalDatastoreType.OPERATIONAL, data.path); doReturn(data.path).when(context).getInstanceIdentifier(); - final NormalizedNode normalizedNode = ReadDataTransactionUtil.readData(null, wrapper); + final NormalizedNode normalizedNode = ReadDataTransactionUtil.readData( + RestconfDataServiceConstant.ReadData.ALL, wrapper); final ContainerNode checkingData = Builders .containerBuilder() .withNodeIdentifier(nodeIdentifier) @@ -165,4 +177,142 @@ public class ReadDataTransactionUtilTest { final NormalizedNode normalizedNode = ReadDataTransactionUtil.readData(valueOfContent, null); assertNull(normalizedNode); } + + /** + * Test of parsing default parameters from URI request + */ + @Test + public void parseUriParametersDefaultTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + // no parameters, default values should be used + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo); + + assertEquals("Not correctly parsed URI parameter", + RestconfDataServiceConstant.ReadData.ALL, parsedParameters.getContent()); + assertFalse("Not correctly parsed URI parameter", + parsedParameters.getDepth().isPresent()); + } + + /** + * Test of parsing user defined parameters from URI request + */ + @Test + public void parseUriParametersUserDefinedTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + final String content = "config"; + final String depth = "10"; + + parameters.put("content", Collections.singletonList(content)); + parameters.put("depth", Collections.singletonList(depth)); + + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + final WriterParameters parsedParameters = ReadDataTransactionUtil.parseUriParameters(uriInfo); + + assertEquals("Not correctly parsed URI parameter", + content, parsedParameters.getContent()); + assertTrue("Not correctly parsed URI parameter", + parsedParameters.getDepth().isPresent()); + assertEquals("Not correctly parsed URI parameter", + depth, parsedParameters.getDepth().get().toString()); + } + + /** + * Negative test of parsing request URI parameters when content parameter has not allowed value. + */ + @Test + public void parseUriParametersContentParameterNegativeTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + parameters.put("content", Collections.singletonList("not-allowed-parameter-value")); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try { + ReadDataTransactionUtil.parseUriParameters(uriInfo); + fail("Test expected to fail due to not allowed parameter value"); + } catch (final RestconfDocumentedException e) { + // Bad request + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Negative test of parsing request URI parameters when depth parameter has not allowed value. + */ + @Test + public void parseUriParametersDepthParameterNegativeTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + // inserted value is not allowed + parameters.put("depth", Collections.singletonList("bounded")); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try { + ReadDataTransactionUtil.parseUriParameters(uriInfo); + fail("Test expected to fail due to not allowed parameter value"); + } catch (final RestconfDocumentedException e) { + // Bad request + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Negative test of parsing request URI parameters when depth parameter has not allowed value (less than minimum). + */ + @Test + public void parseUriParametersDepthMinimalParameterNegativeTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + // inserted value is too low + parameters.put( + "depth", Collections.singletonList(String.valueOf(RestconfDataServiceConstant.ReadData.MIN_DEPTH - 1))); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try { + ReadDataTransactionUtil.parseUriParameters(uriInfo); + fail("Test expected to fail due to not allowed parameter value"); + } catch (final RestconfDocumentedException e) { + // Bad request + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } + + /** + * Negative test of parsing request URI parameters when depth parameter has not allowed value (more than maximum). + */ + @Test + public void parseUriParametersDepthMaximalParameterNegativeTest() { + final UriInfo uriInfo = Mockito.mock(UriInfo.class); + final MultivaluedHashMap parameters = new MultivaluedHashMap<>(); + + // inserted value is too high + parameters.put( + "depth", Collections.singletonList(String.valueOf(RestconfDataServiceConstant.ReadData.MAX_DEPTH + 1))); + when(uriInfo.getQueryParameters()).thenReturn(parameters); + + try { + ReadDataTransactionUtil.parseUriParameters(uriInfo); + fail("Test expected to fail due to not allowed parameter value"); + } catch (final RestconfDocumentedException e) { + // Bad request + assertEquals("Error type is not correct", ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType()); + assertEquals("Error tag is not correct", ErrorTag.INVALID_VALUE, e.getErrors().get(0).getErrorTag()); + assertEquals("Error status code is not correct", 400, e.getErrors().get(0).getErrorTag().getStatusCode()); + } + } }