2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2019, 2021 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2020-2021, 2023 Nordix Foundation.
7 * Modifications Copyright (C) 2022 Bell Canada. All rights reserved.
8 * ================================================================================
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
13 * http://www.apache.org/licenses/LICENSE-2.0
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ============LICENSE_END=========================================================
23 package org.onap.policy.pap.main.comm;
25 import static org.assertj.core.api.Assertions.assertThat;
26 import static org.assertj.core.api.Assertions.assertThatCode;
27 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
28 import static org.junit.jupiter.api.Assertions.assertEquals;
29 import static org.junit.jupiter.api.Assertions.assertFalse;
30 import static org.junit.jupiter.api.Assertions.assertNotNull;
31 import static org.junit.jupiter.api.Assertions.assertSame;
32 import static org.junit.jupiter.api.Assertions.assertTrue;
33 import static org.mockito.ArgumentMatchers.any;
34 import static org.mockito.ArgumentMatchers.eq;
35 import static org.mockito.Mockito.doAnswer;
36 import static org.mockito.Mockito.mock;
37 import static org.mockito.Mockito.never;
38 import static org.mockito.Mockito.times;
39 import static org.mockito.Mockito.verify;
40 import static org.mockito.Mockito.when;
42 import jakarta.ws.rs.core.Response.Status;
43 import java.time.Instant;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.Collection;
47 import java.util.Collections;
48 import java.util.Comparator;
49 import java.util.Iterator;
50 import java.util.List;
51 import java.util.stream.Collectors;
52 import org.assertj.core.api.Assertions;
53 import org.junit.jupiter.api.AfterEach;
54 import org.junit.jupiter.api.BeforeEach;
55 import org.junit.jupiter.api.Test;
56 import org.mockito.ArgumentCaptor;
57 import org.mockito.Captor;
58 import org.mockito.Mock;
59 import org.mockito.MockitoAnnotations;
60 import org.onap.policy.models.base.PfModelException;
61 import org.onap.policy.models.base.PfModelRuntimeException;
62 import org.onap.policy.models.pdp.concepts.Pdp;
63 import org.onap.policy.models.pdp.concepts.PdpGroup;
64 import org.onap.policy.models.pdp.concepts.PdpMessage;
65 import org.onap.policy.models.pdp.concepts.PdpStateChange;
66 import org.onap.policy.models.pdp.concepts.PdpStatus;
67 import org.onap.policy.models.pdp.concepts.PdpSubGroup;
68 import org.onap.policy.models.pdp.concepts.PdpUpdate;
69 import org.onap.policy.models.pdp.enums.PdpState;
70 import org.onap.policy.models.tosca.authorative.concepts.ToscaConceptIdentifier;
71 import org.onap.policy.models.tosca.authorative.concepts.ToscaPolicy;
72 import org.onap.policy.pap.main.comm.msgdata.Request;
73 import org.onap.policy.pap.main.comm.msgdata.RequestListener;
74 import org.onap.policy.pap.main.parameters.PdpModifyRequestMapParams;
75 import org.onap.policy.pap.main.service.PdpGroupService;
76 import org.onap.policy.pap.main.service.PolicyStatusService;
77 import org.springframework.test.util.ReflectionTestUtils;
79 class PdpModifyRequestMapTest extends CommonRequestBase {
80 private static final String MY_REASON = "my reason";
81 private static final int EXPIRED_SECONDS = 100;
84 * Used to capture input to dao.createPdpGroups().
87 private ArgumentCaptor<List<PdpGroup>> createCaptor;
91 * Used to capture input to dao.updatePdpGroups().
94 private ArgumentCaptor<List<PdpGroup>> updateCaptor;
97 * Used to capture input to undeployer.undeploy().
100 private ArgumentCaptor<Collection<ToscaConceptIdentifier>> undeployCaptor;
103 private PdpRequests requests;
106 private PolicyUndeployer undeployer;
109 private PdpStatusMessageHandler responseHandler;
112 private PdpGroupService pdpGroupService;
115 private PolicyStatusService policyStatusService;
118 private PdpUpdate update;
119 private PdpStateChange change;
120 private PdpStatus response;
122 AutoCloseable autoCloseable;
127 * @throws Exception if an error occurs
131 public void setUp() throws Exception {
133 autoCloseable = MockitoAnnotations.openMocks(this);
135 response = new PdpStatus();
137 update = makeUpdate(PDP1, MY_GROUP, MY_SUBGROUP);
138 change = makeStateChange(PDP1, MY_STATE);
140 when(requests.getPdpName()).thenReturn(PDP1);
141 when(requests.isFirstInQueue(any())).thenReturn(true);
143 response.setName(MY_NAME);
144 response.setState(MY_STATE);
145 response.setPdpGroup(update.getPdpGroup());
146 response.setPdpSubgroup(update.getPdpSubgroup());
147 response.setPolicies(Collections.emptyList());
149 map = new MyMap(mapParams);
153 void tearDown() throws Exception {
154 autoCloseable.close();
158 void testPdpModifyRequestMap() {
159 assertSame(mapParams, ReflectionTestUtils.getField(map, "params"));
160 assertSame(lock, ReflectionTestUtils.getField(map, "modifyLock"));
165 assertTrue(map.isEmpty());
167 map.addRequest(change);
168 assertFalse(map.isEmpty());
171 getListener(getSingletons(1).get(0)).success(PDP1, response);
173 assertTrue(map.isEmpty());
174 verify(responseHandler, never()).handlePdpStatus(response);
178 void testStopPublishing() {
179 // try with non-existent PDP
180 map.stopPublishing(PDP1);
182 // now start a PDP and try it
183 map.addRequest(change);
184 map.stopPublishing(PDP1);
185 verify(requests).stopPublishing();
187 // try again - it shouldn't stop publishing again
188 map.stopPublishing(PDP1);
189 verify(requests, times(1)).stopPublishing();
193 void testAddRequestPdpUpdatePdpStateChange_BothNull() {
194 // nulls should be ok
195 Assertions.assertThatCode(() -> map.addRequest(null, null)).doesNotThrowAnyException();
199 void testAddRequestPdpUpdatePdpStateChange_NullUpdate() {
200 map.addRequest(null, change);
202 Request req = getSingletons(1).get(0);
203 assertSame(change, req.getMessage());
204 assertEquals("pdp_1 PdpStateChange", req.getName());
208 void testAddRequestPdpUpdatePdpStateChange_NullStateChange() {
209 map.addRequest(update, null);
211 Request req = getSingletons(1).get(0);
212 assertSame(update, req.getMessage());
213 assertEquals("pdp_1 PdpUpdate", req.getName());
217 * Tests addRequest() when two requests are provided and the second is an "activate"
221 void testAddRequestPdpUpdatePdpStateChange_BothProvided_Active() {
222 change.setState(PdpState.ACTIVE);
223 map.addRequest(update, change);
225 // should have only allocated one request structure
226 assertEquals(1, map.nalloc);
228 // both requests should have been added
229 List<Request> values = getSingletons(2);
231 // update should appear first
232 Request req = values.remove(0);
233 assertSame(update, req.getMessage());
234 assertEquals("pdp_1 PdpUpdate", req.getName());
236 req = values.remove(0);
237 assertSame(change, req.getMessage());
238 assertEquals("pdp_1 PdpStateChange", req.getName());
242 * Tests addRequest() when two requests are provided and the second is "deactivate"
246 void testAddRequestPdpUpdatePdpStateChange_BothProvided_Passive() {
247 change.setState(PdpState.PASSIVE);
248 map.addRequest(update, change);
250 // should have only allocated one request structure
251 assertEquals(1, map.nalloc);
253 // both requests should have been added
254 List<Request> values = getSingletons(2);
256 // state-change should appear first
257 Request req = values.remove(0);
258 assertSame(change, req.getMessage());
259 assertEquals("pdp_1 PdpStateChange", req.getName());
261 req = values.remove(0);
262 assertSame(update, req.getMessage());
263 assertEquals("pdp_1 PdpUpdate", req.getName());
267 void testAddRequestPdpUpdatePdpStateChange() {
269 map.addRequest(null, null);
271 map.addRequest(change);
273 Request req = getSingletons(1).get(0);
274 assertSame(change, req.getMessage());
275 assertEquals("pdp_1 PdpStateChange", req.getName());
277 // broadcast should throw an exception
278 change.setName(null);
279 assertThatIllegalArgumentException().isThrownBy(() -> map.addRequest(change))
280 .withMessageStartingWith("unexpected broadcast message: PdpStateChange");
284 void testAddRequestPdpUpdate() {
286 map.addRequest((PdpUpdate) null);
288 map.addRequest(update);
290 Request req = getSingletons(1).get(0);
291 assertSame(update, req.getMessage());
292 assertEquals("pdp_1 PdpUpdate", req.getName());
294 // broadcast should throw an exception
295 update.setName(null);
296 assertThatIllegalArgumentException().isThrownBy(() -> map.addRequest(update))
297 .withMessageStartingWith("unexpected broadcast message: PdpUpdate");
301 void testAddRequestPdpStateChange() {
303 map.addRequest((PdpStateChange) null);
305 map.addRequest(change);
307 Request req = getSingletons(1).get(0);
308 assertSame(change, req.getMessage());
309 assertEquals("pdp_1 PdpStateChange", req.getName());
311 // broadcast should throw an exception
312 change.setName(null);
313 assertThatIllegalArgumentException().isThrownBy(() -> map.addRequest(change))
314 .withMessageStartingWith("unexpected broadcast message: PdpStateChange");
318 void testAddSingleton() {
319 map.addRequest(change);
320 assertEquals(1, map.nalloc);
322 // should have one singleton
325 // add another request with the same PDP
326 map.addRequest(makeStateChange(PDP1, MY_STATE));
327 assertEquals(1, map.nalloc);
329 // should now have another singleton
333 // add another request with a different PDP
334 map.addRequest(makeStateChange(DIFFERENT, MY_STATE));
336 // should now have another allocation
337 assertEquals(2, map.nalloc);
339 // should now have another singleton
344 void testStartNextRequest_NoMore() {
345 map.addRequest(change);
348 getListener(getSingletons(1).get(0)).success(PDP1, response);
350 verify(responseHandler, never()).handlePdpStatus(response);
353 * the above should have removed the requests so next time should allocate a new
356 map.addRequest(change);
357 assertEquals(2, map.nalloc);
361 void testStartNextRequest_HaveMore() {
362 map.addRequest(update);
363 map.addRequest(change);
365 Request updateReq = getSingletons(2).get(0);
367 // indicate success with the update
368 when(requests.startNextRequest(updateReq)).thenReturn(true);
369 getListener(updateReq).success(PDP1, response);
371 // should be called for the update
372 verify(responseHandler).handlePdpStatus(response);
374 // should have started the next request
375 verify(requests).startNextRequest(updateReq);
378 * requests should still be there, so adding another request should not allocate a
381 map.addRequest(update);
382 assertEquals(1, map.nalloc);
386 void testRemoveExpiredPdps() {
387 PdpGroup group1 = makeGroup(MY_GROUP);
388 group1.setPdpSubgroups(List.of(makeSubGroup(MY_SUBGROUP, PDP1)));
390 PdpGroup group2 = makeGroup(MY_GROUP2);
391 group2.setPdpSubgroups(List.of(makeSubGroup(MY_SUBGROUP, PDP2, PDP3), makeSubGroup(MY_SUBGROUP2, PDP4)));
393 // expire all items in group2's first subgroup
394 Instant expired = Instant.now().minusSeconds(EXPIRED_SECONDS);
395 group2.getPdpSubgroups().get(0).getPdpInstances().forEach(pdp -> pdp.setLastUpdate(expired));
397 when(pdpGroupService.getFilteredPdpGroups(any())).thenReturn(List.of(group1, group2));
400 map.removeExpiredPdps();
402 // should have removed from the group
403 List<PdpGroup> groups = getGroupUpdates();
404 assertThat(groups).hasSize(1);
405 assertThat(groups.get(0)).isSameAs(group2);
406 assertThat(group2.getPdpSubgroups()).hasSize(2);
408 final Iterator<PdpSubGroup> iter = group2.getPdpSubgroups().iterator();
410 PdpSubGroup subgrp = iter.next();
411 assertThat(subgrp.getPdpInstances()).isEmpty();
412 assertThat(subgrp.getCurrentInstanceCount()).isZero();
414 subgrp = iter.next();
415 assertThat(subgrp.getPdpInstances()).hasSize(1);
416 assertThat(subgrp.getCurrentInstanceCount()).isEqualTo(1);
417 assertThat(subgrp.getPdpInstances().get(0).getInstanceId()).isEqualTo(PDP4);
421 void testRemoveExpiredPdps_NothingExpired() {
422 PdpGroup group1 = makeGroup(MY_GROUP);
423 group1.setPdpSubgroups(List.of(makeSubGroup(MY_SUBGROUP, PDP1)));
425 when(pdpGroupService.getFilteredPdpGroups(any())).thenReturn(List.of(group1));
428 map.removeExpiredPdps();
430 verify(pdpGroupService, never()).updatePdpGroups(any());
431 verify(publisher, never()).enqueue(any());
435 void testRemoveExpiredPdps_DaoEx() {
436 when(pdpGroupService.getFilteredPdpGroups(any())).thenThrow(makeRuntimeException());
438 assertThatCode(map::removeExpiredPdps).doesNotThrowAnyException();
442 void testRemoveExpiredPdps_DaoRtEx() {
443 when(pdpGroupService.getFilteredPdpGroups(any())).thenThrow(makeRuntimeException());
445 assertThatCode(map::removeExpiredPdps).doesNotThrowAnyException();
449 void testRemoveFromSubgroup() {
450 PdpGroup group = makeGroup(MY_GROUP);
451 group.setPdpSubgroups(List.of(makeSubGroup(MY_SUBGROUP, PDP1, PDP2, PDP3)));
453 // expire pdp1 and pdp3
454 Instant expired = Instant.now().minusSeconds(EXPIRED_SECONDS);
455 List<Pdp> pdps = group.getPdpSubgroups().get(0).getPdpInstances();
456 pdps.get(0).setLastUpdate(expired);
457 pdps.get(2).setLastUpdate(expired);
458 when(pdpGroupService.getFilteredPdpGroups(any())).thenReturn(List.of(group));
461 map.removeExpiredPdps();
463 // should have removed from the group
464 List<PdpGroup> groups = getGroupUpdates();
465 assertThat(groups).hasSize(1);
466 assertThat(groups.get(0)).isSameAs(group);
467 assertThat(group.getPdpSubgroups()).hasSize(1);
468 assertThat(group.getPdpSubgroups().get(0).getCurrentInstanceCount()).isEqualTo(1);
470 pdps = group.getPdpSubgroups().get(0).getPdpInstances();
471 assertThat(pdps).hasSize(1);
472 assertThat(pdps.get(0).getInstanceId()).isEqualTo(PDP2);
475 protected PfModelException makeException() {
476 return new PfModelException(Status.BAD_REQUEST, "expected exception");
479 protected PfModelRuntimeException makeRuntimeException() {
480 return new PfModelRuntimeException(Status.BAD_REQUEST, "expected exception");
484 void testMakePdpRequests() {
485 // this should invoke the real method without throwing an exception
486 PdpModifyRequestMap reqMap =
487 new PdpModifyRequestMap(pdpGroupService, policyStatusService, responseHandler, undeployer, notifier);
488 reqMap.initialize(mapParams);
489 reqMap.addRequest(change);
491 QueueToken<PdpMessage> token = queue.poll();
492 assertNotNull(token);
493 assertSame(change, token.get());
495 verify(dispatcher).register(eq(change.getRequestId()), any());
496 verify(timers).register(eq(change.getRequestId()), any());
500 void testSingletonListenerFailure() throws Exception {
501 map.addRequest(change);
504 invokeFailureHandler();
506 verify(undeployer, never()).undeploy(any(), any(), any());
507 verify(requests, never()).stopPublishing();
509 // requests should have been removed from the map so this should allocate another
510 map.addRequest(update);
511 assertEquals(2, map.nalloc);
515 * Tests Listener.failure() when something has to be undeployed.
518 void testSingletonListenerFailureUndeploy() throws Exception {
520 ToscaConceptIdentifier ident = new ToscaConceptIdentifier("undeployed", "2.3.4");
521 ToscaPolicy policy = mock(ToscaPolicy.class);
522 when(policy.getIdentifier()).thenReturn(ident);
524 // add some policies to the update
525 update.setPoliciesToBeDeployed(List.of(policy));
527 map.addRequest(update);
530 * Reconfigure the request when undeploy() is called. Also arrange for undeploy()
531 * to throw an exception.
533 Request req = getSingletons(1).get(0);
536 PdpUpdate update2 = new PdpUpdate(update);
537 update2.setPoliciesToBeDeployed(Collections.emptyList());
538 update2.setPoliciesToBeUndeployed(List.of(policy.getIdentifier()));
539 assertTrue(req.reconfigure(update2));
540 throw makeException();
541 }).when(undeployer).undeploy(any(), any(), any());
543 // indicate that all policies failed (because response has no policies)
544 response.setName(PDP1);
545 req.setNotifier(notifier);
546 req.checkResponse(response);
549 invokeFailureHandler();
551 verify(undeployer).undeploy(eq(MY_GROUP), eq(MY_SUBGROUP), undeployCaptor.capture());
552 assertEquals(List.of(ident).toString(), undeployCaptor.getValue().toString());
554 // no effect on the map
555 map.addRequest(update);
556 assertEquals(1, map.nalloc);
560 * Tests Listener.failure() when something has to be undeployed, but the message
564 void testSingletonListenerFailureUndeployMessageUnchanged() throws Exception {
566 ToscaConceptIdentifier ident = new ToscaConceptIdentifier("msg-unchanged", "8.7.6");
567 ToscaPolicy policy = mock(ToscaPolicy.class);
568 when(policy.getIdentifier()).thenReturn(ident);
570 // add some policies to the update
571 update.setPoliciesToBeDeployed(List.of(policy));
573 map.addRequest(update);
575 // indicate that all policies failed (because response has no policies)
576 response.setName(PDP1);
577 Request req = getSingletons(1).get(0);
578 req.setNotifier(notifier);
579 req.checkResponse(response);
582 invokeFailureHandler();
584 verify(undeployer).undeploy(eq(MY_GROUP), eq(MY_SUBGROUP), undeployCaptor.capture());
585 assertEquals(List.of(ident).toString(), undeployCaptor.getValue().toString());
587 // requests should have been removed from the map so this should allocate another
588 map.addRequest(update);
589 assertEquals(2, map.nalloc);
593 void testSingletonListenerSuccess() {
594 map.addRequest(change);
597 invokeSuccessHandler();
599 verify(requests, never()).stopPublishing();
601 // requests should have been removed from the map so this should allocate another
602 map.addRequest(update);
603 assertEquals(2, map.nalloc);
607 void testRequestCompleted_LastRequest() {
608 map.addRequest(change);
611 invokeSuccessHandler();
613 verify(requests, never()).stopPublishing();
615 // requests should have been removed from the map so this should allocate another
616 map.addRequest(update);
617 assertEquals(2, map.nalloc);
621 void testRequestCompleted_NameMismatch() {
622 // use a different name
623 when(requests.getPdpName()).thenReturn(DIFFERENT);
625 map.addRequest(change);
627 // put the PDP in a group
628 PdpGroup group = makeGroup(MY_GROUP);
629 group.setPdpSubgroups(List.of(makeSubGroup(MY_SUBGROUP, PDP1, DIFFERENT)));
631 // invoke the method - with a different name (i.e., PDP1 instead of DIFFERENT)
632 invokeSuccessHandler();
634 verify(requests, never()).stopPublishing();
636 // no effect on the map
637 map.addRequest(update);
638 assertEquals(1, map.nalloc);
641 verify(pdpGroupService, never()).updatePdpGroups(any());
645 void testRequestCompleted_AlreadyStopped() {
646 map.addRequest(change);
648 map.stopPublishing(PDP1);
651 invokeSuccessHandler();
653 // should have called this a second time
654 verify(requests, times(2)).stopPublishing();
656 // requests should have been removed from the map so this should allocate another
657 map.addRequest(update);
658 assertEquals(2, map.nalloc);
662 void testRequestCompleted_NotFirstInQueue() {
663 map.addRequest(change);
665 when(requests.isFirstInQueue(any())).thenReturn(false);
668 invokeSuccessHandler();
670 // should not have called this
671 verify(requests, never()).stopPublishing();
673 // no effect on the map
674 map.addRequest(update);
675 assertEquals(1, map.nalloc);
679 void testSingletonListenerRetryCountExhausted() {
680 final var request = map.addRequest(change);
683 invokeLastRetryHandler(request);
685 verify(requests).stopPublishing();
690 * Invokes the first request's listener.success() method.
692 private void invokeSuccessHandler() {
693 getListener(getSingletons(1).get(0)).success(PDP1, response);
697 * Invokes the first request's listener.failure() method.
699 private void invokeFailureHandler() {
700 getListener(getSingletons(1).get(0)).failure(PDP1, MY_REASON);
704 * Invokes the first request's listener.retryCountExhausted() method.
706 * @param request request whose count was exhausted
708 private void invokeLastRetryHandler(Request request) {
709 getListener(getSingletons(1).get(0)).retryCountExhausted(request);
713 * Gets the singleton requests added to {@link #requests}.
715 * @param count number of singletons expected
716 * @return the singleton requests
718 private List<Request> getSingletons(int count) {
719 ArgumentCaptor<Request> captor = ArgumentCaptor.forClass(Request.class);
721 verify(requests, times(count)).addSingleton(captor.capture());
722 return captor.getAllValues();
726 * Gets the listener from a request.
728 * @param request request of interest
729 * @return the request's listener
731 private RequestListener getListener(Request request) {
732 return (RequestListener) ReflectionTestUtils.getField(request, "listener");
735 private PdpGroup makeGroup(String name) {
736 PdpGroup group = new PdpGroup();
743 private PdpSubGroup makeSubGroup(String pdpType, String... pdpNames) {
744 PdpSubGroup subgroup = new PdpSubGroup();
746 subgroup.setPdpType(pdpType);
747 subgroup.setCurrentInstanceCount(pdpNames.length);
748 subgroup.setPdpInstances(Arrays.stream(pdpNames).map(this::makePdp).collect(Collectors.toList()));
753 private Pdp makePdp(String pdpName) {
755 pdp.setInstanceId(pdpName);
756 pdp.setLastUpdate(Instant.now());
762 * Gets the input to the method.
764 * @return the input that was passed to the dao.updatePdpGroups() method
766 private List<PdpGroup> getGroupUpdates() {
767 verify(pdpGroupService).updatePdpGroups(updateCaptor.capture());
769 return copyList(updateCaptor.getValue());
773 * Copies a list and sorts it by group name.
775 * @param source source list to copy
776 * @return a copy of the source list
778 private List<PdpGroup> copyList(List<PdpGroup> source) {
779 List<PdpGroup> newlst = new ArrayList<>(source);
780 newlst.sort(Comparator.comparing(PdpGroup::getName));
784 private class MyMap extends PdpModifyRequestMap {
786 * Number of times requests were allocated.
788 private int nalloc = 0;
790 public MyMap(PdpModifyRequestMapParams params) {
791 super(pdpGroupService, policyStatusService, responseHandler, undeployer, notifier);
792 super.initialize(params);
797 protected PdpRequests makePdpRequests(String pdpName) {