Make ListenerAdapter serialize JSON directly
[netconf.git] / restconf / restconf-nb-rfc8040 / src / test / java / org / opendaylight / restconf / nb / rfc8040 / jersey / providers / errors / RestconfDocumentedExceptionMapperTest.java
1 /*
2  * Copyright © 2019 FRINX s.r.o. All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors;
9
10 import static org.mockito.Mockito.doReturn;
11 import static org.mockito.Mockito.mock;
12
13 import com.google.common.collect.Lists;
14 import java.net.URI;
15 import java.util.Arrays;
16 import java.util.Collections;
17 import java.util.List;
18 import javax.ws.rs.core.HttpHeaders;
19 import javax.ws.rs.core.MediaType;
20 import javax.ws.rs.core.Response;
21 import javax.ws.rs.core.Response.Status;
22 import org.json.JSONException;
23 import org.json.JSONObject;
24 import org.json.XML;
25 import org.junit.Assert;
26 import org.junit.BeforeClass;
27 import org.junit.Test;
28 import org.junit.runner.RunWith;
29 import org.junit.runners.Parameterized;
30 import org.junit.runners.Parameterized.Parameter;
31 import org.junit.runners.Parameterized.Parameters;
32 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
33 import org.opendaylight.restconf.common.errors.RestconfError;
34 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
35 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
36 import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
37 import org.opendaylight.yangtools.yang.common.QName;
38 import org.opendaylight.yangtools.yang.common.QNameModule;
39 import org.opendaylight.yangtools.yang.common.Revision;
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;
44
45 @RunWith(Parameterized.class)
46 public class RestconfDocumentedExceptionMapperTest {
47
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 QNameModule MONITORING_MODULE_INFO = QNameModule.create(
51             URI.create("instance:identifier:patch:module"), Revision.of("2015-11-21"));
52
53     private static RestconfDocumentedExceptionMapper exceptionMapper;
54
55     @BeforeClass
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();
62
63         exceptionMapper = new RestconfDocumentedExceptionMapper(schemaContextHandler);
64     }
65
66     /**
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).
70      *
71      * @return Testing data for parametrized test.
72      */
73     @Parameters(name = "{index}: {0}: {1}")
74     public static Iterable<Object[]> data() {
75         final RestconfDocumentedException sampleComplexError
76                 = new RestconfDocumentedException("general message", new IllegalStateException("cause"),
77                 Lists.newArrayList(
78                         new RestconfError(ErrorType.APPLICATION, ErrorTag.BAD_ATTRIBUTE,
79                                 "message 1", "app tag #1"),
80                         new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED,
81                                 "message 2", "app tag #2", "my info"),
82                         new RestconfError(ErrorType.RPC, ErrorTag.DATA_MISSING,
83                                 "message 3", " app tag #3", "my error info", YangInstanceIdentifier.builder()
84                                 .node(QName.create(MONITORING_MODULE_INFO, "patch-cont"))
85                                 .node(QName.create(MONITORING_MODULE_INFO, "my-list1"))
86                                 .nodeWithKey(QName.create(MONITORING_MODULE_INFO, "my-list1"),
87                                         QName.create(MONITORING_MODULE_INFO, "name"), "sample")
88                                 .node(QName.create(MONITORING_MODULE_INFO, "my-leaf12"))
89                                 .build())));
90
91         return Arrays.asList(new Object[][]{
92             {
93                 "Mapping of the exception without any errors and XML output derived from content type",
94                 new RestconfDocumentedException(Status.BAD_REQUEST),
95                 mockHttpHeaders(MediaType.APPLICATION_XML_TYPE, Collections.emptyList()),
96                 Response.status(Status.BAD_REQUEST)
97                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_XML_TYPE)
98                         .entity(EMPTY_XML)
99                         .build()
100             },
101             {
102                 "Mapping of the exception without any errors and JSON output derived from unsupported content type",
103                 new RestconfDocumentedException(Status.INTERNAL_SERVER_ERROR),
104                 mockHttpHeaders(MediaType.APPLICATION_FORM_URLENCODED_TYPE, Collections.emptyList()),
105                 Response.status(Status.INTERNAL_SERVER_ERROR)
106                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
107                         .entity(EMPTY_JSON)
108                         .build()
109             },
110             {
111                 "Mapping of the exception without any errors and JSON output derived from missing content type "
112                         + "and accepted media types",
113                 new RestconfDocumentedException(Status.NOT_IMPLEMENTED),
114                 mockHttpHeaders(null, Collections.emptyList()),
115                 Response.status(Status.NOT_IMPLEMENTED)
116                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
117                         .entity(EMPTY_JSON)
118                         .build()
119             },
120             {
121                 "Mapping of the exception without any errors and JSON output derived from expected types - both JSON"
122                         + "and XML types are accepted, but server should prefer JSON format",
123                 new RestconfDocumentedException(Status.INTERNAL_SERVER_ERROR),
124                 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, Lists.newArrayList(
125                         MediaType.APPLICATION_FORM_URLENCODED_TYPE, MediaType.APPLICATION_XML_TYPE,
126                         MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_OCTET_STREAM_TYPE)),
127                 Response.status(Status.INTERNAL_SERVER_ERROR)
128                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
129                         .entity(EMPTY_JSON)
130                         .build()
131             },
132             {
133                 "Mapping of the exception without any errors and JSON output derived from expected types - there"
134                         + "is only a wildcard type that should be mapped to default type",
135                 new RestconfDocumentedException(Status.NOT_FOUND),
136                 mockHttpHeaders(null, Lists.newArrayList(MediaType.WILDCARD_TYPE)),
137                 Response.status(Status.NOT_FOUND)
138                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
139                         .entity(EMPTY_JSON)
140                         .build()
141             },
142             {
143                 "Mapping of the exception without any errors and XML output derived from expected types - "
144                         + "we should choose the most specific and supported type",
145                 new RestconfDocumentedException(Status.NOT_FOUND),
146                 mockHttpHeaders(null, Lists.newArrayList(MediaType.valueOf("*/yang-data+json"),
147                         MediaType.valueOf("application/yang-data+xml"), MediaType.WILDCARD_TYPE)),
148                 Response.status(Status.NOT_FOUND)
149                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_XML_TYPE)
150                         .entity(EMPTY_XML)
151                         .build()
152             },
153             {
154                 "Mapping of the exception without any errors and XML output derived from expected types - "
155                         + "we should choose the most specific and supported type",
156                 new RestconfDocumentedException(Status.NOT_FOUND),
157                 mockHttpHeaders(null, Lists.newArrayList(MediaType.valueOf("*/unsupported"),
158                         MediaType.valueOf("application/*"), MediaType.WILDCARD_TYPE)),
159                 Response.status(Status.NOT_FOUND)
160                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
161                         .entity(EMPTY_JSON)
162                         .build()
163             },
164             {
165                 "Mapping of the exception with one error entry but null status code. This status code should"
166                         + "be derived from single error entry; JSON output",
167                 new RestconfDocumentedException("Sample error message"),
168                 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, Collections.singletonList(
169                         RestconfDocumentedExceptionMapper.YANG_PATCH_JSON_TYPE)),
170                 Response.status(Status.INTERNAL_SERVER_ERROR)
171                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
172                         .entity("{\n"
173                                 + "  \"errors\": {\n"
174                                 + "    \"error\": [\n"
175                                 + "      {\n"
176                                 + "        \"error-message\": \"Sample error message\",\n"
177                                 + "        \"error-tag\": \"operation-failed\",\n"
178                                 + "        \"error-type\": \"application\"\n"
179                                 + "      }\n"
180                                 + "    ]\n"
181                                 + "  }\n"
182                                 + "}\n")
183                         .build()
184             },
185             {
186                 "Mapping of the exception with two error entries but null status code. This status code should"
187                         + "be derived from the first error entry that is specified; XML output",
188                 new RestconfDocumentedException("general message", new IllegalStateException("cause"),
189                         Lists.newArrayList(
190                                 new RestconfError(ErrorType.APPLICATION, ErrorTag.BAD_ATTRIBUTE, "message 1"),
191                                 new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "message 2"))),
192                 mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, Collections.singletonList(
193                         RestconfDocumentedExceptionMapper.YANG_PATCH_XML_TYPE)),
194                 Response.status(Status.BAD_REQUEST)
195                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_XML_TYPE)
196                         .entity("<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n"
197                                 + "<error>\n"
198                                 + "<error-message>message 1</error-message>\n"
199                                 + "<error-tag>bad-attribute</error-tag>\n"
200                                 + "<error-type>application</error-type>\n"
201                                 + "</error>\n"
202                                 + "<error>\n"
203                                 + "<error-message>message 2</error-message>\n"
204                                 + "<error-tag>operation-failed</error-tag>\n"
205                                 + "<error-type>application</error-type>\n"
206                                 + "</error>\n"
207                                 + "</errors>")
208                         .build()
209             },
210             {
211                 "Mapping of the exception with three entries and optional entries set: error app tag (the first error),"
212                         + " error info (the second error), and error path (the last error); JSON output",
213                 sampleComplexError, mockHttpHeaders(MediaType.APPLICATION_JSON_TYPE, Collections.singletonList(
214                         MediaType.APPLICATION_JSON_TYPE)),
215                 Response.status(Status.BAD_REQUEST)
216                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE)
217                         .entity("{\n"
218                                 + "  \"errors\": {\n"
219                                 + "    \"error\": [\n"
220                                 + "      {\n"
221                                 + "        \"error-type\": \"application\",\n"
222                                 + "        \"error-message\": \"message 1\",\n"
223                                 + "        \"error-tag\": \"bad-attribute\",\n"
224                                 + "        \"error-app-tag\": \"app tag #1\"\n"
225                                 + "      },\n"
226                                 + "      {\n"
227                                 + "        \"error-type\": \"application\",\n"
228                                 + "        \"error-message\": \"message 2\",\n"
229                                 + "        \"error-tag\": \"operation-failed\",\n"
230                                 + "        \"error-app-tag\": \"app tag #2\",\n"
231                                 + "        \"error-info\": \"my info\"\n"
232                                 + "      },\n"
233                                 + "      {\n"
234                                 + "        \"error-type\": \"rpc\",\n"
235                                 + "        \"error-path\": \"/instance-identifier-patch-module:patch-cont/"
236                                 + "my-list1[name='sample']/my-leaf12\",\n"
237                                 + "        \"error-message\": \"message 3\",\n"
238                                 + "        \"error-tag\": \"data-missing\",\n"
239                                 + "        \"error-app-tag\": \" app tag #3\",\n"
240                                 + "        \"error-info\": \"my error info\"\n"
241                                 + "      }\n"
242                                 + "    ]\n"
243                                 + "  }\n"
244                                 + "}\n")
245                         .build()
246             },
247             {
248                 "Mapping of the exception with three entries and optional entries set: error app tag (the first error),"
249                         + " error info (the second error), and error path (the last error); XML output",
250                 sampleComplexError, mockHttpHeaders(RestconfDocumentedExceptionMapper.YANG_PATCH_JSON_TYPE,
251                         Collections.singletonList(RestconfDocumentedExceptionMapper.YANG_DATA_XML_TYPE)),
252                 Response.status(Status.BAD_REQUEST)
253                         .type(RestconfDocumentedExceptionMapper.YANG_DATA_XML_TYPE)
254                         .entity("<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n"
255                                 + "<error>\n"
256                                 + "<error-type>application</error-type>\n"
257                                 + "<error-message>message 1</error-message>\n"
258                                 + "<error-tag>bad-attribute</error-tag>\n"
259                                 + "<error-app-tag>app tag #1</error-app-tag>\n"
260                                 + "</error>\n"
261                                 + "<error>\n"
262                                 + "<error-type>application</error-type>\n"
263                                 + "<error-message>message 2</error-message>\n"
264                                 + "<error-tag>operation-failed</error-tag>\n"
265                                 + "<error-app-tag>app tag #2</error-app-tag>\n"
266                                 + "<error-info>my info</error-info></error>\n"
267                                 + "<error>\n"
268                                 + "<error-type>rpc</error-type>\n"
269                                 + "<error-path xmlns:a=\"instance:identifier:patch:module\">/a:patch-cont/"
270                                 + "a:my-list1[a:name='sample']/a:my-leaf12</error-path>\n"
271                                 + "<error-message>message 3</error-message>\n"
272                                 + "<error-tag>data-missing</error-tag>\n"
273                                 + "<error-app-tag> app tag #3</error-app-tag>\n"
274                                 + "<error-info>my error info</error-info>\n"
275                                 + "</error>\n"
276                                 + "</errors>")
277                         .build()
278             }
279         });
280     }
281
282     @Parameter
283     public String testDescription;
284     @Parameter(1)
285     public RestconfDocumentedException thrownException;
286     @Parameter(2)
287     public HttpHeaders httpHeaders;
288     @Parameter(3)
289     public Response expectedResponse;
290
291     @Test
292     public void testMappingOfExceptionToResponse() throws JSONException {
293         exceptionMapper.setHttpHeaders(httpHeaders);
294         final Response response = exceptionMapper.toResponse(thrownException);
295         compareResponseWithExpectation(expectedResponse, response);
296     }
297
298     private static HttpHeaders mockHttpHeaders(final MediaType contentType, final List<MediaType> acceptedTypes) {
299         final HttpHeaders httpHeaders = mock(HttpHeaders.class);
300         doReturn(contentType).when(httpHeaders).getMediaType();
301         doReturn(acceptedTypes).when(httpHeaders).getAcceptableMediaTypes();
302         return httpHeaders;
303     }
304
305     private static void compareResponseWithExpectation(final Response expectedResponse, final Response actualResponse)
306             throws JSONException {
307         final String errorMessage = String.format("Actual response %s doesn't equal to expected response %s",
308                 actualResponse, expectedResponse);
309         Assert.assertEquals(errorMessage, expectedResponse.getStatus(), actualResponse.getStatus());
310         Assert.assertEquals(errorMessage, expectedResponse.getMediaType(), actualResponse.getMediaType());
311         if (RestconfDocumentedExceptionMapper.YANG_DATA_JSON_TYPE.equals(expectedResponse.getMediaType())) {
312             JSONAssert.assertEquals(expectedResponse.getEntity().toString(),
313                     actualResponse.getEntity().toString(), true);
314         } else {
315             final JSONObject expectedResponseInJson = XML.toJSONObject(expectedResponse.getEntity().toString());
316             final JSONObject actualResponseInJson = XML.toJSONObject(actualResponse.getEntity().toString());
317             JSONAssert.assertEquals(expectedResponseInJson, actualResponseInJson, true);
318         }
319     }
320 }