Migrate restconf to MD-SAL APIs
[netconf.git] / restconf / restconf-nb-bierman02 / src / test / java / org / opendaylight / controller / sal / restconf / impl / test / BrokerFacadeTest.java
1 /*
2  * Copyright (c) 2014, 2015 Brocade Communications 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 package org.opendaylight.controller.sal.restconf.impl.test;
9
10 import static org.junit.Assert.assertEquals;
11 import static org.junit.Assert.assertFalse;
12 import static org.junit.Assert.assertNotNull;
13 import static org.junit.Assert.assertSame;
14 import static org.junit.Assert.assertTrue;
15 import static org.junit.Assert.fail;
16 import static org.mockito.ArgumentMatchers.any;
17 import static org.mockito.ArgumentMatchers.eq;
18 import static org.mockito.Mockito.doReturn;
19 import static org.mockito.Mockito.inOrder;
20 import static org.mockito.Mockito.mock;
21 import static org.mockito.Mockito.times;
22 import static org.mockito.Mockito.verify;
23 import static org.mockito.Mockito.verifyNoMoreInteractions;
24 import static org.mockito.Mockito.when;
25 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateBooleanFluentFuture;
26 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFailedFluentFuture;
27 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateFluentFuture;
28 import static org.opendaylight.yangtools.util.concurrent.FluentFutures.immediateTrueFluentFuture;
29
30 import com.google.common.collect.ImmutableClassToInstanceMap;
31 import com.google.common.collect.Lists;
32 import com.google.common.util.concurrent.FluentFuture;
33 import java.util.Optional;
34 import org.junit.Before;
35 import org.junit.Test;
36 import org.mockito.InOrder;
37 import org.mockito.Mock;
38 import org.mockito.Mockito;
39 import org.mockito.MockitoAnnotations;
40 import org.opendaylight.controller.md.sal.rest.common.TestRestconfUtils;
41 import org.opendaylight.mdsal.common.api.CommitInfo;
42 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
43 import org.opendaylight.mdsal.common.api.ReadFailedException;
44 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
45 import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService;
46 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
47 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction;
48 import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction;
49 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction;
50 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
51 import org.opendaylight.mdsal.dom.api.DOMNotificationService;
52 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
53 import org.opendaylight.mdsal.dom.api.DOMRpcService;
54 import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
55 import org.opendaylight.netconf.sal.restconf.impl.BrokerFacade;
56 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
57 import org.opendaylight.netconf.sal.restconf.impl.PutResult;
58 import org.opendaylight.netconf.sal.streams.listeners.ListenerAdapter;
59 import org.opendaylight.netconf.sal.streams.listeners.NotificationListenerAdapter;
60 import org.opendaylight.netconf.sal.streams.listeners.Notificator;
61 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
62 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
63 import org.opendaylight.restconf.common.errors.RestconfError;
64 import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
65 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
66 import org.opendaylight.restconf.common.patch.PatchContext;
67 import org.opendaylight.restconf.common.patch.PatchStatusContext;
68 import org.opendaylight.restconf.common.util.DataChangeScope;
69 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
70 import org.opendaylight.yangtools.concepts.ListenerRegistration;
71 import org.opendaylight.yangtools.yang.common.QName;
72 import org.opendaylight.yangtools.yang.common.RpcError;
73 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
74 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
75 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
76 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
77 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
78 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
79 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
80
81 /**
82  * Unit tests for BrokerFacade.
83  *
84  * @author Thomas Pantelis
85  */
86 public class BrokerFacadeTest {
87
88     @Mock
89     private DOMDataBroker domDataBroker;
90     @Mock
91     private DOMNotificationService domNotification;
92     @Mock
93     private DOMRpcService mockRpcService;
94     @Mock
95     private DOMMountPoint mockMountInstance;
96     @Mock
97     private DOMDataTreeReadTransaction readTransaction;
98     @Mock
99     private DOMDataTreeWriteTransaction writeTransaction;
100     @Mock
101     private DOMDataTreeReadWriteTransaction rwTransaction;
102
103     private BrokerFacade brokerFacade;
104     private final NormalizedNode<?, ?> dummyNode = createDummyNode("test:module", "2014-01-09", "interfaces");
105     private final FluentFuture<Optional<NormalizedNode<?, ?>>> dummyNodeInFuture = wrapDummyNode(this.dummyNode);
106     private final QName qname = TestUtils.buildQName("interfaces","test:module", "2014-01-09");
107     private final SchemaPath type = SchemaPath.create(true, this.qname);
108     private final YangInstanceIdentifier instanceID = YangInstanceIdentifier.builder().node(this.qname).build();
109     private ControllerContext controllerContext;
110
111     @Before
112     public void setUp() throws Exception {
113         MockitoAnnotations.initMocks(this);
114
115         controllerContext = TestRestconfUtils.newControllerContext(
116                 TestUtils.loadSchemaContext("/full-versions/test-module", "/modules"));
117
118         brokerFacade = BrokerFacade.newInstance(mockRpcService, domDataBroker, domNotification, controllerContext);
119
120         when(this.domDataBroker.newReadOnlyTransaction()).thenReturn(this.readTransaction);
121         when(this.domDataBroker.newWriteOnlyTransaction()).thenReturn(this.writeTransaction);
122         when(this.domDataBroker.newReadWriteTransaction()).thenReturn(this.rwTransaction);
123         when(this.domDataBroker.getExtensions()).thenReturn(ImmutableClassToInstanceMap.of(
124             DOMDataTreeChangeService.class, Mockito.mock(DOMDataTreeChangeService.class)));
125     }
126
127     private static FluentFuture<Optional<NormalizedNode<?, ?>>> wrapDummyNode(final NormalizedNode<?, ?> dummyNode) {
128         return immediateFluentFuture(Optional.of(dummyNode));
129     }
130
131     private static FluentFuture<Boolean> wrapExistence(final boolean exists) {
132         return immediateBooleanFluentFuture(exists);
133     }
134
135     /**
136      * Value of this node shouldn't be important for testing purposes.
137      */
138     private static NormalizedNode<?, ?> createDummyNode(final String namespace, final String date,
139             final String localName) {
140         return Builders.containerBuilder()
141                 .withNodeIdentifier(new NodeIdentifier(QName.create(namespace, date, localName))).build();
142     }
143
144     @Test
145     public void testReadConfigurationData() {
146         when(this.readTransaction.read(any(LogicalDatastoreType.class), any(YangInstanceIdentifier.class))).thenReturn(
147                 this.dummyNodeInFuture);
148
149         final NormalizedNode<?, ?> actualNode = this.brokerFacade.readConfigurationData(this.instanceID);
150
151         assertSame("readConfigurationData", this.dummyNode, actualNode);
152     }
153
154     @Test
155     public void testReadOperationalData() {
156         when(this.readTransaction.read(any(LogicalDatastoreType.class), any(YangInstanceIdentifier.class))).thenReturn(
157                 this.dummyNodeInFuture);
158
159         final NormalizedNode<?, ?> actualNode = this.brokerFacade.readOperationalData(this.instanceID);
160
161         assertSame("readOperationalData", this.dummyNode, actualNode);
162     }
163
164     @Test
165     public void test503() throws Exception {
166         final RpcError error = RpcResultBuilder.newError(
167                 RpcError.ErrorType.TRANSPORT,
168                 ErrorTag.RESOURCE_DENIED.getTagValue(),
169                 "Master is down. Please try again.");
170         doReturn(immediateFailedFluentFuture(new ReadFailedException("Read from transaction failed", error)))
171                 .when(readTransaction).read(any(LogicalDatastoreType.class), any(YangInstanceIdentifier.class));
172         try {
173             brokerFacade.readConfigurationData(this.instanceID, "explicit");
174             fail("This test should fail.");
175         } catch (final RestconfDocumentedException e) {
176             assertEquals("getErrorTag", ErrorTag.RESOURCE_DENIED_TRANSPORT, e.getErrors().get(0).getErrorTag());
177             assertEquals("getErrorType", ErrorType.TRANSPORT, e.getErrors().get(0).getErrorType());
178             assertEquals("getErrorMessage", "Master is down. Please try again.",
179                     e.getErrors().get(0).getErrorMessage());
180         }
181     }
182
183     @Test
184     public void testInvokeRpc() throws Exception {
185         final DOMRpcResult expResult = mock(DOMRpcResult.class);
186         doReturn(immediateFluentFuture(expResult)).when(this.mockRpcService).invokeRpc(this.type, this.dummyNode);
187
188         final FluentFuture<DOMRpcResult> actualFuture = this.brokerFacade.invokeRpc(this.type, this.dummyNode);
189         assertNotNull("Future is null", actualFuture);
190         final DOMRpcResult actualResult = actualFuture.get();
191         assertSame("invokeRpc", expResult, actualResult);
192     }
193
194     @Test
195     public void testCommitConfigurationDataPut() throws Exception {
196         doReturn(CommitInfo.emptyFluentFuture()).when(this.rwTransaction).commit();
197
198         doReturn(immediateFluentFuture(Optional.of(mock(NormalizedNode.class)))).when(this.rwTransaction)
199         .read(LogicalDatastoreType.CONFIGURATION, this.instanceID);
200
201         final PutResult result = this.brokerFacade.commitConfigurationDataPut(mock(SchemaContext.class),
202                 this.instanceID, this.dummyNode, null, null);
203
204         assertSame("commitConfigurationDataPut", CommitInfo.emptyFluentFuture(), result.getFutureOfPutData());
205
206         final InOrder inOrder = inOrder(this.domDataBroker, this.rwTransaction);
207         inOrder.verify(this.domDataBroker).newReadWriteTransaction();
208         inOrder.verify(this.rwTransaction).put(LogicalDatastoreType.CONFIGURATION, this.instanceID, this.dummyNode);
209         inOrder.verify(this.rwTransaction).commit();
210     }
211
212     @Test
213     public void testCommitConfigurationDataPost() {
214         when(this.rwTransaction.exists(LogicalDatastoreType.CONFIGURATION, this.instanceID))
215                 .thenReturn(wrapExistence(false));
216
217         doReturn(CommitInfo.emptyFluentFuture()).when(this.rwTransaction).commit();
218
219         final FluentFuture<? extends CommitInfo> actualFuture = this.brokerFacade
220                 .commitConfigurationDataPost(mock(SchemaContext.class), this.instanceID, this.dummyNode, null, null);
221
222         assertSame("commitConfigurationDataPost", CommitInfo.emptyFluentFuture(), actualFuture);
223
224         final InOrder inOrder = inOrder(this.domDataBroker, this.rwTransaction);
225         inOrder.verify(this.domDataBroker).newReadWriteTransaction();
226         inOrder.verify(this.rwTransaction).exists(LogicalDatastoreType.CONFIGURATION, this.instanceID);
227         inOrder.verify(this.rwTransaction).put(LogicalDatastoreType.CONFIGURATION, this.instanceID, this.dummyNode);
228         inOrder.verify(this.rwTransaction).commit();
229     }
230
231     @Test(expected = RestconfDocumentedException.class)
232     public void testCommitConfigurationDataPostAlreadyExists() {
233         when(this.rwTransaction.exists(eq(LogicalDatastoreType.CONFIGURATION), any(YangInstanceIdentifier.class)))
234                 .thenReturn(immediateTrueFluentFuture());
235         try {
236             // Schema context is only necessary for ensuring parent structure
237             this.brokerFacade.commitConfigurationDataPost((SchemaContext) null, this.instanceID, this.dummyNode, null,
238                     null);
239         } catch (final RestconfDocumentedException e) {
240             assertEquals("getErrorTag", RestconfError.ErrorTag.DATA_EXISTS, e.getErrors().get(0).getErrorTag());
241             throw e;
242         }
243     }
244
245     /**
246      * Positive test of delete operation when data to delete exits. Returned value and order of steps are validated.
247      */
248     @Test
249     public void testCommitConfigurationDataDelete() throws Exception {
250         // assume that data to delete exists
251         prepareDataForDelete(true);
252
253         // expected result
254         doReturn(CommitInfo.emptyFluentFuture()).when(this.rwTransaction).commit();
255
256         // test
257         final FluentFuture<? extends CommitInfo> actualFuture = this.brokerFacade
258                 .commitConfigurationDataDelete(this.instanceID);
259
260         // verify result and interactions
261         assertSame("commitConfigurationDataDelete", CommitInfo.emptyFluentFuture(), actualFuture);
262
263         // check exists, delete, submit
264         final InOrder inOrder = inOrder(this.domDataBroker, this.rwTransaction);
265         inOrder.verify(this.rwTransaction).exists(LogicalDatastoreType.CONFIGURATION, this.instanceID);
266         inOrder.verify(this.rwTransaction).delete(LogicalDatastoreType.CONFIGURATION, this.instanceID);
267         inOrder.verify(this.rwTransaction).commit();
268     }
269
270     /**
271      * Negative test of delete operation when data to delete does not exist. Error 404 should be returned.
272      */
273     @Test
274     public void testCommitConfigurationDataDeleteNoData() throws Exception {
275         // assume that data to delete does not exist
276         prepareDataForDelete(false);
277
278         // try to delete and expect 404 error
279         try {
280             this.brokerFacade.commitConfigurationDataDelete(this.instanceID);
281             fail("Delete operation should fail due to missing data");
282         } catch (final RestconfDocumentedException e) {
283             assertEquals(ErrorType.PROTOCOL, e.getErrors().get(0).getErrorType());
284             assertEquals(ErrorTag.DATA_MISSING, e.getErrors().get(0).getErrorTag());
285             assertEquals(404, e.getErrors().get(0).getErrorTag().getStatusCode());
286         }
287     }
288
289     /**
290      * Prepare conditions to test delete operation. Data to delete exists or does not exist according to value of
291      * {@code assumeDataExists} parameter.
292      * @param assumeDataExists boolean to assume if data exists
293      */
294     private void prepareDataForDelete(final boolean assumeDataExists) {
295         when(this.rwTransaction.exists(LogicalDatastoreType.CONFIGURATION, this.instanceID))
296                 .thenReturn(immediateBooleanFluentFuture(assumeDataExists));
297     }
298
299     @Test
300     public void testRegisterToListenDataChanges() {
301         final ListenerAdapter listener = Notificator.createListener(this.instanceID, "stream",
302                 NotificationOutputType.XML, controllerContext);
303
304         @SuppressWarnings("unchecked")
305         final ListenerRegistration<ListenerAdapter> mockRegistration = mock(ListenerRegistration.class);
306
307         DOMDataTreeChangeService changeService = this.domDataBroker.getExtensions()
308                 .getInstance(DOMDataTreeChangeService.class);
309         DOMDataTreeIdentifier loc = new DOMDataTreeIdentifier(LogicalDatastoreType.CONFIGURATION, this.instanceID);
310         when(changeService.registerDataTreeChangeListener(eq(loc), eq(listener))).thenReturn(mockRegistration);
311
312         this.brokerFacade.registerToListenDataChanges(
313                 LogicalDatastoreType.CONFIGURATION, DataChangeScope.BASE, listener);
314
315         verify(changeService).registerDataTreeChangeListener(loc, listener);
316
317         assertEquals("isListening", true, listener.isListening());
318
319         this.brokerFacade.registerToListenDataChanges(
320                 LogicalDatastoreType.CONFIGURATION, DataChangeScope.BASE, listener);
321         verifyNoMoreInteractions(changeService);
322     }
323
324     /**
325      * Create, register, close and remove notification listener.
326      */
327     @Test
328     public void testRegisterToListenNotificationChanges() throws Exception {
329         // create test notification listener
330         final String identifier = "create-notification-stream/toaster:toastDone";
331         final SchemaPath path = SchemaPath.create(true,
332                 QName.create("http://netconfcentral.org/ns/toaster", "2009-11-20", "toastDone"));
333         Notificator.createNotificationListener(Lists.newArrayList(path), identifier, "XML", controllerContext);
334         final NotificationListenerAdapter listener = Notificator.getNotificationListenerFor(identifier).get(0);
335
336         // mock registration
337         final ListenerRegistration<NotificationListenerAdapter> registration = mock(ListenerRegistration.class);
338         when(this.domNotification.registerNotificationListener(listener, listener.getSchemaPath()))
339                 .thenReturn(registration);
340
341         // test to register listener for the first time
342         this.brokerFacade.registerToListenNotification(listener);
343         assertEquals("Registration was not successful", true, listener.isListening());
344
345         // try to register for the second time
346         this.brokerFacade.registerToListenNotification(listener);
347         assertEquals("Registration was not successful", true, listener.isListening());
348
349         // registrations should be invoked only once
350         verify(this.domNotification, times(1)).registerNotificationListener(listener, listener.getSchemaPath());
351
352         final DOMTransactionChain transactionChain = mock(DOMTransactionChain.class);
353         final DOMDataTreeWriteTransaction wTx = mock(DOMDataTreeWriteTransaction.class);
354         doReturn(CommitInfo.emptyFluentFuture()).when(wTx).commit();
355         when(transactionChain.newWriteOnlyTransaction()).thenReturn(wTx);
356         // close and remove test notification listener
357         listener.close();
358         Notificator.removeListenerIfNoSubscriberExists(listener);
359     }
360
361     /**
362      * Test Patch method on the server with no data.
363      */
364     @Test
365     public void testPatchConfigurationDataWithinTransactionServer() throws Exception {
366         final PatchContext patchContext = mock(PatchContext.class);
367         final InstanceIdentifierContext<?> identifierContext = mock(InstanceIdentifierContext.class);
368
369         when(patchContext.getData()).thenReturn(Lists.newArrayList());
370         doReturn(identifierContext).when(patchContext).getInstanceIdentifierContext();
371
372         // no mount point
373         when(identifierContext.getMountPoint()).thenReturn(null);
374
375         doReturn(CommitInfo.emptyFluentFuture()).when(this.rwTransaction).commit();
376
377         final PatchStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext);
378
379         // assert success
380         assertTrue("Patch operation should be successful on server", status.isOk());
381     }
382
383     /**
384      * Test Patch method on mounted device with no data.
385      */
386     @Test
387     public void testPatchConfigurationDataWithinTransactionMount() throws Exception {
388         final PatchContext patchContext = mock(PatchContext.class);
389         final InstanceIdentifierContext<?> identifierContext = mock(InstanceIdentifierContext.class);
390         final DOMMountPoint mountPoint = mock(DOMMountPoint.class);
391         final DOMDataBroker mountDataBroker = mock(DOMDataBroker.class);
392         final DOMDataTreeReadWriteTransaction transaction = mock(DOMDataTreeReadWriteTransaction.class);
393
394         when(patchContext.getData()).thenReturn(Lists.newArrayList());
395         doReturn(identifierContext).when(patchContext).getInstanceIdentifierContext();
396
397         // return mount point with broker
398         when(identifierContext.getMountPoint()).thenReturn(mountPoint);
399         when(mountPoint.getService(DOMDataBroker.class)).thenReturn(Optional.of(mountDataBroker));
400         when(mountDataBroker.newReadWriteTransaction()).thenReturn(transaction);
401         doReturn(CommitInfo.emptyFluentFuture()).when(transaction).commit();
402
403         final PatchStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext);
404
405         // assert success
406         assertTrue("Patch operation should be successful on mounted device", status.isOk());
407     }
408
409     /**
410      * Negative test for Patch operation when mounted device does not support {@link DOMDataBroker service.}
411      * Patch operation should fail with global error.
412      */
413     @Test
414     public void testPatchConfigurationDataWithinTransactionMountFail() throws Exception {
415         final PatchContext patchContext = mock(PatchContext.class);
416         final InstanceIdentifierContext<?> identifierContext = mock(InstanceIdentifierContext.class);
417         final DOMMountPoint mountPoint = mock(DOMMountPoint.class);
418         final DOMDataBroker mountDataBroker = mock(DOMDataBroker.class);
419         final DOMDataTreeReadWriteTransaction transaction = mock(DOMDataTreeReadWriteTransaction.class);
420
421         when(patchContext.getData()).thenReturn(Lists.newArrayList());
422         doReturn(identifierContext).when(patchContext).getInstanceIdentifierContext();
423         when(identifierContext.getMountPoint()).thenReturn(mountPoint);
424
425         // missing broker on mounted device
426         when(mountPoint.getService(DOMDataBroker.class)).thenReturn(Optional.empty());
427
428         when(mountDataBroker.newReadWriteTransaction()).thenReturn(transaction);
429         doReturn(CommitInfo.emptyFluentFuture()).when(transaction).commit();
430
431         final PatchStatusContext status = this.brokerFacade.patchConfigurationDataWithinTransaction(patchContext);
432
433         // assert not successful operation with error
434         assertNotNull(status.getGlobalErrors());
435         assertEquals(1, status.getGlobalErrors().size());
436         assertEquals(ErrorType.APPLICATION, status.getGlobalErrors().get(0).getErrorType());
437         assertEquals(ErrorTag.OPERATION_FAILED, status.getGlobalErrors().get(0).getErrorTag());
438
439         assertFalse("Patch operation should fail on mounted device without Broker", status.isOk());
440     }
441 }