Bug 6679 - api explorer creates false examples
[netconf.git] / restconf / sal-rest-docgen / src / test / java / org / opendaylight / controller / sal / rest / doc / impl / ApiDocGeneratorTest.java
1 /*
2  * Copyright (c) 2014, 2015 Cisco Systems, Inc. and others.  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
9 package org.opendaylight.controller.sal.rest.doc.impl;
10
11 import static org.junit.Assert.assertEquals;
12 import static org.junit.Assert.assertNotNull;
13 import static org.junit.Assert.assertTrue;
14 import static org.junit.Assert.fail;
15
16 import com.google.common.base.Preconditions;
17 import java.sql.Date;
18 import java.util.Arrays;
19 import java.util.HashSet;
20 import java.util.List;
21 import java.util.Set;
22 import java.util.TreeSet;
23 import javax.ws.rs.core.UriInfo;
24 import org.json.JSONException;
25 import org.json.JSONObject;
26 import org.junit.After;
27 import org.junit.Before;
28 import org.junit.Test;
29 import org.opendaylight.controller.sal.core.api.model.SchemaService;
30 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocGenerator;
31 import org.opendaylight.netconf.sal.rest.doc.swagger.Api;
32 import org.opendaylight.netconf.sal.rest.doc.swagger.ApiDeclaration;
33 import org.opendaylight.netconf.sal.rest.doc.swagger.Operation;
34 import org.opendaylight.netconf.sal.rest.doc.swagger.Parameter;
35 import org.opendaylight.netconf.sal.rest.doc.swagger.Resource;
36 import org.opendaylight.netconf.sal.rest.doc.swagger.ResourceList;
37 import org.opendaylight.yangtools.yang.model.api.Module;
38 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
39
40 /**
41  *
42  */
43 public class ApiDocGeneratorTest {
44
45     public static final String HTTP_HOST = "http://host";
46     private static final String NAMESPACE = "http://netconfcentral.org/ns/toaster2";
47     private static final String STRING_DATE = "2009-11-20";
48     private static final Date DATE = Date.valueOf(STRING_DATE);
49     private static final String NAMESPACE_2 = "http://netconfcentral.org/ns/toaster";
50     private static final Date REVISION_2 = Date.valueOf(STRING_DATE);
51     private ApiDocGenerator generator;
52     private DocGenTestHelper helper;
53     private SchemaContext schemaContext;
54
55     @Before
56     public void setUp() throws Exception {
57         this.generator = new ApiDocGenerator();
58         this.helper = new DocGenTestHelper();
59         this.helper.setUp();
60
61         this.schemaContext = this.helper.getSchemaContext();
62     }
63
64     @After
65     public void after() throws Exception {
66     }
67
68     /**
69      * Method: getApiDeclaration(String module, String revision, UriInfo uriInfo)
70      */
71     @Test
72     public void testGetModuleDoc() throws Exception {
73         Preconditions.checkArgument(this.helper.getModules() != null, "No modules found");
74
75         for (final Module m : this.helper.getSchemaContext().getModules()) {
76             if (m.getQNameModule().getNamespace().toString().equals(NAMESPACE)
77                     && m.getQNameModule().getRevision().equals(DATE)) {
78                 final ApiDeclaration doc = this.generator.getSwaggerDocSpec(m, "http://localhost:8080/restconf", "",
79                         this.schemaContext);
80                 validateToaster(doc);
81                 validateTosterDocContainsModulePrefixes(doc);
82                 validateSwaggerModules(doc);
83                 validateSwaggerApisForPost(doc);
84             }
85         }
86     }
87
88     /**
89      * Validate whether ApiDelcaration contains Apis with concrete path and whether this Apis contain specified POST
90      * operations.
91      */
92     private void validateSwaggerApisForPost(final ApiDeclaration doc) {
93         // two POST URI with concrete schema name in summary
94         final Api lstApi = findApi("/config/toaster2:lst/", doc);
95         assertNotNull("Api /config/toaster2:lst/ wasn't found", lstApi);
96         assertTrue("POST for cont1 in lst is missing",
97                 findOperation(lstApi.getOperations(), "POST", "(config)lstPOST", "toaster2/lst(config)lst1-TOP",
98                         "toaster2/lst(config)cont1-TOP"));
99
100         final Api cont1Api = findApi("/config/toaster2:lst/cont1/", doc);
101         assertNotNull("Api /config/toaster2:lst/cont1/ wasn't found", cont1Api);
102         assertTrue("POST for cont11 in cont1 is missing",
103                 findOperation(cont1Api.getOperations(), "POST", "(config)cont1POST", "toaster2/lst/cont1(config)cont11-TOP",
104                         "toaster2/lst/cont1(config)lst11-TOP"));
105
106         // no POST URI
107         final Api cont11Api = findApi("/config/toaster2:lst/cont1/cont11/", doc);
108         assertNotNull("Api /config/toaster2:lst/cont1/cont11/ wasn't found", cont11Api);
109         assertTrue("POST operation shouldn't be present.", findOperations(cont11Api.getOperations(), "POST").isEmpty());
110
111     }
112
113     /**
114      * Tries to find operation with name {@code operationName} and with summary {@code summary}
115      */
116     private boolean findOperation(final List<Operation> operations, final String operationName, final String type,
117             final String... searchedParameters) {
118         final Set<Operation> filteredOperations = findOperations(operations, operationName);
119         for (final Operation operation : filteredOperations) {
120             if (operation.getType().equals(type)) {
121                 final List<Parameter> parameters = operation.getParameters();
122                 return containAllParameters(parameters, searchedParameters);
123             }
124         }
125         return false;
126     }
127
128     private Set<Operation> findOperations(final List<Operation> operations, final String operationName) {
129         final Set<Operation> filteredOperations = new HashSet<>();
130         for (final Operation operation : operations) {
131             if (operation.getMethod().equals(operationName)) {
132                 filteredOperations.add(operation);
133             }
134         }
135         return filteredOperations;
136     }
137
138     private boolean containAllParameters(final List<Parameter> searchedIns, final String[] searchedWhats) {
139         for (final String searchedWhat : searchedWhats) {
140             boolean parameterFound = false;
141             for (final Parameter searchedIn : searchedIns) {
142                 if (searchedIn.getType().equals(searchedWhat)) {
143                     parameterFound = true;
144                 }
145             }
146             if (!parameterFound) {
147                 return false;
148             }
149         }
150         return true;
151     }
152
153     /**
154      * Tries to find {@code Api} with path {@code path}
155      */
156     private Api findApi(final String path, final ApiDeclaration doc) {
157         for (final Api api : doc.getApis()) {
158             if (api.getPath().equals(path)) {
159                 return api;
160             }
161         }
162         return null;
163     }
164
165     /**
166      * Validates whether doc {@code doc} contains concrete specified models.
167      */
168     private void validateSwaggerModules(final ApiDeclaration doc) {
169         final JSONObject models = doc.getModels();
170         assertNotNull(models);
171         try {
172             final JSONObject configLstTop = models.getJSONObject("toaster2(config)lst-TOP");
173             assertNotNull(configLstTop);
174
175             containsReferences(configLstTop, "lst", "toaster2(config)");
176
177             final JSONObject configLst = models.getJSONObject("toaster2(config)lst");
178             assertNotNull(configLst);
179
180             containsReferences(configLst, "lst1", "toaster2/lst(config)");
181             containsReferences(configLst, "cont1", "toaster2/lst(config)");
182
183             final JSONObject configLst1Top = models.getJSONObject("toaster2/lst(config)lst1-TOP");
184             assertNotNull(configLst1Top);
185
186             containsReferences(configLst1Top, "lst1", "toaster2/lst(config)");
187
188             final JSONObject configLst1 = models.getJSONObject("toaster2/lst(config)lst1");
189             assertNotNull(configLst1);
190
191             final JSONObject configCont1Top = models.getJSONObject("toaster2/lst(config)cont1-TOP");
192             assertNotNull(configCont1Top);
193
194             containsReferences(configCont1Top, "cont1", "toaster2/lst(config)");
195             final JSONObject configCont1 = models.getJSONObject("toaster2/lst(config)cont1");
196             assertNotNull(configCont1);
197
198             containsReferences(configCont1, "cont11", "toaster2/lst/cont1(config)");
199             containsReferences(configCont1, "lst11", "toaster2/lst/cont1(config)");
200
201             final JSONObject configCont11Top = models.getJSONObject("toaster2/lst/cont1(config)cont11-TOP");
202             assertNotNull(configCont11Top);
203
204             containsReferences(configCont11Top, "cont11", "toaster2/lst/cont1(config)");
205             final JSONObject configCont11 = models.getJSONObject("toaster2/lst/cont1(config)cont11");
206             assertNotNull(configCont11);
207
208             final JSONObject configlst11Top = models.getJSONObject("toaster2/lst/cont1(config)lst11-TOP");
209             assertNotNull(configlst11Top);
210
211             containsReferences(configlst11Top, "lst11", "toaster2/lst/cont1(config)");
212             final JSONObject configLst11 = models.getJSONObject("toaster2/lst/cont1(config)lst11");
213             assertNotNull(configLst11);
214         } catch (final JSONException e) {
215             fail("JSONException wasn't expected");
216         }
217
218     }
219
220     /**
221      * Checks whether object {@code mainObject} contains in properties/items key $ref with concrete value.
222      */
223     private void containsReferences(final JSONObject mainObject, final String childObject, final String prefix)
224             throws JSONException {
225         final JSONObject properties = mainObject.getJSONObject("properties");
226         assertNotNull(properties);
227
228         final JSONObject nodeInProperties = properties.getJSONObject(childObject);
229         assertNotNull(nodeInProperties);
230
231         final JSONObject itemsInNodeInProperties = nodeInProperties.getJSONObject("items");
232         assertNotNull(itemsInNodeInProperties);
233
234         final String itemRef = itemsInNodeInProperties.getString("$ref");
235         assertEquals(prefix + childObject, itemRef);
236     }
237
238     @Test
239     public void testEdgeCases() throws Exception {
240         Preconditions.checkArgument(this.helper.getModules() != null, "No modules found");
241
242         for (final Module m : this.helper.getModules()) {
243             if (m.getQNameModule().getNamespace().toString().equals(NAMESPACE_2)
244                     && m.getQNameModule().getRevision().equals(REVISION_2)) {
245                 final ApiDeclaration doc = this.generator.getSwaggerDocSpec(m, "http://localhost:8080/restconf", "",
246                         this.schemaContext);
247                 assertNotNull(doc);
248
249                 // testing bugs.opendaylight.org bug 1290. UnionType model type.
250                 final String jsonString = doc.getModels().toString();
251                 assertTrue(jsonString.contains(
252                         "testUnion\":{\"type\":\"integer or string\",\"required\":false}"));
253             }
254         }
255     }
256
257     @Test
258     public void testRPCsModel() throws Exception {
259         Preconditions.checkArgument(this.helper.getModules() != null, "No modules found");
260
261         for (final Module m : this.helper.getModules()) {
262             if (m.getQNameModule().getNamespace().toString().equals(NAMESPACE_2)
263                     && m.getQNameModule().getRevision().equals(REVISION_2)) {
264                 final ApiDeclaration doc = this.generator.getSwaggerDocSpec(m, "http://localhost:8080/restconf", "",
265                         this.schemaContext);
266                 assertNotNull(doc);
267
268                 final JSONObject models = doc.getModels();
269                 final JSONObject inputTop = models.getJSONObject("(make-toast)input-TOP");
270                 final String testString = "{\"input\":{\"type\":\"object\",\"items\":{\"$ref\":\"(make-toast)input\"}}}";
271                 assertEquals(testString, inputTop.getJSONObject("properties").toString());
272                 final JSONObject input = models.getJSONObject("(make-toast)input");
273                 final JSONObject properties = input.getJSONObject("properties");
274                 assertTrue(properties.has("toasterDoneness"));
275                 assertTrue(properties.has("toasterToastType"));
276             }
277         }
278     }
279
280     /**
281      * Tests whether from yang files are generated all required paths for HTTP operations (GET, DELETE, PUT, POST)
282      *
283      * If container | list is augmented then in path there should be specified module name followed with collon (e. g.
284      * "/config/module1:element1/element2/module2:element3")
285      *
286      * @param doc
287      * @throws Exception
288      */
289     private void validateToaster(final ApiDeclaration doc) throws Exception {
290         final Set<String> expectedUrls = new TreeSet<>(Arrays.asList(new String[] { "/config/toaster2:toaster/",
291                 "/operational/toaster2:toaster/", "/operations/toaster2:cancel-toast",
292                 "/operations/toaster2:make-toast", "/operations/toaster2:restock-toaster",
293                 "/config/toaster2:toaster/toasterSlot/{slotId}/toaster-augmented:slotInfo/" }));
294
295         final Set<String> actualUrls = new TreeSet<>();
296
297         Api configApi = null;
298         for (final Api api : doc.getApis()) {
299             actualUrls.add(api.getPath());
300             if (api.getPath().contains("/config/toaster2:toaster/")) {
301                 configApi = api;
302             }
303         }
304
305         boolean containsAll = actualUrls.containsAll(expectedUrls);
306         if (!containsAll) {
307             expectedUrls.removeAll(actualUrls);
308             fail("Missing expected urls: " + expectedUrls);
309         }
310
311         final Set<String> expectedConfigMethods = new TreeSet<>(Arrays.asList(new String[] { "GET", "PUT", "DELETE" }));
312         final Set<String> actualConfigMethods = new TreeSet<>();
313         for (final Operation oper : configApi.getOperations()) {
314             actualConfigMethods.add(oper.getMethod());
315         }
316
317         containsAll = actualConfigMethods.containsAll(expectedConfigMethods);
318         if (!containsAll) {
319             expectedConfigMethods.removeAll(actualConfigMethods);
320             fail("Missing expected method on config API: " + expectedConfigMethods);
321         }
322
323         // TODO: we should really do some more validation of the
324         // documentation...
325         /**
326          * Missing validation: Explicit validation of URLs, and their methods Input / output models.
327          */
328     }
329
330     @Test
331     public void testGetResourceListing() throws Exception {
332         final UriInfo info = this.helper.createMockUriInfo(HTTP_HOST);
333         final SchemaService mockSchemaService = this.helper.createMockSchemaService(this.schemaContext);
334
335         this.generator.setSchemaService(mockSchemaService);
336
337         final ResourceList resourceListing = this.generator.getResourceListing(info);
338
339         Resource toaster = null;
340         Resource toaster2 = null;
341         for (final Resource r : resourceListing.getApis()) {
342             final String path = r.getPath();
343             if (path.contains("toaster2")) {
344                 toaster2 = r;
345             } else if (path.contains("toaster")) {
346                 toaster = r;
347             }
348         }
349
350         assertNotNull(toaster2);
351         assertNotNull(toaster);
352
353         assertEquals(HTTP_HOST + "/toaster(2009-11-20)", toaster.getPath());
354         assertEquals(HTTP_HOST + "/toaster2(2009-11-20)", toaster2.getPath());
355     }
356
357     private void validateTosterDocContainsModulePrefixes(final ApiDeclaration doc) {
358         final JSONObject topLevelJson = doc.getModels();
359         try {
360             final JSONObject configToaster = topLevelJson.getJSONObject("toaster2(config)toaster");
361             assertNotNull("(config)toaster JSON object missing", configToaster);
362             // without module prefix
363             containsProperties(configToaster, "toasterSlot");
364
365             final JSONObject toasterSlot = topLevelJson.getJSONObject("toaster2/toaster(config)toasterSlot");
366             assertNotNull("(config)toasterSlot JSON object missing", toasterSlot);
367             // with module prefix
368             containsProperties(toasterSlot, "toaster-augmented:slotInfo");
369
370         } catch (final JSONException e) {
371             fail("Json exception while reading JSON object. Original message " + e.getMessage());
372         }
373     }
374
375     private void containsProperties(final JSONObject jsonObject, final String... properties) throws JSONException {
376         for (final String property : properties) {
377             final JSONObject propertiesObject = jsonObject.getJSONObject("properties");
378             assertNotNull("Properties object missing in ", propertiesObject);
379             final JSONObject concretePropertyObject = propertiesObject.getJSONObject(property);
380             assertNotNull(property + " is missing", concretePropertyObject);
381         }
382     }
383 }