Remove the option to deliver streams over WebSockets
[netconf.git] / restconf / restconf-nb / src / test / java / org / opendaylight / restconf / nb / jaxrs / RestconfDataGetTest.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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 package org.opendaylight.restconf.nb.jaxrs;
9
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
12 import static org.junit.jupiter.api.Assertions.assertNotNull;
13 import static org.junit.jupiter.api.Assertions.assertNull;
14 import static org.mockito.ArgumentMatchers.any;
15 import static org.mockito.Mockito.doReturn;
16 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
17
18 import java.util.Optional;
19 import javax.ws.rs.core.MultivaluedHashMap;
20 import org.junit.jupiter.api.BeforeEach;
21 import org.junit.jupiter.api.Test;
22 import org.junit.jupiter.api.extension.ExtendWith;
23 import org.mockito.Mock;
24 import org.mockito.junit.jupiter.MockitoExtension;
25 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
26 import org.opendaylight.mdsal.dom.api.DOMActionService;
27 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
28 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
29 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
30 import org.opendaylight.mdsal.dom.api.DOMRpcService;
31 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
32 import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
33 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
34 import org.opendaylight.yangtools.yang.common.ErrorTag;
35 import org.opendaylight.yangtools.yang.common.ErrorType;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
39 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
40 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
41 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
42 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
43
44 // FIXME: this test suite should be refactored as an AbstractDataBrokerTest without mocks:
45 //          - AbstractRestconfTest.restconf should be replaced with an instance wired to
46 //            AbstractDataBrokerTest.getDomBroker() et al.
47 //          - then each test case should initialize the datastores with test data
48 //          - then each test case should execute the request
49 //        if you are doing this, please structure it so that the infra can be brought down to AbstractRestconfTest and
50 //        reused in Netconf822Test and the like
51 @ExtendWith(MockitoExtension.class)
52 class RestconfDataGetTest extends AbstractRestconfTest {
53     private static final NodeIdentifier PLAYLIST_NID = new NodeIdentifier(PLAYLIST_QNAME);
54     private static final NodeIdentifier LIBRARY_NID = new NodeIdentifier(LIBRARY_QNAME);
55
56     // config contains one child the same as in operational and one additional
57     private static final ContainerNode CONFIG_JUKEBOX = ImmutableNodes.newContainerBuilder()
58             .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
59             .withChild(CONT_PLAYER)
60             .withChild(ImmutableNodes.newContainerBuilder().withNodeIdentifier(LIBRARY_NID).build())
61             .build();
62     // operational contains one child the same as in config and one additional
63     private static final ContainerNode OPER_JUKEBOX = ImmutableNodes.newContainerBuilder()
64             .withNodeIdentifier(new NodeIdentifier(JUKEBOX_QNAME))
65             .withChild(CONT_PLAYER)
66             .withChild(ImmutableNodes.newSystemMapBuilder().withNodeIdentifier(PLAYLIST_NID).build())
67             .build();
68
69     @Mock
70     private DOMDataTreeReadTransaction tx;
71
72     @BeforeEach
73     void beforeEach() {
74         doReturn(tx).when(dataBroker).newReadOnlyTransaction();
75     }
76
77     @Test
78     void testReadData() {
79         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
80         doReturn(immediateFluentFuture(Optional.of(EMPTY_JUKEBOX))).when(tx)
81                 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
82         doReturn(immediateFluentFuture(Optional.empty()))
83                 .when(tx).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
84
85         final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar));
86         assertEquals(EMPTY_JUKEBOX, body.data());
87         assertFormat("""
88             {
89               "example-jukebox:jukebox": {
90                 "player": {
91                   "gap": "0.2"
92                 }
93               }
94             }""", body::formatToJSON, true);
95         assertFormat("""
96             <jukebox xmlns="http://example.com/ns/example-jukebox">
97               <player>
98                 <gap>0.2</gap>
99               </player>
100             </jukebox>""", body::formatToXML, true);
101     }
102
103     @Test
104     void testReadRootData() {
105         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
106         doReturn(immediateFluentFuture(Optional.of(wrapNodeByDataRootContainer(CONFIG_JUKEBOX))))
107                 .when(tx)
108                 .read(LogicalDatastoreType.CONFIGURATION, YangInstanceIdentifier.of());
109         doReturn(immediateFluentFuture(Optional.of(wrapNodeByDataRootContainer(OPER_JUKEBOX))))
110                 .when(tx)
111                 .read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.of());
112
113         final var body = assertNormalizedBody(200, ar -> restconf.dataGET(uriInfo, ar));
114         final var data = assertInstanceOf(ContainerNode.class, body.data());
115         final var rootNodes = data.body();
116         assertEquals(1, rootNodes.size());
117         final var allDataChildren = assertInstanceOf(ContainerNode.class, rootNodes.iterator().next()).body();
118         assertEquals(3, allDataChildren.size());
119
120         assertFormat("""
121             {
122               "example-jukebox:jukebox": {
123                 "player": {
124                   "gap": "0.2"
125                 }
126               }
127             }""", body::formatToJSON, true);
128         assertFormat("""
129             <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
130               <jukebox xmlns="http://example.com/ns/example-jukebox">
131                 <library/>
132                 <player>
133                   <gap>0.2</gap>
134                 </player>
135               </jukebox>
136             </data>""", body::formatToXML, true);
137     }
138
139     private static ContainerNode wrapNodeByDataRootContainer(final DataContainerChild data) {
140         return ImmutableNodes.newContainerBuilder()
141             .withNodeIdentifier(NodeIdentifier.create(SchemaContext.NAME))
142             .withChild(data)
143             .build();
144     }
145
146     /**
147      * Test read data from mount point when both {@link LogicalDatastoreType#CONFIGURATION} and
148      * {@link LogicalDatastoreType#OPERATIONAL} contains the same data and some additional data to be merged.
149      */
150     @Test
151     void testReadDataMountPoint() {
152         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
153         doReturn(immediateFluentFuture(Optional.of(CONFIG_JUKEBOX))).when(tx)
154                 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
155         doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(tx)
156                 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
157
158         doReturn(Optional.of(mountPoint)).when(mountPointService)
159             .getMountPoint(any(YangInstanceIdentifier.class));
160         doReturn(Optional.of(new FixedDOMSchemaService(JUKEBOX_SCHEMA))).when(mountPoint)
161             .getService(DOMSchemaService.class);
162         doReturn(Optional.of(dataBroker)).when(mountPoint).getService(DOMDataBroker.class);
163         doReturn(Optional.of(rpcService)).when(mountPoint).getService(DOMRpcService.class);
164         doReturn(Optional.empty()).when(mountPoint).getService(DOMActionService.class);
165         doReturn(Optional.empty()).when(mountPoint).getService(DOMMountPointService.class);
166         doReturn(Optional.empty()).when(mountPoint).getService(NetconfDataTreeService.class);
167
168         // response must contain all child nodes from config and operational containers merged in one container
169         final var body = assertNormalizedBody(200, ar -> restconf.dataGET(
170             apiPath("example-jukebox:jukebox/yang-ext:mount/example-jukebox:jukebox"), uriInfo, ar));
171         final var data = assertInstanceOf(ContainerNode.class, body.data());
172         assertEquals(3, data.size());
173         assertNotNull(data.childByArg(CONT_PLAYER.name()));
174         assertNotNull(data.childByArg(LIBRARY_NID));
175         assertNotNull(data.childByArg(PLAYLIST_NID));
176
177         assertFormat("""
178             {
179               "example-jukebox:jukebox": {
180                 "player": {
181                   "gap": "0.2"
182                 }
183               }
184             }""", body::formatToJSON, true);
185         assertFormat("""
186             <jukebox xmlns="http://example.com/ns/example-jukebox">
187               <library/>
188               <player>
189                 <gap>0.2</gap>
190               </player>
191             </jukebox>""", body::formatToXML, true);
192     }
193
194     @Test
195     void testReadDataNoData() {
196         doReturn(new MultivaluedHashMap<>()).when(uriInfo).getQueryParameters();
197         doReturn(immediateFluentFuture(Optional.empty()))
198                 .when(tx).read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
199         doReturn(immediateFluentFuture(Optional.empty()))
200                 .when(tx).read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
201
202         final var error = assertError(ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar));
203         assertEquals(ErrorType.PROTOCOL, error.getErrorType());
204         assertEquals(ErrorTag.DATA_MISSING, error.getErrorTag());
205         assertEquals("Request could not be completed because the relevant data model content does not exist",
206             error.getErrorMessage());
207     }
208
209     /**
210      * Read data from config datastore according to content parameter.
211      */
212     @Test
213     void testReadDataConfigTest() {
214         final var parameters = new MultivaluedHashMap<String, String>();
215         parameters.putSingle("content", "config");
216
217         doReturn(parameters).when(uriInfo).getQueryParameters();
218         doReturn(immediateFluentFuture(Optional.of(CONFIG_JUKEBOX))).when(tx)
219                 .read(LogicalDatastoreType.CONFIGURATION, JUKEBOX_IID);
220
221         // response must contain only config data
222         final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar));
223         final var data = assertInstanceOf(ContainerNode.class, body.data());
224         // config data present
225         assertNotNull(data.childByArg(CONT_PLAYER.name()));
226         assertNotNull(data.childByArg(LIBRARY_NID));
227         // state data absent
228         assertNull(data.childByArg(PLAYLIST_NID));
229
230         assertFormat("""
231             {
232               "example-jukebox:jukebox": {
233                 "player": {
234                   "gap": "0.2"
235                 }
236               }
237             }""", body::formatToJSON, true);
238         assertFormat("""
239             <jukebox xmlns="http://example.com/ns/example-jukebox">
240               <library/>
241               <player>
242                 <gap>0.2</gap>
243               </player>
244             </jukebox>""", body::formatToXML, true);
245     }
246
247     /**
248      * Read data from operational datastore according to content parameter.
249      */
250     @Test
251     void testReadDataOperationalTest() {
252         final var parameters = new MultivaluedHashMap<String, String>();
253         parameters.putSingle("content", "nonconfig");
254
255         doReturn(parameters).when(uriInfo).getQueryParameters();
256         doReturn(immediateFluentFuture(Optional.of(OPER_JUKEBOX))).when(tx)
257                 .read(LogicalDatastoreType.OPERATIONAL, JUKEBOX_IID);
258
259         // response must contain only operational data
260         final var body = assertNormalizedBody(200, ar -> restconf.dataGET(JUKEBOX_API_PATH, uriInfo, ar));
261         final var data = assertInstanceOf(ContainerNode.class, body.data());
262         // state data present
263         assertNotNull(data.childByArg(CONT_PLAYER.name()));
264         assertNotNull(data.childByArg(PLAYLIST_NID));
265
266         // config data absent
267         assertNull(data.childByArg(LIBRARY_NID));
268
269         assertFormat("""
270             {
271               "example-jukebox:jukebox": {
272                 "player": {
273                   "gap": "0.2"
274                 }
275               }
276             }""", body::formatToJSON, true);
277         assertFormat("""
278             <jukebox xmlns="http://example.com/ns/example-jukebox">
279               <player>
280                 <gap>0.2</gap>
281               </player>
282             </jukebox>""", body::formatToXML, true);
283     }
284
285     @Test
286     void readListEntry() {
287         final var parameters = new MultivaluedHashMap<String, String>();
288         parameters.putSingle("content", "nonconfig");
289
290         doReturn(parameters).when(uriInfo).getQueryParameters();
291         doReturn(immediateFluentFuture(Optional.of(ImmutableNodes.newMapEntryBuilder()
292             .withNodeIdentifier(NodeIdentifierWithPredicates.of(ARTIST_QNAME, NAME_QNAME, "IAmN0t"))
293             .withChild(ImmutableNodes.leafNode(NAME_QNAME, "IAmN0t"))
294             .build()))).when(tx).read(LogicalDatastoreType.OPERATIONAL, YangInstanceIdentifier.builder()
295                 .node(JUKEBOX_QNAME)
296                 .node(LIBRARY_QNAME)
297                 .node(ARTIST_QNAME)
298                 .nodeWithKey(ARTIST_QNAME, NAME_QNAME, "IAmN0t")
299                 .build());
300
301         final var body = assertNormalizedBody(200,
302             ar -> restconf.dataGET(apiPath("example-jukebox:jukebox/library/artist=IAmN0t"), uriInfo, ar));
303         assertFormat("""
304             {
305               "example-jukebox:artist": [
306                 {
307                   "name": "IAmN0t"
308                 }
309               ]
310             }""", body::formatToJSON, true);
311         assertFormat("""
312             <artist xmlns="http://example.com/ns/example-jukebox">
313               <name>IAmN0t</name>
314             </artist>""", body::formatToXML, true);
315     }
316 }