2 * Copyright © 2019 FRINX s.r.o. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors;
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assume.assumeTrue;
12 import static org.mockito.Mockito.doReturn;
13 import static org.mockito.Mockito.mock;
15 import java.util.Arrays;
16 import java.util.List;
17 import javax.ws.rs.core.HttpHeaders;
18 import javax.ws.rs.core.MediaType;
19 import javax.ws.rs.core.Response;
20 import javax.ws.rs.core.Response.Status;
21 import org.json.JSONException;
22 import org.json.JSONObject;
24 import org.junit.BeforeClass;
25 import org.junit.Test;
26 import org.junit.runner.RunWith;
27 import org.junit.runners.Parameterized;
28 import org.junit.runners.Parameterized.Parameter;
29 import org.junit.runners.Parameterized.Parameters;
30 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
31 import org.opendaylight.restconf.common.errors.RestconfError;
32 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
33 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
34 import org.opendaylight.yangtools.yang.common.ErrorTag;
35 import org.opendaylight.yangtools.yang.common.ErrorType;
36 import org.opendaylight.yangtools.yang.common.QName;
37 import org.opendaylight.yangtools.yang.common.QNameModule;
38 import org.opendaylight.yangtools.yang.common.Revision;
39 import org.opendaylight.yangtools.yang.common.XMLNamespace;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
42 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
43 import org.skyscreamer.jsonassert.JSONAssert;
45 @RunWith(Parameterized.class)
46 public class RestconfDocumentedExceptionMapperTest {
48 private static final String EMPTY_XML = "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"></errors>";
49 private static final String EMPTY_JSON = "{}";
50 private static final QNameModule MONITORING_MODULE_INFO = QNameModule.create(
51 XMLNamespace.of("instance:identifier:patch:module"), Revision.of("2015-11-21"));
53 private static RestconfDocumentedExceptionMapper exceptionMapper;
56 public static void setupExceptionMapper() {
57 final SchemaContext schemaContext = YangParserTestUtils.parseYangResources(
58 RestconfDocumentedExceptionMapperTest.class, "/restconf/impl/ietf-restconf@2017-01-26.yang",
59 "/instanceidentifier/yang/instance-identifier-patch-module.yang");
60 final SchemaContextHandler schemaContextHandler = mock(SchemaContextHandler.class);
61 doReturn(schemaContext).when(schemaContextHandler).get();
63 exceptionMapper = new RestconfDocumentedExceptionMapper(schemaContextHandler);
67 * Testing entries 0 - 6: testing of media types and empty responses.
68 * Testing entries 7 - 8: testing of deriving of status codes from error entries.
69 * Testing entries 9 - 10: testing of serialization of different optional fields of error entries (JSON/XML).
71 * @return Testing data for parametrized test.
73 @Parameters(name = "{index}: {0}: {1}")
74 public static Iterable<Object[]> data() {
75 final RestconfDocumentedException sampleComplexError =
76 new RestconfDocumentedException("general message", new IllegalStateException("cause"), List.of(
77 new RestconfError(ErrorType.APPLICATION, ErrorTag.BAD_ATTRIBUTE, "message 1", "app tag #1"),
78 new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED,
79 "message 2", "app tag #2", "my info"),
80 new RestconfError(ErrorType.RPC, ErrorTag.DATA_MISSING,
81 "message 3", " app tag #3", "my error info", YangInstanceIdentifier.builder()
82 .node(QName.create(MONITORING_MODULE_INFO, "patch-cont"))
83 .node(QName.create(MONITORING_MODULE_INFO, "my-list1"))
84 .nodeWithKey(QName.create(MONITORING_MODULE_INFO, "my-list1"),
85 QName.create(MONITORING_MODULE_INFO, "name"), "sample")
86 .node(QName.create(MONITORING_MODULE_INFO, "my-leaf12"))
89 return Arrays.asList(new Object[][] {
91 "Mapping of the exception without any errors and XML output derived from content type",
92 new RestconfDocumentedException(Status.BAD_REQUEST),
93 mockHttpHeaders(MediaType.APPLICATION_XML_TYPE, List.of()),
94 Response.status(Status.BAD_REQUEST)
95 .type(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)
100 "Mapping of the exception without any errors and JSON output derived from unsupported content type",
101 new RestconfDocumentedException(Status.INTERNAL_SERVER_ERROR),
102 mockHttpHeaders(MediaType.APPLICATION_FORM_URLENCODED_TYPE, List.of()),
103 Response.status(Status.INTERNAL_SERVER_ERROR)
104 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
109 "Mapping of the exception without any errors and JSON output derived from missing content type "
110 + "and accepted media types",
111 new RestconfDocumentedException(Status.NOT_IMPLEMENTED),
112 mockHttpHeaders(null, List.of()),
113 Response.status(Status.NOT_IMPLEMENTED)
114 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
119 "Mapping of the exception without any errors and JSON output derived from expected types - both JSON"
120 + "and XML types are accepted, but server should prefer JSON format",
121 new RestconfDocumentedException(Status.INTERNAL_SERVER_ERROR),
122 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, List.of(
123 MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.APPLICATION_XML_TYPE,
124 MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_OCTET_STREAM_TYPE)),
125 Response.status(Status.INTERNAL_SERVER_ERROR)
126 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
131 "Mapping of the exception without any errors and JSON output derived from expected types - there"
132 + "is only a wildcard type that should be mapped to default type",
133 new RestconfDocumentedException(Status.NOT_FOUND),
134 mockHttpHeaders(null, List.of(MediaType.WILDCARD_TYPE)),
135 Response.status(Status.NOT_FOUND)
136 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
141 "Mapping of the exception without any errors and XML output derived from expected types - "
142 + "we should choose the most specific and supported type",
143 new RestconfDocumentedException(Status.NOT_FOUND),
144 mockHttpHeaders(null, List.of(MediaType.valueOf("*/yang-data+json"),
145 MediaType.valueOf("application/yang-data+xml"), MediaType.WILDCARD_TYPE)),
146 Response.status(Status.NOT_FOUND)
147 .type(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)
152 "Mapping of the exception without any errors and XML output derived from expected types - "
153 + "we should choose the most specific and supported type",
154 new RestconfDocumentedException(Status.NOT_FOUND),
155 mockHttpHeaders(null, List.of(MediaType.valueOf("*/unsupported"),
156 MediaType.valueOf("application/*"), MediaType.WILDCARD_TYPE)),
157 Response.status(Status.NOT_FOUND)
158 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
163 "Mapping of the exception with one error entry but null status code. This status code should"
164 + "be derived from single error entry; JSON output",
165 new RestconfDocumentedException("Sample error message"),
166 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, List.of(MediaTypes.APPLICATION_YANG_PATCH_JSON_TYPE)),
167 Response.status(Status.INTERNAL_SERVER_ERROR)
168 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
173 + " \"error-tag\": \"operation-failed\",\n"
174 + " \"error-message\": \"Sample error message\",\n"
175 + " \"error-type\": \"application\"\n"
183 "Mapping of the exception with two error entries but null status code. This status code should"
184 + "be derived from the first error entry that is specified; XML output",
185 new RestconfDocumentedException("general message", new IllegalStateException("cause"), List.of(
186 new RestconfError(ErrorType.APPLICATION, ErrorTag.BAD_ATTRIBUTE, "message 1"),
187 new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "message 2"))),
188 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, List.of(MediaTypes.APPLICATION_YANG_PATCH_XML_TYPE)),
189 Response.status(Status.BAD_REQUEST)
190 .type(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)
191 .entity("<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n"
193 + "<error-message>message 1</error-message>\n"
194 + "<error-tag>bad-attribute</error-tag>\n"
195 + "<error-type>application</error-type>\n"
198 + "<error-message>message 2</error-message>\n"
199 + "<error-tag>operation-failed</error-tag>\n"
200 + "<error-type>application</error-type>\n"
206 "Mapping of the exception with three entries and optional entries set: error app tag (the first error),"
207 + " error info (the second error), and error path (the last error); JSON output",
208 sampleComplexError, mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, List.of(
209 MediaType.APPLICATION_JSON_TYPE)),
210 Response.status(Status.BAD_REQUEST)
211 .type(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
216 + " \"error-tag\": \"bad-attribute\",\n"
217 + " \"error-app-tag\": \"app tag #1\",\n"
218 + " \"error-message\": \"message 1\",\n"
219 + " \"error-type\": \"application\"\n"
222 + " \"error-tag\": \"operation-failed\",\n"
223 + " \"error-app-tag\": \"app tag #2\",\n"
224 + " \"error-info\": \"my info\",\n"
225 + " \"error-message\": \"message 2\",\n"
226 + " \"error-type\": \"application\"\n"
229 + " \"error-tag\": \"data-missing\",\n"
230 + " \"error-app-tag\": \" app tag #3\",\n"
231 + " \"error-info\": \"my error info\",\n"
232 + " \"error-message\": \"message 3\",\n"
233 + " \"error-path\": \"/instance-identifier-patch-module:patch-cont/"
234 + "my-list1[name='sample']/my-leaf12\",\n"
235 + " \"error-type\": \"rpc\"\n"
243 "Mapping of the exception with three entries and optional entries set: error app tag (the first error),"
244 + " error info (the second error), and error path (the last error); XML output",
245 sampleComplexError, mockHttpHeaders(MediaTypes.APPLICATION_YANG_PATCH_JSON_TYPE,
246 List.of(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)),
247 Response.status(Status.BAD_REQUEST)
248 .type(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)
249 .entity("<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n"
251 + "<error-type>application</error-type>\n"
252 + "<error-message>message 1</error-message>\n"
253 + "<error-tag>bad-attribute</error-tag>\n"
254 + "<error-app-tag>app tag #1</error-app-tag>\n"
257 + "<error-type>application</error-type>\n"
258 + "<error-message>message 2</error-message>\n"
259 + "<error-tag>operation-failed</error-tag>\n"
260 + "<error-app-tag>app tag #2</error-app-tag>\n"
261 + "<error-info>my info</error-info></error>\n"
263 + "<error-type>rpc</error-type>\n"
264 + "<error-path xmlns:a=\"instance:identifier:patch:module\">/a:patch-cont/"
265 + "a:my-list1[a:name='sample']/a:my-leaf12</error-path>\n"
266 + "<error-message>message 3</error-message>\n"
267 + "<error-tag>data-missing</error-tag>\n"
268 + "<error-app-tag> app tag #3</error-app-tag>\n"
269 + "<error-info>my error info</error-info>\n"
278 public String testDescription;
280 public RestconfDocumentedException thrownException;
282 public HttpHeaders httpHeaders;
284 public Response expectedResponse;
287 public void testMappingOfExceptionToResponse() throws JSONException {
288 exceptionMapper.setHttpHeaders(httpHeaders);
289 final Response response = exceptionMapper.toResponse(thrownException);
290 compareResponseWithExpectation(expectedResponse, response);
294 public void testFormatingJson() throws JSONException {
295 assumeTrue(expectedResponse.getMediaType().equals(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE));
297 exceptionMapper.setHttpHeaders(httpHeaders);
298 final Response response = exceptionMapper.toResponse(thrownException);
299 assertEquals(expectedResponse.getEntity().toString(), response.getEntity().toString());
302 private static HttpHeaders mockHttpHeaders(final MediaType contentType, final List<MediaType> acceptedTypes) {
303 final HttpHeaders httpHeaders = mock(HttpHeaders.class);
304 doReturn(contentType).when(httpHeaders).getMediaType();
305 doReturn(acceptedTypes).when(httpHeaders).getAcceptableMediaTypes();
309 private static void compareResponseWithExpectation(final Response expectedResponse, final Response actualResponse)
310 throws JSONException {
311 final String errorMessage = String.format("Actual response %s doesn't equal to expected response %s",
312 actualResponse, expectedResponse);
313 assertEquals(errorMessage, expectedResponse.getStatus(), actualResponse.getStatus());
314 assertEquals(errorMessage, expectedResponse.getMediaType(), actualResponse.getMediaType());
315 if (MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE.equals(expectedResponse.getMediaType())) {
316 JSONAssert.assertEquals(expectedResponse.getEntity().toString(),
317 actualResponse.getEntity().toString(), true);
319 final JSONObject expectedResponseInJson = XML.toJSONObject(expectedResponse.getEntity().toString());
320 final JSONObject actualResponseInJson = XML.toJSONObject(actualResponse.getEntity().toString());
321 JSONAssert.assertEquals(expectedResponseInJson, actualResponseInJson, true);