2 * ============LICENSE_START=======================================================
4 * ================================================================================
5 * Copyright (C) 2020-2021 AT&T Intellectual Property. All rights reserved.
6 * Modifications Copyright (C) 2023, 2024 Nordix Foundation.
7 * ================================================================================
8 * Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 * ============LICENSE_END=========================================================
22 package org.onap.policy.controlloop.actorserviceprovider.impl;
24 import static org.assertj.core.api.Assertions.assertThat;
25 import static org.assertj.core.api.Assertions.assertThatCode;
26 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
27 import static org.assertj.core.api.Assertions.assertThatThrownBy;
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.Mockito.when;
36 import jakarta.ws.rs.Consumes;
37 import jakarta.ws.rs.DELETE;
38 import jakarta.ws.rs.GET;
39 import jakarta.ws.rs.POST;
40 import jakarta.ws.rs.PUT;
41 import jakarta.ws.rs.Path;
42 import jakarta.ws.rs.Produces;
43 import jakarta.ws.rs.client.Entity;
44 import jakarta.ws.rs.client.InvocationCallback;
45 import jakarta.ws.rs.core.MediaType;
46 import jakarta.ws.rs.core.Response;
47 import jakarta.ws.rs.core.Response.Status;
48 import java.util.Collections;
50 import java.util.Properties;
51 import java.util.UUID;
52 import java.util.concurrent.CancellationException;
53 import java.util.concurrent.CompletableFuture;
54 import java.util.concurrent.ExecutionException;
55 import java.util.concurrent.Executor;
56 import java.util.concurrent.Future;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.TimeoutException;
59 import java.util.concurrent.atomic.AtomicReference;
62 import org.junit.jupiter.api.AfterAll;
63 import org.junit.jupiter.api.BeforeAll;
64 import org.junit.jupiter.api.BeforeEach;
65 import org.junit.jupiter.api.Test;
66 import org.junit.jupiter.api.TestInstance;
67 import org.junit.jupiter.api.extension.ExtendWith;
68 import org.mockito.Mock;
69 import org.mockito.Mockito;
70 import org.mockito.junit.jupiter.MockitoExtension;
71 import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
72 import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams;
73 import org.onap.policy.common.endpoints.event.comm.bus.internal.BusTopicParams.TopicParamsBuilder;
74 import org.onap.policy.common.endpoints.http.client.HttpClient;
75 import org.onap.policy.common.endpoints.http.client.HttpClientFactory;
76 import org.onap.policy.common.endpoints.http.client.HttpClientFactoryInstance;
77 import org.onap.policy.common.endpoints.http.server.HttpServletServer;
78 import org.onap.policy.common.endpoints.http.server.HttpServletServerFactoryInstance;
79 import org.onap.policy.common.endpoints.properties.PolicyEndPointProperties;
80 import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType;
81 import org.onap.policy.common.gson.GsonMessageBodyHandler;
82 import org.onap.policy.common.utils.coder.CoderException;
83 import org.onap.policy.common.utils.network.NetworkUtil;
84 import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
85 import org.onap.policy.controlloop.actorserviceprovider.OperationResult;
86 import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
87 import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig;
88 import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpParams;
90 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
91 @ExtendWith(MockitoExtension.class)
92 class HttpOperationTest {
94 private static final IllegalStateException EXPECTED_EXCEPTION = new IllegalStateException("expected exception");
95 private static final String ACTOR = "my-actor";
96 private static final String OPERATION = "my-name";
97 private static final String HTTP_CLIENT = "my-client";
98 private static final String HTTP_NO_SERVER = "my-http-no-server-client";
99 private static final String MEDIA_TYPE_APPLICATION_JSON = "application/json";
100 private static final String BASE_URI = "oper";
101 private static final String PATH = "/my-path";
102 private static final String TEXT = "my-text";
103 private static final UUID REQ_ID = UUID.randomUUID();
106 * {@code True} if the server should reject the request, {@code false} otherwise.
108 private static boolean rejectRequest;
110 // call counts of each method type in the server
111 private static int nget;
112 private static int npost;
113 private static int nput;
114 private static int ndelete;
117 private HttpClient client;
119 private HttpClientFactory clientFactory;
121 private Response response;
123 private Executor executor;
125 private ControlLoopOperationParams params;
126 private OperationOutcome outcome;
127 private AtomicReference<InvocationCallback<Response>> callback;
128 private Future<Response> future;
129 private HttpConfig config;
130 private MyGetOperation<String> oper;
133 * Starts the simulator.
136 void setUpBeforeClass() throws Exception {
138 int port = NetworkUtil.allocPort();
141 * Start the simulator. Must use "Properties" to configure it, otherwise the
142 * server will use the wrong serialization provider.
144 Properties svrprops = getServerProperties("my-server", port);
145 HttpServletServerFactoryInstance.getServerFactory().build(svrprops).forEach(HttpServletServer::start);
147 if (!NetworkUtil.isTcpPortOpen("localhost", port, 100, 100)) {
148 HttpServletServerFactoryInstance.getServerFactory().destroy();
149 throw new IllegalStateException("server is not running");
153 * Start the clients, one to the server, and one to a non-existent server.
155 TopicParamsBuilder builder = BusTopicParams.builder().managed(true).hostname("localhost").basePath(BASE_URI);
157 HttpClientFactoryInstance.getClientFactory().build(builder.clientName(HTTP_CLIENT).port(port).build());
159 HttpClientFactoryInstance.getClientFactory()
160 .build(builder.clientName(HTTP_NO_SERVER).port(NetworkUtil.allocPort()).build());
164 * Destroys the Http factories and stops the appender.
167 static void tearDownAfterClass() {
168 HttpClientFactoryInstance.getClientFactory().destroy();
169 HttpServletServerFactoryInstance.getServerFactory().destroy();
173 * Initializes fields, including {@link #oper}, and resets thestatic fields used by
178 rejectRequest = false;
184 Mockito.lenient().when(response.readEntity(String.class)).thenReturn(TEXT);
185 Mockito.lenient().when(response.getStatus()).thenReturn(200);
187 params = ControlLoopOperationParams.builder().actor(ACTOR).operation(OPERATION).requestId(REQ_ID).build();
189 outcome = params.makeOutcome();
191 callback = new AtomicReference<>();
192 future = new CompletableFuture<>();
194 Mockito.lenient().when(clientFactory.get(any())).thenReturn(client);
196 initConfig(HTTP_CLIENT);
198 oper = new MyGetOperation<>(String.class);
202 void testHttpOperator() {
203 assertEquals(ACTOR, oper.getActorName());
204 assertEquals(OPERATION, oper.getName());
205 assertEquals(ACTOR + "." + OPERATION, oper.getFullName());
209 void testMakeHeaders() {
210 assertEquals(Collections.emptyMap(), oper.makeHeaders());
215 assertEquals(PATH, oper.getPath());
221 initRealConfig(HTTP_CLIENT);
223 oper = new MyGetOperation<>(String.class);
225 assertThat(oper.getUrl()).endsWith("/" + BASE_URI + PATH);
229 void testDoConfigureMapOfStringObject_testGetClient_testGetPath_testGetTimeoutMs() {
231 // use value from operator
232 assertEquals(1000L, oper.getTimeoutMs(null));
233 assertEquals(1000L, oper.getTimeoutMs(0));
235 // should use given value
236 assertEquals(20 * 1000L, oper.getTimeoutMs(20));
240 * Tests handleResponse() when it completes.
243 void testHandleResponseComplete() throws Exception {
244 CompletableFuture<OperationOutcome> future2 = oper.handleResponse(outcome, PATH, cb -> {
249 assertFalse(future2.isDone());
250 assertNotNull(callback.get());
251 callback.get().completed(response);
253 assertSame(outcome, future2.get(5, TimeUnit.SECONDS));
254 assertSame(TEXT, outcome.getResponse());
256 assertEquals(OperationResult.SUCCESS, outcome.getResult());
260 * Tests handleResponse() when it fails.
263 void testHandleResponseFailed() {
264 CompletableFuture<OperationOutcome> future2 = oper.handleResponse(outcome, PATH, cb -> {
269 assertFalse(future2.isDone());
270 assertNotNull(callback.get());
271 callback.get().failed(EXPECTED_EXCEPTION);
273 assertThatThrownBy(() -> future2.get(5, TimeUnit.SECONDS)).hasCause(EXPECTED_EXCEPTION);
275 // future and future2 may be completed in parallel so we must wait again
276 assertThatThrownBy(() -> future.get(5, TimeUnit.SECONDS)).isInstanceOf(CancellationException.class);
277 assertTrue(future.isCancelled());
281 * Tests processResponse() when it's a success and the response type is a String.
284 void testProcessResponseSuccessString() throws Exception {
285 CompletableFuture<OperationOutcome> result = oper.processResponse(outcome, PATH, response);
286 assertTrue(result.isDone());
287 assertSame(outcome, result.get());
288 assertEquals(OperationResult.SUCCESS, outcome.getResult());
289 assertSame(TEXT, outcome.getResponse());
293 * Tests processResponse() when it's a failure.
296 void testProcessResponseFailure() throws Exception {
297 when(response.getStatus()).thenReturn(555);
298 CompletableFuture<OperationOutcome> result = oper.processResponse(outcome, PATH, response);
299 assertTrue(result.isDone());
300 assertSame(outcome, result.get());
301 assertEquals(OperationResult.FAILURE, outcome.getResult());
302 assertSame(TEXT, outcome.getResponse());
306 * Tests processResponse() when the decoder succeeds.
309 void testProcessResponseDecodeOk() throws Exception {
310 when(response.readEntity(String.class)).thenReturn("10");
312 MyGetOperation<Integer> oper2 = new MyGetOperation<>(Integer.class);
314 CompletableFuture<OperationOutcome> result = oper2.processResponse(outcome, PATH, response);
315 assertTrue(result.isDone());
316 assertSame(outcome, result.get());
317 assertEquals(OperationResult.SUCCESS, outcome.getResult());
318 assertEquals(Integer.valueOf(10), outcome.getResponse());
322 * Tests processResponse() when the decoder throws an exception.
325 void testProcessResponseDecodeExcept() {
326 MyGetOperation<Integer> oper2 = new MyGetOperation<>(Integer.class);
328 assertThatIllegalArgumentException().isThrownBy(() -> oper2.processResponse(outcome, PATH, response));
332 void testPostProcessResponse() {
333 assertThatCode(() -> oper.postProcessResponse(outcome, PATH, null, null)).doesNotThrowAnyException();
337 void testIsSuccess() {
338 when(response.getStatus()).thenReturn(200);
339 assertTrue(oper.isSuccess(response, null));
341 when(response.getStatus()).thenReturn(555);
342 assertFalse(oper.isSuccess(response, null));
349 void testGet() throws Exception {
351 initRealConfig(HTTP_CLIENT);
353 MyGetOperation<MyResponse> oper2 = new MyGetOperation<>(MyResponse.class);
355 OperationOutcome outcome = runOperation(oper2);
356 assertNotNull(outcome);
357 assertEquals(1, nget);
358 assertEquals(OperationResult.SUCCESS, outcome.getResult());
359 assertTrue(outcome.getResponse() instanceof MyResponse);
366 void testDelete() throws Exception {
368 initRealConfig(HTTP_CLIENT);
370 MyDeleteOperation oper2 = new MyDeleteOperation();
372 OperationOutcome outcome = runOperation(oper2);
373 assertNotNull(outcome);
374 assertEquals(1, ndelete);
375 assertEquals(OperationResult.SUCCESS, outcome.getResult());
376 assertTrue(outcome.getResponse() instanceof String);
383 void testPost() throws Exception {
385 initRealConfig(HTTP_CLIENT);
386 MyPostOperation oper2 = new MyPostOperation();
388 OperationOutcome outcome = runOperation(oper2);
389 assertNotNull(outcome);
390 assertEquals(1, npost);
391 assertEquals(OperationResult.SUCCESS, outcome.getResult());
392 assertTrue(outcome.getResponse() instanceof MyResponse);
399 void testPut() throws Exception {
401 initRealConfig(HTTP_CLIENT);
403 MyPutOperation oper2 = new MyPutOperation();
405 OperationOutcome outcome = runOperation(oper2);
406 assertNotNull(outcome);
407 assertEquals(1, nput);
408 assertEquals(OperationResult.SUCCESS, outcome.getResult());
409 assertTrue(outcome.getResponse() instanceof MyResponse);
413 void testMakeDecoder() {
414 assertNotNull(oper.getCoder());
418 * Gets server properties.
420 * @param name server name
421 * @param port server port
422 * @return server properties
424 private static Properties getServerProperties(String name, int port) {
425 final Properties props = new Properties();
426 props.setProperty(PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES, name);
428 final String svcpfx = PolicyEndPointProperties.PROPERTY_HTTP_SERVER_SERVICES + "." + name;
430 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_REST_CLASSES_SUFFIX, Server.class.getName());
431 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_HOST_SUFFIX, "localhost");
432 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_PORT_SUFFIX, String.valueOf(port));
433 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_MANAGED_SUFFIX, "true");
434 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SWAGGER_SUFFIX, "false");
436 props.setProperty(svcpfx + PolicyEndPointProperties.PROPERTY_HTTP_SERIALIZATION_PROVIDER,
437 GsonMessageBodyHandler.class.getName());
442 * Initializes the configuration.
444 * @param clientName name of the client which it should use
446 private void initConfig(String clientName) {
447 initConfig(clientName, clientFactory);
451 * Initializes the configuration with a real client.
453 * @param clientName name of the client which it should use
455 private void initConfig(String clientName, HttpClientFactory factory) {
456 HttpParams params = HttpParams.builder().clientName(clientName).path(PATH).timeoutSec(1).build();
457 config = new HttpConfig(executor, params, factory);
461 * Initializes the configuration with a real client.
463 * @param clientName name of the client which it should use
465 private void initRealConfig(String clientName) {
466 initConfig(clientName, HttpClientFactoryInstance.getClientFactory());
470 * Runs the operation.
472 * @param operator operator on which to start the operation
473 * @return the outcome of the operation, or {@code null} if it does not complete in
476 private <T> OperationOutcome runOperation(HttpOperation<T> operator)
477 throws InterruptedException, ExecutionException, TimeoutException {
479 CompletableFuture<OperationOutcome> future = operator.start();
481 return future.get(5, TimeUnit.SECONDS);
486 static class MyRequest {
487 private String input = "some input";
492 static class MyResponse {
493 private String output = "some output";
496 private class MyGetOperation<T> extends HttpOperation<T> {
497 MyGetOperation(Class<T> responseClass) {
498 super(HttpOperationTest.this.params, HttpOperationTest.this.config, responseClass, Collections.emptyList());
502 protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) {
503 Map<String, Object> headers = makeHeaders();
505 headers.put("Accept", MediaType.APPLICATION_JSON);
506 String url = getUrl();
508 logMessage(EventType.OUT, CommInfrastructure.REST, url, null);
511 return handleResponse(outcome, url,
512 callback -> getClient().get(callback, getPath(), headers));
517 private class MyPostOperation extends HttpOperation<MyResponse> {
519 super(HttpOperationTest.this.params, HttpOperationTest.this.config, MyResponse.class,
520 Collections.emptyList());
524 protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) {
526 MyRequest request = new MyRequest();
528 Map<String, Object> headers = makeHeaders();
530 headers.put("Accept", MediaType.APPLICATION_JSON);
531 String url = getUrl();
533 String strRequest = prettyPrint(request);
534 logMessage(EventType.OUT, CommInfrastructure.REST, url, strRequest);
536 Entity<String> entity = Entity.entity(strRequest, MediaType.APPLICATION_JSON);
539 return handleResponse(outcome, url,
540 callback -> getClient().post(callback, getPath(), entity, headers));
545 private class MyPutOperation extends HttpOperation<MyResponse> {
547 super(HttpOperationTest.this.params, HttpOperationTest.this.config, MyResponse.class,
548 Collections.emptyList());
552 protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) {
554 MyRequest request = new MyRequest();
556 Map<String, Object> headers = makeHeaders();
558 headers.put("Accept", MediaType.APPLICATION_JSON);
559 String url = getUrl();
561 String strRequest = prettyPrint(request);
562 logMessage(EventType.OUT, CommInfrastructure.REST, url, strRequest);
564 Entity<String> entity = Entity.entity(strRequest, MediaType.APPLICATION_JSON);
567 return handleResponse(outcome, url,
568 callback -> getClient().put(callback, getPath(), entity, headers));
573 private class MyDeleteOperation extends HttpOperation<String> {
574 MyDeleteOperation() {
575 super(HttpOperationTest.this.params, HttpOperationTest.this.config, String.class, Collections.emptyList());
579 protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) {
580 Map<String, Object> headers = makeHeaders();
582 headers.put("Accept", MediaType.APPLICATION_JSON);
583 String url = getUrl();
585 logMessage(EventType.OUT, CommInfrastructure.REST, url, null);
588 return handleResponse(outcome, url,
589 callback -> getClient().delete(callback, getPath(), headers));
597 @Path("/" + BASE_URI)
598 @Produces(MEDIA_TYPE_APPLICATION_JSON)
599 @Consumes(value = {MEDIA_TYPE_APPLICATION_JSON})
600 public static class Server {
603 * Generates a response to a GET.
605 * @return resulting response
609 public Response getRequest() {
613 return Response.status(Status.BAD_REQUEST).build();
616 return Response.status(Status.OK).entity(new MyResponse()).build();
621 * Generates a response to a POST.
623 * @param request incoming request
624 * @return resulting response
628 public Response postRequest(MyRequest request) {
632 return Response.status(Status.BAD_REQUEST).build();
635 return Response.status(Status.OK).entity(new MyResponse()).build();
640 * Generates a response to a PUT.
642 * @param request incoming request
643 * @return resulting response
647 public Response putRequest(MyRequest request) {
651 return Response.status(Status.BAD_REQUEST).build();
654 return Response.status(Status.OK).entity(new MyResponse()).build();
659 * Generates a response to a DELETE.
661 * @return resulting response
665 public Response deleteRequest() {
669 return Response.status(Status.BAD_REQUEST).build();
672 return Response.status(Status.OK).entity(new MyResponse()).build();