X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=blobdiff_plain;f=opendaylight%2Fmd-sal%2Fsal-rest-connector%2Fsrc%2Ftest%2Fjava%2Forg%2Fopendaylight%2Fcontroller%2Fsal%2Frestconf%2Fimpl%2Ftest%2FRestconfDocumentedExceptionMapperTest.java;fp=opendaylight%2Fmd-sal%2Fsal-rest-connector%2Fsrc%2Ftest%2Fjava%2Forg%2Fopendaylight%2Fcontroller%2Fsal%2Frestconf%2Fimpl%2Ftest%2FRestconfDocumentedExceptionMapperTest.java;h=fc5d7be724d9c9fee4438834bbf45d9e8cf3c9bf;hb=26da3c2a206a753356b507b018052cbb9cccca7d;hp=0000000000000000000000000000000000000000;hpb=fdb2cda290536cbd60fd5046729feeabf8081a2d;p=controller.git diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java new file mode 100644 index 0000000000..fc5d7be724 --- /dev/null +++ b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java @@ -0,0 +1,966 @@ +/* + * Copyright (c) 2014 Brocade Communications 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.controller.sal.restconf.impl.test; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathFactory; + +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.controller.sal.rest.api.Draft02; +import org.opendaylight.controller.sal.rest.api.RestconfService; +import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper; +import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider; +import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider; +import org.opendaylight.controller.sal.restconf.impl.ControllerContext; +import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException; +import org.opendaylight.controller.sal.restconf.impl.RestconfError; +import org.opendaylight.controller.sal.restconf.impl.StructuredData; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag; +import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import com.google.common.io.ByteStreams; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * Unit tests for RestconfDocumentedExceptionMapper. + * + * @author Thomas Pantelis + */ +public class RestconfDocumentedExceptionMapperTest extends JerseyTest { + + interface ErrorInfoVerifier { + void verifyXML( Node errorInfoNode ); + void verifyJson( JsonElement errorInfoElement ); + } + + static class ComplexErrorInfoVerifier implements ErrorInfoVerifier { + + Map expErrorInfo; + + public ComplexErrorInfoVerifier( Map expErrorInfo ) { + this.expErrorInfo = expErrorInfo; + } + + @Override + public void verifyXML( Node errorInfoNode ) { + + Map mutableExpMap = Maps.newHashMap( expErrorInfo ); + NodeList childNodes = errorInfoNode.getChildNodes(); + for( int i = 0; i < childNodes.getLength(); i++ ) { + Node child = childNodes.item( i ); + if( child instanceof Element ) { + String expValue = mutableExpMap.remove( child.getNodeName() ); + assertNotNull( "Found unexpected \"error-info\" child node: " + + child.getNodeName(), expValue ); + assertEquals( "Text content for \"error-info\" child node " + + child.getNodeName(), expValue, child.getTextContent() ); + } + } + + if( !mutableExpMap.isEmpty() ) { + fail( "Missing \"error-info\" child nodes: " + mutableExpMap ); + } + } + + @Override + public void verifyJson( JsonElement errorInfoElement ) { + + assertTrue( "\"error-info\" Json element is not an Object", + errorInfoElement.isJsonObject() ); + + Map actualErrorInfo = Maps.newHashMap(); + for( Entry entry: errorInfoElement.getAsJsonObject().entrySet() ) { + String leafName = entry.getKey(); + JsonElement leafElement = entry.getValue(); + actualErrorInfo.put( leafName, leafElement.getAsString() ); + } + + Map mutableExpMap = Maps.newHashMap( expErrorInfo ); + for( Entry actual: actualErrorInfo.entrySet() ) { + String expValue = mutableExpMap.remove( actual.getKey() ); + assertNotNull( "Found unexpected \"error-info\" child node: " + + actual.getKey(), expValue ); + assertEquals( "Text content for \"error-info\" child node " + + actual.getKey(), expValue, actual.getValue() ); + } + + if( !mutableExpMap.isEmpty() ) { + fail( "Missing \"error-info\" child nodes: " + mutableExpMap ); + } + } + } + + static class SimpleErrorInfoVerifier implements ErrorInfoVerifier { + + String expTextContent; + + public SimpleErrorInfoVerifier( String expErrorInfo ) { + this.expTextContent = expErrorInfo; + } + + void verifyContent( String actualContent ) { + assertNotNull( "Actual \"error-info\" text content is null", actualContent ); + assertTrue( "", actualContent.contains( expTextContent ) ); + } + + @Override + public void verifyXML( Node errorInfoNode ) { + verifyContent( errorInfoNode.getTextContent() ); + } + + @Override + public void verifyJson( JsonElement errorInfoElement ) { + verifyContent( errorInfoElement.getAsString() ); + } + } + + static RestconfService mockRestConf = mock( RestconfService.class ); + + static XPath XPATH = XPathFactory.newInstance().newXPath(); + static XPathExpression ERROR_LIST; + static XPathExpression ERROR_TYPE; + static XPathExpression ERROR_TAG; + static XPathExpression ERROR_MESSAGE; + static XPathExpression ERROR_APP_TAG; + static XPathExpression ERROR_INFO; + + @BeforeClass + public static void init() throws Exception { + ControllerContext.getInstance().setGlobalSchema( TestUtils.loadSchemaContext("/modules") ); + + NamespaceContext nsContext = new NamespaceContext() { + @Override + public Iterator getPrefixes( String namespaceURI ) { + return null; + } + + @Override + public String getPrefix( String namespaceURI ) { + return null; + } + + @Override + public String getNamespaceURI( String prefix ) { + return "ietf-restconf".equals( prefix ) ? Draft02.RestConfModule.NAMESPACE : null; + } + }; + + XPATH.setNamespaceContext( nsContext ); + ERROR_LIST = XPATH.compile( "ietf-restconf:errors/ietf-restconf:error" ); + ERROR_TYPE = XPATH.compile( "ietf-restconf:error-type" ); + ERROR_TAG = XPATH.compile( "ietf-restconf:error-tag" ); + ERROR_MESSAGE = XPATH.compile( "ietf-restconf:error-message" ); + ERROR_APP_TAG = XPATH.compile( "ietf-restconf:error-app-tag" ); + ERROR_INFO = XPATH.compile( "ietf-restconf:error-info" ); + } + + @Override + @Before + public void setUp() throws Exception { + reset( mockRestConf ); + super.setUp(); + } + + @Override + protected Application configure() { + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig = resourceConfig.registerInstances( mockRestConf, StructuredDataToXmlProvider.INSTANCE, + StructuredDataToJsonProvider.INSTANCE ); + resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class ); + return resourceConfig; + } + + void stageMockEx( RestconfDocumentedException ex ) { + reset( mockRestConf ); + when( mockRestConf.readOperationalData( any( String.class ) ) ).thenThrow( ex ); + } + + void testJsonResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType, + ErrorTag expErrorTag, String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + stageMockEx( ex ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, expStatus ); + + verifyJsonResponseBody( stream, expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + @Test + public void testToJsonResponseWithMessageOnly() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR, + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null ); + + // To test verification code +// String json = +// "{ errors: {" + +// " error: [{" + +// " error-tag : \"operation-failed\"" + +// " ,error-type : \"application\"" + +// " ,error-message : \"An error occurred\"" + +// " ,error-info : {" + +// " session-id: \"123\"" + +// " ,address: \"1.2.3.4\"" + +// " }" + +// " }]" + +// " }" + +// "}"; +// +// verifyJsonResponseBody( new java.io.StringBufferInputStream(json ), ErrorType.APPLICATION, +// ErrorTag.OPERATION_FAILED, "An error occurred", null, +// com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) ); + } + + @Test + public void testToJsonResponseWithInUseErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.IN_USE ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.IN_USE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithInvalidValueErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC, + ErrorTag.INVALID_VALUE ), + Status.BAD_REQUEST, ErrorType.RPC, + ErrorTag.INVALID_VALUE, "mock error", null, null ); + + } + + @Test + public void testToJsonResponseWithTooBigErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT, + ErrorTag.TOO_BIG ), + Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT, + ErrorTag.TOO_BIG, "mock error", null, null ); + + } + + @Test + public void testToJsonResponseWithMissingAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithBadAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE, "mock error", null, null ); + } + @Test + public void testToJsonResponseWithUnknownAttributeErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithBadElementErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT ), + Status.BAD_REQUEST, + ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithUnknownElementErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithUnknownNamespaceErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithMalformedMessageErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithAccessDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED ), + Status.FORBIDDEN, ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithLockDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithResourceDeniedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithRollbackFailedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithDataExistsErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithDataMissingErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithOperationNotSupportedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED ), + Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithOperationFailedErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithPartialOperationErrorTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION, "mock error", null, null ); + } + + @Test + public void testToJsonResponseWithErrorAppTag() throws Exception { + + testJsonResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag" ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null ); + } + + @Test + public void testToJsonResponseWithMultipleErrors() throws Exception { + + List errorList = Arrays.asList( + new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ), + new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) ); + stageMockEx( new RestconfDocumentedException( errorList ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, Status.CONFLICT ); + + JsonArray arrayElement = parseJsonErrorArrayElement( stream ); + + assertEquals( "\"error\" Json array element length", 2, arrayElement.size() ); + + verifyJsonErrorNode( arrayElement.get( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, + "mock error1", null, null ); + + verifyJsonErrorNode( arrayElement.get( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, + "mock error2", null, null ); + } + + @Test + public void testToJsonResponseWithErrorInfo() throws Exception { + + String errorInfo = "
1.2.3.4
123"; + testJsonResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag", errorInfo ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", + new ComplexErrorInfoVerifier( ImmutableMap.of( + "session-id", "123", "address", "1.2.3.4" ) ) ); + } + + @Test + public void testToJsonResponseWithExceptionCause() throws Exception { + + Exception cause = new Exception( "mock exception cause" ); + testJsonResponse( new RestconfDocumentedException( "mock error", cause ), + Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, "mock error", null, + new SimpleErrorInfoVerifier( cause.getMessage() ) ); + } + + void testXMLResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType, + ErrorTag expErrorTag, String expErrorMessage, + String expErrorAppTag, ErrorInfoVerifier errorInfoVerifier ) throws Exception + { + stageMockEx( ex ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, expStatus ); + + verifyXMLResponseBody( stream, expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + @Test + public void testToXMLResponseWithMessageOnly() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR, + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null ); + + // To test verification code +// String xml = +// ""+ +// " " + +// " application"+ +// " operation-failed"+ +// " An error occurred"+ +// " " + +// " 123" + +// "
1.2.3.4
" + +// "
" + +// "
" + +// "
"; +// +// verifyXMLResponseBody( new java.io.StringBufferInputStream(xml), ErrorType.APPLICATION, +// ErrorTag.OPERATION_FAILED, "An error occurred", null, +// com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) ); + } + + @Test + public void testToXMLResponseWithInUseErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.IN_USE ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.IN_USE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithInvalidValueErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC, + ErrorTag.INVALID_VALUE ), + Status.BAD_REQUEST, ErrorType.RPC, + ErrorTag.INVALID_VALUE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithTooBigErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT, + ErrorTag.TOO_BIG ), + Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT, + ErrorTag.TOO_BIG, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithMissingAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithBadAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ATTRIBUTE, "mock error", null, null ); + } + @Test + public void testToXMLResponseWithUnknownAttributeErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithBadElementErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.BAD_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithUnknownElementErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithUnknownNamespaceErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithMalformedMessageErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE ), + Status.BAD_REQUEST, ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithAccessDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED ), + Status.FORBIDDEN, ErrorType.PROTOCOL, + ErrorTag.ACCESS_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithLockDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.LOCK_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithResourceDeniedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.RESOURCE_DENIED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithRollbackFailedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.ROLLBACK_FAILED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithDataExistsErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithDataMissingErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING ), + Status.CONFLICT, ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithOperationNotSupportedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED ), + Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL, + ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithOperationFailedErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.OPERATION_FAILED, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithPartialOperationErrorTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION ), + Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL, + ErrorTag.PARTIAL_OPERATION, "mock error", null, null ); + } + + @Test + public void testToXMLResponseWithErrorAppTag() throws Exception { + + testXMLResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag" ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null ); + } + + @Test + public void testToXMLResponseWithErrorInfo() throws Exception { + + String errorInfo = "
1.2.3.4
123"; + testXMLResponse( new RestconfDocumentedException( new RestconfError( + ErrorType.APPLICATION, ErrorTag.INVALID_VALUE, + "mock error", "mock-app-tag", errorInfo ) ), + Status.BAD_REQUEST, ErrorType.APPLICATION, + ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", + new ComplexErrorInfoVerifier( ImmutableMap.of( + "session-id", "123", "address", "1.2.3.4" ) ) ); + } + + @Test + public void testToXMLResponseWithExceptionCause() throws Exception { + + Exception cause = new Exception( "mock exception cause" ); + testXMLResponse( new RestconfDocumentedException( "mock error", cause ), + Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION, + ErrorTag.OPERATION_FAILED, "mock error", null, + new SimpleErrorInfoVerifier( cause.getMessage() ) ); + } + + @Test + public void testToXMLResponseWithMultipleErrors() throws Exception { + + List errorList = Arrays.asList( + new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ), + new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) ); + stageMockEx( new RestconfDocumentedException( errorList ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, Status.CONFLICT ); + + Document doc = parseXMLDocument( stream ); + + NodeList children = getXMLErrorList( doc, 2 ); + + verifyXMLErrorNode( children.item( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, + "mock error1", null, null ); + + verifyXMLErrorNode( children.item( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, + "mock error2", null, null ); + } + + @Test + public void testToResponseWithAcceptHeader() throws Exception { + + stageMockEx( new RestconfDocumentedException( "mock error" ) ); + + Response resp = target("/operational/foo") + .request().header( "Accept", MediaType.APPLICATION_JSON ).get(); + + InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, + Status.INTERNAL_SERVER_ERROR ); + + verifyJsonResponseBody( stream, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", + null, null ); + } + + @Test + public void testToResponseWithStatusOnly() throws Exception { + + // The StructuredDataToJsonProvider should throw a RestconfDocumentedException with no data + + when( mockRestConf.readOperationalData( any( String.class ) ) ) + .thenReturn( new StructuredData( null, null, null ) ); + + Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get(); + + verifyResponse( resp, MediaType.TEXT_PLAIN, Status.NOT_FOUND ); + } + + InputStream verifyResponse( Response resp, String expMediaType, Status expStatus ) { + assertEquals( "getMediaType", MediaType.valueOf( expMediaType ), resp.getMediaType() ); + assertEquals( "getStatus", expStatus.getStatusCode(), resp.getStatus() ); + + Object entity = resp.getEntity(); + assertEquals( "Response entity", true, entity instanceof InputStream ); + InputStream stream = (InputStream)entity; + return stream; + } + + void verifyJsonResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + JsonArray arrayElement = parseJsonErrorArrayElement( stream ); + + assertEquals( "\"error\" Json array element length", 1, arrayElement.size() ); + + verifyJsonErrorNode( arrayElement.get( 0 ), expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + private JsonArray parseJsonErrorArrayElement( InputStream stream ) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteStreams.copy( stream, bos ); + + System.out.println("JSON: "+bos.toString()); + + JsonParser parser = new JsonParser(); + JsonElement rootElement; + + try { + rootElement = parser.parse( + new InputStreamReader( new ByteArrayInputStream( bos.toByteArray() ) ) ); + } + catch( Exception e ) { + throw new IllegalArgumentException( "Invalid JSON response:\n" + bos.toString(), e ); + } + + assertTrue( "Root element of Json is not an Object", rootElement.isJsonObject() ); + + Set> errorsEntrySet = rootElement.getAsJsonObject().entrySet(); + assertEquals( "Json Object element set count", 1, errorsEntrySet.size() ); + + Entry errorsEntry = errorsEntrySet.iterator().next(); + JsonElement errorsElement = errorsEntry.getValue(); + assertEquals( "First Json element name", "errors", errorsEntry.getKey() ); + assertTrue( "\"errors\" Json element is not an Object", errorsElement.isJsonObject() ); + + Set> errorListEntrySet = errorsElement.getAsJsonObject().entrySet(); + assertEquals( "Root \"errors\" element child count", 1, errorListEntrySet.size() ); + + JsonElement errorListElement = errorListEntrySet.iterator().next().getValue(); + assertEquals( "\"errors\" child Json element name", "error", + errorListEntrySet.iterator().next().getKey() ); + assertTrue( "\"error\" Json element is not an Array", errorListElement.isJsonArray() ); + + return errorListElement.getAsJsonArray(); + } + + void verifyJsonErrorNode( JsonElement errorEntryElement, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) { + + JsonElement errorInfoElement = null; + Map actualErrorInfo = null; + Map leafMap = Maps.newHashMap(); + for( Entry entry: errorEntryElement.getAsJsonObject().entrySet() ) { + String leafName = entry.getKey(); + JsonElement leafElement = entry.getValue(); + + if( "error-info".equals( leafName ) ) { + assertNotNull( "Found unexpected \"error-info\" element", errorInfoVerifier ); + errorInfoElement = leafElement; + } + else { + assertTrue( "\"error\" leaf Json element " + leafName + + " is not a Primitive", leafElement.isJsonPrimitive() ); + + leafMap.put( leafName, leafElement.getAsString() ); + } + } + + assertEquals( "error-type", expErrorType.getErrorTypeTag(), leafMap.remove( "error-type" ) ); + assertEquals( "error-tag", expErrorTag.getTagValue(), leafMap.remove( "error-tag" ) ); + + verifyOptionalJsonLeaf( leafMap.remove( "error-message" ), expErrorMessage, "error-message" ); + verifyOptionalJsonLeaf( leafMap.remove( "error-app-tag" ), expErrorAppTag, "error-app-tag" ); + + if( !leafMap.isEmpty() ) { + fail( "Found unexpected Json leaf elements for \"error\" element: " + leafMap ); + } + + if( errorInfoVerifier != null ) { + assertNotNull( "Missing \"error-info\" element", errorInfoElement ); + errorInfoVerifier.verifyJson( errorInfoElement ); + } + } + + void verifyOptionalJsonLeaf( String actualValue, String expValue, String tagName ) { + if( expValue != null ) { + assertEquals( tagName, expValue, actualValue ); + } + else { + assertNull( "Found unexpected \"error\" leaf entry for: " + tagName, actualValue ); + } + } + + void verifyXMLResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) + throws Exception { + + Document doc = parseXMLDocument( stream ); + + NodeList children = getXMLErrorList( doc, 1 ); + + verifyXMLErrorNode( children.item( 0 ), expErrorType, expErrorTag, expErrorMessage, + expErrorAppTag, errorInfoVerifier ); + } + + private Document parseXMLDocument( InputStream stream ) throws IOException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setCoalescing(true); + factory.setIgnoringElementContentWhitespace(true); + factory.setIgnoringComments(true); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteStreams.copy( stream, bos ); + + System.out.println("XML: "+bos.toString()); + + Document doc = null; + try { + doc = factory.newDocumentBuilder().parse( new ByteArrayInputStream( bos.toByteArray() ) ); + } + catch( Exception e ) { + throw new IllegalArgumentException( "Invalid XML response:\n" + bos.toString(), e ); + } + return doc; + } + + void verifyXMLErrorNode( Node errorNode, ErrorType expErrorType, ErrorTag expErrorTag, + String expErrorMessage, String expErrorAppTag, + ErrorInfoVerifier errorInfoVerifier ) throws Exception { + + String errorType = (String)ERROR_TYPE.evaluate( errorNode, XPathConstants.STRING ); + assertEquals( "error-type", expErrorType.getErrorTypeTag(), errorType ); + + String errorTag = (String)ERROR_TAG.evaluate( errorNode, XPathConstants.STRING ); + assertEquals( "error-tag", expErrorTag.getTagValue(), errorTag ); + + verifyOptionalXMLLeaf( errorNode, ERROR_MESSAGE, expErrorMessage, "error-message" ); + verifyOptionalXMLLeaf( errorNode, ERROR_APP_TAG, expErrorAppTag, "error-app-tag" ); + + Node errorInfoNode = (Node)ERROR_INFO.evaluate( errorNode, XPathConstants.NODE ); + if( errorInfoVerifier != null ) { + assertNotNull( "Missing \"error-info\" node", errorInfoNode ); + + errorInfoVerifier.verifyXML( errorInfoNode ); + } + else { + assertNull( "Found unexpected \"error-info\" node", errorInfoNode ); + } + } + + void verifyOptionalXMLLeaf( Node fromNode, XPathExpression xpath, String expValue, + String tagName ) throws Exception { + if( expValue != null ) { + String actual = (String)xpath.evaluate( fromNode, XPathConstants.STRING ); + assertEquals( tagName, expValue, actual ); + } + else { + assertNull( "Found unexpected \"error\" leaf entry for: " + tagName, + xpath.evaluate( fromNode, XPathConstants.NODE ) ); + } + } + + NodeList getXMLErrorList( Node fromNode, int count ) throws Exception { + NodeList errorList = (NodeList)ERROR_LIST.evaluate( fromNode, XPathConstants.NODESET ); + assertNotNull( "Root errors node is empty", errorList ); + assertEquals( "Root errors node child count", count, errorList.getLength() ); + return errorList; + } +}