diff options
Diffstat (limited to 'models-interactions/model-actors/actor.so/src')
12 files changed, 1102 insertions, 27 deletions
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 index 000000000..1b49ab579 --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/RestManagerResponse.java @@ -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(); + } +} diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java index 9c9e6dc62..e4b9c14fe 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoActorServiceProvider.java @@ -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** diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java index 41ecd07a0..1ca6c734c 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/SoOperation.java @@ -21,12 +21,14 @@ 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. diff --git a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java index 4c35f9abe..077c8578b 100644 --- a/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleCreate.java @@ -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 index 000000000..5134d58da --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/main/java/org/onap/policy/controlloop/actor/so/VfModuleDelete.java @@ -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(); + } +} diff --git a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/BasicSoOperation.java b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/BasicSoOperation.java index 35f1ef823..f33d501b6 100644 --- a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/BasicSoOperation.java +++ b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/BasicSoOperation.java @@ -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 index 000000000..7a9541cc4 --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/RestManagerResponseTest.java @@ -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; + } +} diff --git a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/SoActorServiceProviderTest.java b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/SoActorServiceProviderTest.java index b73a65e44..cdd8a334c 100644 --- a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/SoActorServiceProviderTest.java +++ b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/SoActorServiceProviderTest.java @@ -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()); diff --git a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleCreateTest.java b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleCreateTest.java index 8bd607f32..8c084b8dc 100644 --- a/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleCreateTest.java +++ b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleCreateTest.java @@ -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 index 000000000..c08fa37d1 --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/test/java/org/onap/policy/controlloop/actor/so/VfModuleDeleteTest.java @@ -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 index 000000000..5b7cce5f3 --- /dev/null +++ b/models-interactions/model-actors/actor.so/src/test/resources/VfModuleDelete.json @@ -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 diff --git a/models-interactions/model-actors/actor.so/src/test/resources/service.yaml b/models-interactions/model-actors/actor.so/src/test/resources/service.yaml index 4bf074fc5..e1cb0d9f3 100644 --- a/models-interactions/model-actors/actor.so/src/test/resources/service.yaml +++ b/models-interactions/model-actors/actor.so/src/test/resources/service.yaml @@ -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 |