Add SO VF Module Delete Operation 22/102922/10
authorJim Hahn <jrh3@att.com>
Wed, 4 Mar 2020 02:02:13 +0000 (21:02 -0500)
committerJim Hahn <jrh3@att.com>
Thu, 5 Mar 2020 20:42:01 +0000 (15:42 -0500)
Redesigned the SO Operation classes; moved some code from the subclass
to the superclass so it could be reused by the VF Module Delete Operation.
JerseyClient does not support DELETE with a request body, so had to
implement a delete() method using java11 HttpClient.
Fix some issues found while testing with drools-apps.
Added "delete" operation to SO simulator.

Issue-ID: POLICY-2371
Signed-off-by: Jim Hahn <jrh3@att.com>
Change-Id: I269fe13cf90c295ec2bbac92bc5a59b3820ea265

15 files changed:
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java [new file with mode: 0644]
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java
models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java [new file with mode: 0644]
models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/BasicSoOperation.java
models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/RestManagerResponseTest.java [new file with mode: 0644]
models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/SoActorServiceProviderTest.java
models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleCreateTest.java
models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleDeleteTest.java [new file with mode: 0644]
models-interactions/model-actors/actor.so/src/test/resources/VfModuleDelete.json [new file with mode: 0644]
models-interactions/model-actors/actor.so/src/test/resources/service.yaml
models-interactions/model-actors/actor.test/src/main/java/org/onap/policy/controlloop/actor/test/BasicHttpOperation.java
models-interactions/model-simulators/src/main/java/org/onap/policy/simulators/SoSimulatorJaxRs.java
models-interactions/model-simulators/src/test/java/org/onap/policy/simulators/SoSimulatorTest.java

diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java
new file mode 100644 (file)
index 0000000..1b49ab5
--- /dev/null
@@ -0,0 +1,199 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.so;
+
+import java.lang.annotation.Annotation;
+import java.net.URI;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import javax.ws.rs.core.EntityTag;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.Link.Builder;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.NewCookie;
+import javax.ws.rs.core.Response;
+import lombok.Getter;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+
+/**
+ * RestManager Response suitable for use with subclasses of HttpOperation. Only a couple
+ * of methods are implemented; the rest throw {@link UnsupportedOperationException}.
+ */
+public class RestManagerResponse extends Response {
+    // TODO move to actorServices
+
+    @Getter
+    private final int status;
+
+    private final String body;
+    private final Coder coder;
+
+    /**
+     * Constructs the object.
+     *
+     * @param status HTTP response status code
+     * @param body response body
+     * @param coder coder to decode the entity body
+     */
+    public RestManagerResponse(int status, String body, Coder coder) {
+        this.status = status;
+        this.body = body;
+        this.coder = coder;
+    }
+
+    @Override
+    public void close() {
+        // do nothing
+    }
+
+    @Override
+    public <T> T readEntity(Class<T> entityType) {
+        if (entityType == String.class) {
+            return entityType.cast(body);
+        }
+
+        try {
+            return coder.decode(body, entityType);
+        } catch (CoderException e) {
+            throw new IllegalArgumentException("cannot decode response", e);
+        }
+    }
+
+    @Override
+    public <T> T readEntity(GenericType<T> entityType) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> T readEntity(Class<T> entityType, Annotation[] annotations) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <T> T readEntity(GenericType<T> entityType, Annotation[] annotations) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public StatusType getStatusInfo() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Object getEntity() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasEntity() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean bufferEntity() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public MediaType getMediaType() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Locale getLanguage() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getLength() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<String> getAllowedMethods() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Map<String, NewCookie> getCookies() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public EntityTag getEntityTag() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Date getDate() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Date getLastModified() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public URI getLocation() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<Link> getLinks() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean hasLink(String relation) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Link getLink(String relation) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Builder getLinkBuilder(String relation) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public MultivaluedMap<String, Object> getMetadata() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public MultivaluedMap<String, String> getStringHeaders() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getHeaderString(String name) {
+        throw new UnsupportedOperationException();
+    }
+}
index 9c9e6dc..e4b9c14 100644 (file)
@@ -99,6 +99,7 @@ public class SoActorServiceProvider extends HttpActor<SoActorParams> {
         super(NAME, SoActorParams.class);
 
         addOperator(new SoOperator(NAME, VfModuleCreate.NAME, VfModuleCreate::new));
+        addOperator(new SoOperator(NAME, VfModuleDelete.NAME, VfModuleDelete::new));
     }
 
     // TODO old code: remove lines down to **HERE**
index 41ecd07..1ca6c73 100644 (file)
 package org.onap.policy.controlloop.actor.so;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import lombok.Getter;
 import org.onap.aai.domain.yang.CloudRegion;
@@ -46,6 +48,7 @@ import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOp
 import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig;
 import org.onap.policy.controlloop.policy.PolicyResult;
 import org.onap.policy.controlloop.policy.Target;
+import org.onap.policy.so.SoCloudConfiguration;
 import org.onap.policy.so.SoModelInfo;
 import org.onap.policy.so.SoRequest;
 import org.onap.policy.so.SoRequestInfo;
@@ -63,6 +66,7 @@ public abstract class SoOperation extends HttpOperation<SoResponse> {
     private static final Logger logger = LoggerFactory.getLogger(SoOperation.class);
     private static final Coder coder = new StandardCoder();
 
+    public static final String PAYLOAD_KEY_VF_COUNT = "vfCount";
     public static final String FAILED = "FAILED";
     public static final String COMPLETE = "COMPLETE";
     public static final int SO_RESPONSE_CODE = 999;
@@ -208,7 +212,8 @@ public abstract class SoOperation extends HttpOperation<SoResponse> {
         // still incomplete
 
         // need a request ID with which to query
-        if (response.getRequestReferences() == null || response.getRequestReferences().getRequestId() == null) {
+        if (response == null || response.getRequestReferences() == null
+                        || response.getRequestReferences().getRequestId() == null) {
             throw new IllegalArgumentException("missing request ID in response");
         }
 
@@ -371,6 +376,31 @@ public abstract class SoOperation extends HttpOperation<SoResponse> {
         }
     }
 
+    /**
+     * Construct cloudConfiguration for the SO requestDetails. Overridden for custom
+     * query.
+     *
+     * @param tenantItem tenant item from A&AI named-query response
+     * @return SO cloud configuration
+     */
+    protected SoCloudConfiguration constructCloudConfigurationCq(Tenant tenantItem, CloudRegion cloudRegionItem) {
+        SoCloudConfiguration cloudConfiguration = new SoCloudConfiguration();
+        cloudConfiguration.setTenantId(tenantItem.getTenantId());
+        cloudConfiguration.setLcpCloudRegionId(cloudRegionItem.getCloudRegionId());
+        return cloudConfiguration;
+    }
+
+    /**
+     * Create simple HTTP headers for unauthenticated requests to SO.
+     *
+     * @return the HTTP headers
+     */
+    protected Map<String, Object> createSimpleHeaders() {
+        Map<String, Object> headers = new HashMap<>();
+        headers.put("Accept", MediaType.APPLICATION_JSON);
+        return headers;
+    }
+
     /*
      * These methods extract data from the Custom Query and throw an
      * IllegalArgumentException if the desired data item is not found.
index 4c35f9a..077c857 100644 (file)
@@ -36,7 +36,6 @@ import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType;
 import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
 import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
 import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig;
-import org.onap.policy.so.SoCloudConfiguration;
 import org.onap.policy.so.SoModelInfo;
 import org.onap.policy.so.SoOperationType;
 import org.onap.policy.so.SoRelatedInstance;
@@ -50,13 +49,11 @@ import org.onap.policy.so.SoRequestParameters;
  * response and stores it in the context. It also passes the count+1 to the guard. Once
  * the "create" completes successfully, it bumps the VF count that's stored in the
  * context.
- * <p/>
- * Note: currently, this only supports storing the count for a single target VF.
  */
 public class VfModuleCreate extends SoOperation {
     public static final String NAME = "VF Module Create";
 
-    public static final String PAYLOAD_KEY_VF_COUNT = "vfCount";
+    private static final String PATH_PREFIX = "/";
 
     /**
      * Constructs the object.
@@ -72,7 +69,7 @@ public class VfModuleCreate extends SoOperation {
     }
 
     /**
-     * Ensures that A&AI customer query has been performed, and then runs the guard.
+     * Ensures that A&AI custom query has been performed, and then runs the guard.
      */
     @Override
     @SuppressWarnings("unchecked")
@@ -115,7 +112,9 @@ public class VfModuleCreate extends SoOperation {
 
         logMessage(EventType.OUT, CommInfrastructure.REST, url, request);
 
-        return handleResponse(outcome, url, callback -> getClient().post(callback, path, entity, null));
+        Map<String, Object> headers = createSimpleHeaders();
+
+        return handleResponse(outcome, url, callback -> getClient().post(callback, path, entity, headers));
     }
 
     /**
@@ -206,23 +205,9 @@ public class VfModuleCreate extends SoOperation {
         buildConfigurationParameters().ifPresent(request.getRequestDetails()::setConfigurationParameters);
 
         // compute the path
-        String path = "/serviceInstances/" + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId()
+        String path = PATH_PREFIX + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId()
                         + "/vfModules/scaleOut";
 
         return Pair.of(path, request);
     }
-
-    /**
-     * Construct cloudConfiguration for the SO requestDetails. Overridden for custom
-     * query.
-     *
-     * @param tenantItem tenant item from A&AI named-query response
-     * @return SO cloud configuration
-     */
-    private SoCloudConfiguration constructCloudConfigurationCq(Tenant tenantItem, CloudRegion cloudRegionItem) {
-        SoCloudConfiguration cloudConfiguration = new SoCloudConfiguration();
-        cloudConfiguration.setTenantId(tenantItem.getTenantId());
-        cloudConfiguration.setLcpCloudRegionId(cloudRegionItem.getCloudRegionId());
-        return cloudConfiguration;
-    }
 }
diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java
new file mode 100644 (file)
index 0000000..5134d58
--- /dev/null
@@ -0,0 +1,288 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.so;
+
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.CompletableFuture;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onap.aai.domain.yang.CloudRegion;
+import org.onap.aai.domain.yang.GenericVnf;
+import org.onap.aai.domain.yang.ServiceInstance;
+import org.onap.aai.domain.yang.Tenant;
+import org.onap.policy.aai.AaiConstants;
+import org.onap.policy.aai.AaiCqResponse;
+import org.onap.policy.common.endpoints.event.comm.Topic.CommInfrastructure;
+import org.onap.policy.common.endpoints.http.client.HttpClient;
+import org.onap.policy.common.endpoints.utils.NetLoggerUtil.EventType;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig;
+import org.onap.policy.controlloop.actorserviceprovider.pipeline.PipelineControllerFuture;
+import org.onap.policy.so.SoModelInfo;
+import org.onap.policy.so.SoOperationType;
+import org.onap.policy.so.SoRequest;
+import org.onap.policy.so.SoRequestDetails;
+
+/**
+ * Operation to delete a VF Module. This gets the VF count from the A&AI Custom Query
+ * response and stores it in the context. It also passes the count-1 to the guard. Once
+ * the "delete" completes successfully, it decrements the VF count that's stored in the
+ * context.
+ */
+public class VfModuleDelete extends SoOperation {
+    public static final String NAME = "VF Module Delete";
+
+    private static final String PATH_PREFIX = "/";
+
+    /**
+     * Constructs the object.
+     *
+     * @param params operation parameters
+     * @param config configuration for this operation
+     */
+    public VfModuleDelete(ControlLoopOperationParams params, HttpConfig config) {
+        super(params, config);
+
+        // ensure we have the necessary parameters
+        validateTarget();
+    }
+
+    /**
+     * Ensures that A&AI custom query has been performed, and then runs the guard.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected CompletableFuture<OperationOutcome> startPreprocessorAsync() {
+
+        // need the VF count
+        ControlLoopOperationParams cqParams = params.toBuilder().actor(AaiConstants.ACTOR_NAME)
+                        .operation(AaiCqResponse.OPERATION).payload(null).retry(null).timeoutSec(null).build();
+
+        // run Custom Query, extract the VF count, and then run the Guard
+
+        // @formatter:off
+        return sequence(() -> params.getContext().obtain(AaiCqResponse.CONTEXT_KEY, cqParams),
+                        this::obtainVfCount, this::startGuardAsync);
+        // @formatter:on
+    }
+
+    @Override
+    protected Map<String, Object> makeGuardPayload() {
+        Map<String, Object> payload = super.makeGuardPayload();
+
+        // run guard with the proposed vf count
+        payload.put(PAYLOAD_KEY_VF_COUNT, getVfCount() - 1);
+
+        return payload;
+    }
+
+    @Override
+    protected CompletableFuture<OperationOutcome> startOperationAsync(int attempt, OperationOutcome outcome) {
+
+        // starting a whole new attempt - reset the count
+        resetGetCount();
+
+        Pair<String, SoRequest> pair = makeRequest();
+        SoRequest request = pair.getRight();
+        String url = getPath() + pair.getLeft();
+
+        logMessage(EventType.OUT, CommInfrastructure.REST, url, request);
+
+        Map<String, Object> headers = createSimpleHeaders();
+
+        // @formatter:off
+        return handleResponse(outcome, url,
+            callback -> delete(url, headers, MediaType.APPLICATION_JSON, request, callback));
+        // @formatter:on
+    }
+
+    /**
+     * Issues an HTTP "DELETE" request, containing a request body, using the java built-in
+     * HttpClient, as the JerseyClient does not support it. This will add the content-type
+     * and authorization headers, so they should not be included within "headers".
+     *
+     * @param <Q> request type
+     * @param uri URI suffix, to be appended to the URI prefix
+     * @param headers headers to be included
+     * @param contentType content type of the request
+     * @param request request to be posted
+     * @param callback response callbacks
+     * @return a future to await the response. Note: it's untested whether canceling this
+     *         future will actually cancel the underlying HTTP request
+     */
+    protected <Q> CompletableFuture<Response> delete(String uri, Map<String, Object> headers, String contentType,
+                    Q request, InvocationCallback<Response> callback) {
+        // TODO move to HttpOperation
+
+        // make sure we can encode it before going any further
+        final String body = encodeRequest(request);
+
+        final String url = getClient().getBaseUrl() + uri;
+
+        Builder builder = HttpRequest.newBuilder(URI.create(url));
+        builder = builder.header("Content-type", contentType);
+        builder = addAuthHeader(builder);
+
+        for (Entry<String, Object> header : headers.entrySet()) {
+            builder = builder.header(header.getKey(), header.getValue().toString());
+        }
+
+        PipelineControllerFuture<Response> controller = new PipelineControllerFuture<>();
+
+        HttpRequest req = builder.method("DELETE", BodyPublishers.ofString(body)).build();
+
+        CompletableFuture<HttpResponse<String>> future = makeHttpClient().sendAsync(req, BodyHandlers.ofString());
+
+        // propagate "cancel" to the future
+        controller.add(future);
+
+        future.thenApply(response -> new RestManagerResponse(response.statusCode(), response.body(), makeCoder()))
+                        .whenComplete((resp, thrown) -> {
+                            if (thrown != null) {
+                                callback.failed(thrown);
+                                controller.completeExceptionally(thrown);
+                            } else {
+                                callback.completed(resp);
+                                controller.complete(resp);
+                            }
+                        });
+
+        return controller;
+    }
+
+    /**
+     * Encodes a request.
+     *
+     * @param <Q> request type
+     * @param request request to be encoded
+     * @return the encoded request
+     */
+    protected <Q> String encodeRequest(Q request) {
+        // TODO move to HttpOperation
+        try {
+            if (request instanceof String) {
+                return request.toString();
+            } else {
+                return makeCoder().encode(request);
+            }
+        } catch (CoderException e) {
+            throw new IllegalArgumentException("cannot encode request", e);
+        }
+    }
+
+    /**
+     * Adds the authorization header to the HTTP request, if configured.
+     *
+     * @param builder request builder to which the header should be added
+     * @return the builder
+     */
+    protected Builder addAuthHeader(Builder builder) {
+        // TODO move to HttpOperation
+        final HttpClient client = getClient();
+        String username = client.getUserName();
+        if (StringUtils.isBlank(username)) {
+            return builder;
+        }
+
+        String password = client.getPassword();
+        if (password == null) {
+            password = "";
+        }
+
+        String encoded = username + ":" + password;
+        encoded = Base64.getEncoder().encodeToString(encoded.getBytes(StandardCharsets.UTF_8));
+        return builder.header("Authorization", "Basic " + encoded);
+    }
+
+    /**
+     * Decrements the VF count that's stored in the context.
+     */
+    @Override
+    protected void successfulCompletion() {
+        setVfCount(getVfCount() - 1);
+    }
+
+    /**
+     * Makes a request.
+     *
+     * @return a pair containing the request URL and the new request
+     */
+    protected Pair<String, SoRequest> makeRequest() {
+        final AaiCqResponse aaiCqResponse = params.getContext().getProperty(AaiCqResponse.CONTEXT_KEY);
+        final SoModelInfo soModelInfo = prepareSoModelInfo();
+        final GenericVnf vnfItem = getVnfItem(aaiCqResponse, soModelInfo);
+        final ServiceInstance vnfServiceItem = getServiceInstance(aaiCqResponse);
+        final Tenant tenantItem = getDefaultTenant(aaiCqResponse);
+        final CloudRegion cloudRegionItem = getDefaultCloudRegion(aaiCqResponse);
+
+        SoRequest request = new SoRequest();
+        request.setOperationType(SoOperationType.DELETE_VF_MODULE);
+
+        //
+        //
+        // Do NOT send SO the requestId, they do not support this field
+        //
+        SoRequestDetails details = new SoRequestDetails();
+        request.setRequestDetails(details);
+        details.setRelatedInstanceList(null);
+        details.setConfigurationParameters(null);
+
+        // cloudConfiguration
+        details.setCloudConfiguration(constructCloudConfigurationCq(tenantItem, cloudRegionItem));
+
+        // modelInfo
+        details.setModelInfo(soModelInfo);
+
+        // requestInfo
+        details.setRequestInfo(constructRequestInfo());
+
+        /*
+         * TODO the legacy SO code always passes null for the last argument, though it
+         * should be passing the vfModuleInstanceId
+         */
+
+        // compute the path
+        String path = PATH_PREFIX + vnfServiceItem.getServiceInstanceId() + "/vnfs/" + vnfItem.getVnfId()
+                        + "/vfModules/null";
+
+        return Pair.of(path, request);
+    }
+
+    // these may be overridden by junit tests
+
+    protected java.net.http.HttpClient makeHttpClient() {
+        return java.net.http.HttpClient.newHttpClient();
+    }
+}
index 35f1ef8..f33d501 100644 (file)
@@ -51,6 +51,7 @@ public abstract class BasicSoOperation extends BasicHttpOperation<SoRequest> {
     public static final String MODEL_VERSION = "my-model-version";
     public static final String MODEL_VERS_ID = "my-model-version-id";
     public static final String SUBSCRIPTION_SVC_TYPE = "my-subscription-service-type";
+    public static final String MY_PATH = "my-path";
     public static final String PATH_GET = "my-path-get/";
     public static final int MAX_GETS = 3;
     public static final int WAIT_SEC_GETS = 20;
@@ -108,6 +109,7 @@ public abstract class BasicSoOperation extends BasicHttpOperation<SoRequest> {
     protected void initConfig() {
         super.initConfig();
         when(config.getClient()).thenReturn(client);
+        when(config.getPath()).thenReturn(MY_PATH);
         when(config.getMaxGets()).thenReturn(MAX_GETS);
         when(config.getPathGet()).thenReturn(PATH_GET);
         when(config.getWaitSecGet()).thenReturn(WAIT_SEC_GETS);
diff --git a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/RestManagerResponseTest.java b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/RestManagerResponseTest.java
new file mode 100644 (file)
index 0000000..7a9541c
--- /dev/null
@@ -0,0 +1,109 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.so;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import javax.ws.rs.core.GenericType;
+import org.junit.Before;
+import org.junit.Test;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.StandardCoder;
+
+public class RestManagerResponseTest {
+    private static final Coder coder = new StandardCoder();
+
+    private static final int MY_STATUS = 200;
+    private static final String MY_TEXT = "{'text': 'hello'}".replace('\'', '"');
+
+    private RestManagerResponse resp;
+
+    @Before
+    public void setUp() {
+        resp = new RestManagerResponse(MY_STATUS, MY_TEXT, coder);
+    }
+
+    @Test
+    public void testGetStatus() {
+        assertEquals(MY_STATUS, resp.getStatus());
+    }
+
+    @Test
+    public void testClose() {
+        assertThatCode(() -> resp.close()).doesNotThrowAnyException();
+    }
+
+    @Test
+    public void testReadEntityClassOfT() {
+        // try with JSON
+        MyObject obj = resp.readEntity(MyObject.class);
+        assertNotNull(obj);
+        assertEquals("hello", obj.text);
+
+        // try plain string
+        resp = new RestManagerResponse(MY_STATUS, "some text", coder);
+        assertEquals("some text", resp.readEntity(String.class));
+
+        // coder throws an exception
+        resp = new RestManagerResponse(MY_STATUS, "{invalid-json", coder);
+        assertThatIllegalArgumentException().isThrownBy(() -> resp.readEntity(MyObject.class))
+                        .withMessage("cannot decode response");
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testUnsupported() {
+        GenericType<String> generic = GenericType.forInstance(String.class);
+
+        assertThatThrownBy(() -> resp.hasEntity()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.bufferEntity()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLength()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.readEntity(generic)).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.readEntity(generic, null)).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getStatusInfo()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getEntity()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getMediaType()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLanguage()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getAllowedMethods()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getCookies()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getEntityTag()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getDate()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLanguage()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLastModified()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLocation()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLinks()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.hasLink(null)).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLink(null)).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getLinkBuilder(null)).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getMetadata()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getStringHeaders()).isInstanceOf(UnsupportedOperationException.class);
+        assertThatThrownBy(() -> resp.getHeaderString(null)).isInstanceOf(UnsupportedOperationException.class);
+    }
+
+
+    private static class MyObject {
+        private String text;
+    }
+}
index b73a65e..cdd8a33 100644 (file)
@@ -74,7 +74,8 @@ public class SoActorServiceProviderTest extends BasicActor {
         SoActorServiceProvider prov = new SoActorServiceProvider();
 
         // verify that it has the operators we expect
-        var expected = Arrays.asList(VfModuleCreate.NAME).stream().sorted().collect(Collectors.toList());
+        var expected = Arrays.asList(VfModuleCreate.NAME, VfModuleDelete.NAME).stream().sorted()
+                        .collect(Collectors.toList());
         var actual = prov.getOperationNames().stream().sorted().collect(Collectors.toList());
 
         assertEquals(expected.toString(), actual.toString());
index 8bd607f..8c084b8 100644 (file)
@@ -164,7 +164,7 @@ public class VfModuleCreateTest extends BasicSoOperation {
 
         CompletableFuture<OperationOutcome> future2 = oper.start();
 
-        outcome = future2.get(500, TimeUnit.SECONDS);
+        outcome = future2.get(5, TimeUnit.SECONDS);
         assertEquals(PolicyResult.SUCCESS, outcome.getResult());
 
         assertEquals(origCount + 1, oper.getVfCount());
@@ -192,7 +192,7 @@ public class VfModuleCreateTest extends BasicSoOperation {
 
         CompletableFuture<OperationOutcome> future2 = oper.start();
 
-        outcome = future2.get(500, TimeUnit.SECONDS);
+        outcome = future2.get(5, TimeUnit.SECONDS);
         assertEquals(PolicyResult.SUCCESS, outcome.getResult());
     }
 
@@ -202,7 +202,7 @@ public class VfModuleCreateTest extends BasicSoOperation {
 
         // @formatter:off
         assertEquals(
-            "/serviceInstances/my-service-instance-id/vnfs/my-vnf-id/vfModules/scaleOut",
+            "/my-service-instance-id/vnfs/my-vnf-id/vfModules/scaleOut",
             pair.getLeft());
         // @formatter:on
 
diff --git a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleDeleteTest.java b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleDeleteTest.java
new file mode 100644 (file)
index 0000000..c08fa37
--- /dev/null
@@ -0,0 +1,440 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP
+ * ================================================================================
+ * Copyright (C) 2020 AT&T Intellectual Property. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.onap.policy.controlloop.actor.so;
+
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.ws.rs.client.InvocationCallback;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.tuple.Pair;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.onap.aai.domain.yang.CloudRegion;
+import org.onap.aai.domain.yang.GenericVnf;
+import org.onap.aai.domain.yang.ModelVer;
+import org.onap.aai.domain.yang.ServiceInstance;
+import org.onap.aai.domain.yang.Tenant;
+import org.onap.policy.aai.AaiCqResponse;
+import org.onap.policy.common.utils.coder.Coder;
+import org.onap.policy.common.utils.coder.CoderException;
+import org.onap.policy.common.utils.coder.StandardCoder;
+import org.onap.policy.controlloop.actorserviceprovider.OperationOutcome;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.ControlLoopOperationParams;
+import org.onap.policy.controlloop.actorserviceprovider.parameters.HttpConfig;
+import org.onap.policy.controlloop.policy.PolicyResult;
+import org.onap.policy.so.SoRequest;
+import org.onap.policy.so.SoResponse;
+
+public class VfModuleDeleteTest extends BasicSoOperation {
+    private static final String EXPECTED_EXCEPTION = "expected exception";
+    private static final String MODEL_NAME2 = "my-model-name-B";
+    private static final String MODEL_VERS2 = "my-model-version-B";
+    private static final String SVC_INSTANCE_ID = "my-service-instance-id";
+    private static final String VNF_ID = "my-vnf-id";
+
+    @Mock
+    private java.net.http.HttpClient javaClient;
+    @Mock
+    private HttpResponse<String> javaResp;
+    @Mock
+    private InvocationCallback<Response> callback;
+
+    private CompletableFuture<HttpResponse<String>> javaFuture;
+    private VfModuleDelete oper;
+
+    public VfModuleDeleteTest() {
+        super(DEFAULT_ACTOR, VfModuleDelete.NAME);
+    }
+
+
+    /**
+     * Sets up.
+     */
+    @Before
+    public void setUp() throws Exception {
+        super.setUp();
+
+        initHostPort();
+
+        configureResponse(coder.encode(response));
+
+        oper = new MyOperation(params, config);
+    }
+
+    @Test
+    public void testConstructor() {
+        assertEquals(DEFAULT_ACTOR, oper.getActorName());
+        assertEquals(VfModuleDelete.NAME, oper.getName());
+
+        // verify that target validation is done
+        params = params.toBuilder().target(null).build();
+        assertThatIllegalArgumentException().isThrownBy(() -> new VfModuleDelete(params, config))
+                        .withMessageContaining("Target information");
+    }
+
+    @Test
+    public void testStartPreprocessorAsync() throws Exception {
+        // insert CQ data so it's there for the check
+        context.setProperty(AaiCqResponse.CONTEXT_KEY, makeCqResponse());
+
+        AtomicBoolean guardStarted = new AtomicBoolean();
+
+        oper = new MyOperation(params, config) {
+            @Override
+            protected CompletableFuture<OperationOutcome> startGuardAsync() {
+                guardStarted.set(true);
+                return super.startGuardAsync();
+            }
+        };
+
+        CompletableFuture<OperationOutcome> future3 = oper.startPreprocessorAsync();
+        assertNotNull(future3);
+        assertTrue(guardStarted.get());
+    }
+
+    @Test
+    public void testStartGuardAsync() throws Exception {
+        // remove CQ data so it's forced to query
+        context.removeProperty(AaiCqResponse.CONTEXT_KEY);
+
+        CompletableFuture<OperationOutcome> future2 = oper.startPreprocessorAsync();
+        assertTrue(executor.runAll(100));
+        assertFalse(future2.isDone());
+
+        provideCqResponse(makeCqResponse());
+        assertTrue(executor.runAll(100));
+        assertTrue(future2.isDone());
+        assertEquals(PolicyResult.SUCCESS, future2.get().getResult());
+    }
+
+    @Test
+    public void testMakeGuardPayload() {
+        final int origCount = 30;
+        oper.setVfCount(origCount);
+
+        CompletableFuture<OperationOutcome> future2 = oper.startPreprocessorAsync();
+        assertTrue(executor.runAll(100));
+        assertTrue(future2.isDone());
+
+        // get the payload from the request
+        ArgumentCaptor<ControlLoopOperationParams> captor = ArgumentCaptor.forClass(ControlLoopOperationParams.class);
+        verify(guardOperator).buildOperation(captor.capture());
+
+        Map<String, Object> payload = captor.getValue().getPayload();
+        assertNotNull(payload);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> resource = (Map<String, Object>) payload.get("resource");
+        assertNotNull(resource);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Object> guard = (Map<String, Object>) resource.get("guard");
+        assertNotNull(guard);
+
+        Integer newCount = (Integer) guard.get(VfModuleDelete.PAYLOAD_KEY_VF_COUNT);
+        assertNotNull(newCount);
+        assertEquals(origCount - 1, newCount.intValue());
+    }
+
+    @Test
+    public void testStartOperationAsync_testSuccessfulCompletion() throws Exception {
+        final int origCount = 30;
+        oper.setVfCount(origCount);
+
+        // use a real executor
+        params = params.toBuilder().executor(ForkJoinPool.commonPool()).build();
+
+        oper = new MyOperation(params, config) {
+            @Override
+            public long getWaitMsGet() {
+                return 1;
+            }
+        };
+
+        CompletableFuture<OperationOutcome> future2 = oper.start();
+
+        outcome = future2.get(5, TimeUnit.SECONDS);
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+
+        assertEquals(origCount - 1, oper.getVfCount());
+    }
+
+    /**
+     * Tests startOperationAsync() when "get" operations are required.
+     */
+    @Test
+    public void testStartOperationAsyncWithGets() throws Exception {
+
+        // indicate that the response was incomplete
+        configureResponse(coder.encode(response).replace("COMPLETE", "incomplete"));
+
+        when(rawResponse.getStatus()).thenReturn(500, 500, 500, 200, 200);
+        when(client.get(any(), any(), any())).thenAnswer(provideResponse(rawResponse));
+
+        // use a real executor
+        params = params.toBuilder().executor(ForkJoinPool.commonPool()).build();
+
+        oper = new MyOperation(params, config) {
+            @Override
+            public long getWaitMsGet() {
+                return 1;
+            }
+        };
+
+        CompletableFuture<OperationOutcome> future2 = oper.start();
+
+        outcome = future2.get(5, TimeUnit.SECONDS);
+        assertEquals(PolicyResult.SUCCESS, outcome.getResult());
+    }
+
+    @Test
+    public void testMakeRequest() throws CoderException {
+        Pair<String, SoRequest> pair = oper.makeRequest();
+
+        assertEquals("/my-service-instance-id/vnfs/my-vnf-id/vfModules/null", pair.getLeft());
+
+        verifyRequest("VfModuleDelete.json", pair.getRight());
+    }
+
+    @Test
+    public void testDelete() throws Exception {
+        SoRequest req = new SoRequest();
+        req.setRequestId(REQ_ID);
+
+        Map<String, Object> headers = Map.of("key-A", "value-A");
+
+        final CompletableFuture<Response> delFuture =
+                        oper.delete("my-uri", headers, MediaType.APPLICATION_JSON, req, callback);
+
+        ArgumentCaptor<HttpRequest> reqCaptor = ArgumentCaptor.forClass(HttpRequest.class);
+        verify(javaClient).sendAsync(reqCaptor.capture(), any());
+
+        HttpRequest req2 = reqCaptor.getValue();
+        assertEquals("http://my-host:6969/my-uri", req2.uri().toString());
+        assertEquals("DELETE", req2.method());
+
+        HttpHeaders headers2 = req2.headers();
+        assertEquals("value-A", headers2.firstValue("key-A").orElse("missing-key"));
+        assertEquals(MediaType.APPLICATION_JSON, headers2.firstValue("Content-type").orElse("missing-key"));
+
+        assertTrue(delFuture.isDone());
+        Response resp = delFuture.get();
+
+        verify(callback).completed(resp);
+
+        assertEquals(200, resp.getStatus());
+
+        SoResponse resp2 = resp.readEntity(SoResponse.class);
+        assertEquals(SoOperation.COMPLETE, resp2.getRequest().getRequestStatus().getRequestState());
+    }
+
+    /**
+     * Tests delete() when an exception is thrown in the future.
+     */
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testDeleteException() throws Exception {
+        Throwable thrown = new IllegalStateException(EXPECTED_EXCEPTION);
+
+        // need a new future, with an exception
+        javaFuture = CompletableFuture.failedFuture(thrown);
+        when(javaClient.sendAsync(any(), any(BodyHandlers.ofString().getClass()))).thenReturn(javaFuture);
+
+        SoRequest req = new SoRequest();
+        req.setRequestId(REQ_ID);
+
+        CompletableFuture<Response> delFuture =
+                        oper.delete("/my-uri", Map.of(), MediaType.APPLICATION_JSON, req, callback);
+
+        assertTrue(delFuture.isCompletedExceptionally());
+
+        ArgumentCaptor<Throwable> thrownCaptor = ArgumentCaptor.forClass(Throwable.class);
+        verify(callback).failed(thrownCaptor.capture());
+        assertSame(thrown, thrownCaptor.getValue().getCause());
+    }
+
+    @Test
+    public void testEncodeBody() {
+        // try when request is already a string
+        assertEquals("hello", oper.encodeRequest("hello"));
+
+        // try with a real request
+        SoRequest req = new SoRequest();
+        req.setRequestId(REQ_ID);
+        assertEquals("{\"requestId\":\"" + REQ_ID.toString() + "\"}", oper.encodeRequest(req));
+
+        // coder throws an exception
+        oper = new MyOperation(params, config) {
+            @Override
+            protected Coder makeCoder() {
+                return new StandardCoder() {
+                    @Override
+                    public String encode(Object object) throws CoderException {
+                        throw new CoderException(EXPECTED_EXCEPTION);
+                    }
+                };
+            }
+        };
+
+        assertThatIllegalArgumentException().isThrownBy(() -> oper.encodeRequest(req))
+                        .withMessage("cannot encode request");
+    }
+
+    /**
+     * Tests addAuthHeader() when there is a username, but no password.
+     */
+    @Test
+    public void testAddAuthHeader() {
+        Builder builder = mock(Builder.class);
+        when(client.getUserName()).thenReturn("the-user");
+        when(client.getPassword()).thenReturn("the-password");
+        oper.addAuthHeader(builder);
+
+        ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
+
+        verify(builder).header(keyCaptor.capture(), valueCaptor.capture());
+
+        assertEquals("Authorization", keyCaptor.getValue());
+
+        String encoded = Base64.getEncoder().encodeToString("the-user:the-password".getBytes(StandardCharsets.UTF_8));
+        assertEquals("Basic " + encoded, valueCaptor.getValue());
+    }
+
+    /**
+     * Tests addAuthHeader() when there is no username.
+     */
+    @Test
+    public void testAddAuthHeaderNoUser() {
+        Builder builder = mock(Builder.class);
+        when(client.getPassword()).thenReturn("world");
+        oper.addAuthHeader(builder);
+        verify(builder, never()).header(any(), any());
+
+        // repeat with empty username
+        when(client.getUserName()).thenReturn("");
+        oper.addAuthHeader(builder);
+        verify(builder, never()).header(any(), any());
+    }
+
+    /**
+     * Tests addAuthHeader() when there is a username, but no password.
+     */
+    @Test
+    public void testAddAuthHeaderUserOnly() {
+        Builder builder = mock(Builder.class);
+        when(client.getUserName()).thenReturn("my-user");
+        oper.addAuthHeader(builder);
+
+        ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
+        ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
+
+        verify(builder).header(keyCaptor.capture(), valueCaptor.capture());
+
+        assertEquals("Authorization", keyCaptor.getValue());
+
+        String encoded = Base64.getEncoder().encodeToString("my-user:".getBytes(StandardCharsets.UTF_8));
+        assertEquals("Basic " + encoded, valueCaptor.getValue());
+    }
+
+    @Test
+    public void testMakeHttpClient() {
+        // must use a real operation to invoke this method
+        assertNotNull(new VfModuleDelete(params, config).makeHttpClient());
+    }
+
+
+    @Override
+    protected void makeContext() {
+        super.makeContext();
+
+        AaiCqResponse cq = mock(AaiCqResponse.class);
+
+        GenericVnf vnf = new GenericVnf();
+        when(cq.getGenericVnfByVfModuleModelInvariantId(MODEL_INVAR_ID)).thenReturn(vnf);
+        vnf.setVnfId(VNF_ID);
+
+        ServiceInstance instance = new ServiceInstance();
+        when(cq.getServiceInstance()).thenReturn(instance);
+        instance.setServiceInstanceId(SVC_INSTANCE_ID);
+
+        when(cq.getDefaultTenant()).thenReturn(new Tenant());
+        when(cq.getDefaultCloudRegion()).thenReturn(new CloudRegion());
+
+        ModelVer modelVers = new ModelVer();
+        when(cq.getModelVerByVersionId(any())).thenReturn(modelVers);
+        modelVers.setModelName(MODEL_NAME2);
+        modelVers.setModelVersion(MODEL_VERS2);
+
+        params.getContext().setProperty(AaiCqResponse.CONTEXT_KEY, cq);
+    }
+
+    private void initHostPort() {
+        when(client.getBaseUrl()).thenReturn("http://my-host:6969/");
+    }
+
+    @SuppressWarnings("unchecked")
+    private void configureResponse(String responseText) throws CoderException {
+        // indicate that the response was completed
+        when(javaResp.statusCode()).thenReturn(200);
+        when(javaResp.body()).thenReturn(responseText);
+
+        javaFuture = CompletableFuture.completedFuture(javaResp);
+        when(javaClient.sendAsync(any(), any(BodyHandlers.ofString().getClass()))).thenReturn(javaFuture);
+    }
+
+    private class MyOperation extends VfModuleDelete {
+
+        public MyOperation(ControlLoopOperationParams params, HttpConfig config) {
+            super(params, config);
+        }
+
+        @Override
+        protected java.net.http.HttpClient makeHttpClient() {
+            return javaClient;
+        }
+    }
+}
diff --git a/models-interactions/model-actors/actor.so/src/test/resources/VfModuleDelete.json b/models-interactions/model-actors/actor.so/src/test/resources/VfModuleDelete.json
new file mode 100644 (file)
index 0000000..5b7cce5
--- /dev/null
@@ -0,0 +1,18 @@
+{
+  "requestDetails": {
+    "modelInfo": {
+      "modelType": "vfModule",
+      "modelInvariantId": "my-model-invariant-id",
+      "modelVersionId": "my-model-version-id",
+      "modelName": "my-model-name",
+      "modelVersion": "my-model-version",
+      "modelCustomizationId": "my-model-customization-id"
+    },
+    "cloudConfiguration": {},
+    "requestInfo": {
+      "source": "POLICY",
+      "suppressRollback": false,
+      "requestorId": "policy"
+    }
+  }
+}
\ No newline at end of file
index 4bf074f..e1cb0d9 100644 (file)
@@ -28,4 +28,6 @@ actors:
     clientName: my-client
     operations:
       VF Module Create:
-        path: serviceInstantiation/v7
\ No newline at end of file
+        path: serviceInstantiation/v7/serviceInstances
+      VF Module Delete:
+        path: serviceInstances/v7
\ No newline at end of file
index 6228756..81156c1 100644 (file)
@@ -30,6 +30,7 @@ import javax.ws.rs.client.Entity;
 import javax.ws.rs.client.Invocation.Builder;
 import javax.ws.rs.client.InvocationCallback;
 import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
@@ -101,6 +102,8 @@ public class BasicHttpOperation<Q> extends BasicOperation {
         when(rawResponse.getStatus()).thenReturn(200);
 
         when(webBuilder.async()).thenReturn(webAsync);
+        when(webBuilder.accept(any(MediaType.class))).thenReturn(webBuilder);
+        when(webBuilder.accept(any(String.class))).thenReturn(webBuilder);
 
         when(webTarget.request()).thenReturn(webBuilder);
         when(webTarget.path(any())).thenReturn(webTarget);
index 1af0b76..b06a66f 100644 (file)
@@ -25,6 +25,7 @@ import com.google.gson.Gson;
 
 import java.util.UUID;
 import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
@@ -36,7 +37,7 @@ import org.onap.policy.so.SoRequestStatus;
 import org.onap.policy.so.SoResponse;
 
 
-@Path("/serviceInstantiation")
+@Path("/")
 public class SoSimulatorJaxRs {
 
     /**
@@ -47,7 +48,7 @@ public class SoSimulatorJaxRs {
      * @return the response
      */
     @POST
-    @Path("/v7/serviceInstances/{serviceInstanceId}/vnfs/{vnfInstanceId}/vfModules/scaleOut")
+    @Path("/serviceInstantiation/v7/serviceInstances/{serviceInstanceId}/vnfs/{vnfInstanceId}/vfModules/scaleOut")
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces("application/json")
     public String soPostQuery(@PathParam("serviceInstanceId") final String serviceInstanceId,
@@ -69,4 +70,36 @@ public class SoSimulatorJaxRs {
 
         return new Gson().toJson(response);
     }
+
+    /**
+     * SO Delete.
+     *
+     * @param serviceInstanceId the service instance Id
+     * @param vnfInstanceId the VNF Id
+     * @return the response
+     */
+    @DELETE
+    @Path("/serviceInstances/v7/{serviceInstanceId}/vnfs/{vnfInstanceId}/vfModules/{vfModuleInstanceId}")
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces("application/json")
+    public String soDelete(@PathParam("serviceInstanceId") final String serviceInstanceId,
+                    @PathParam("vnfInstanceId") final String vnfInstanceId,
+                    @PathParam("vfModuleInstanceId") final String vfModuleInstanceId) {
+        final SoRequest request = new SoRequest();
+        final SoRequestStatus requestStatus = new SoRequestStatus();
+        requestStatus.setRequestState("COMPLETE");
+        request.setRequestStatus(requestStatus);
+        request.setRequestId(UUID.randomUUID());
+
+        final SoResponse response = new SoResponse();
+
+        final SoRequestReferences requestReferences = new SoRequestReferences();
+        final String requestId = UUID.randomUUID().toString();
+        requestReferences.setRequestId(requestId);
+        response.setRequestReferences(requestReferences);
+
+        response.setRequest(request);
+
+        return new Gson().toJson(response);
+    }
 }
index b287e3d..723619e 100644 (file)
@@ -134,7 +134,7 @@ public class SoSimulatorTest {
     }
 
     @Test
-    public void testResponse() {
+    public void testPost() {
         final String request = Serialization.gsonPretty.toJson(this.createTestRequest());
         final Pair<Integer, String> httpDetails = new RestManager().post(
                         "http://localhost:6667/serviceInstantiation/v7/serviceInstances/12345/vnfs/12345/vfModules/scaleOut",
@@ -144,4 +144,16 @@ public class SoSimulatorTest {
         final SoResponse response = Serialization.gsonPretty.fromJson(httpDetails.second, SoResponse.class);
         assertNotNull(response);
     }
+
+    @Test
+    public void testDelete() {
+        final String request = Serialization.gsonPretty.toJson(this.createTestRequest());
+        final Pair<Integer, String> httpDetails = new RestManager().delete(
+                        "http://localhost:6667/serviceInstances/v7/12345/vnfs/12345/vfModules/12345",
+                        "username",
+                        "password", new HashMap<>(), "application/json", request);
+        assertNotNull(httpDetails);
+        final SoResponse response = Serialization.gsonPretty.fromJson(httpDetails.second, SoResponse.class);
+        assertNotNull(response);
+    }
 }