diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProviderTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProviderTest.java new file mode 100644 index 0000000000..a20279ba85 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/CloudFoundryTokenProviderTest.java @@ -0,0 +1,39 @@ +package org.cloudfoundry.multiapps.controller.client; + +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class CloudFoundryTokenProviderTest { + + @Mock + private OAuthClient oAuthClient; + @Mock + private OAuth2AccessTokenWithAdditionalInfo token; + + private CloudFoundryTokenProvider tokenProvider; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + tokenProvider = new CloudFoundryTokenProvider(oAuthClient); + } + + @Test + void testGetTokenDelegatesToOAuthClient() { + Mockito.when(oAuthClient.getToken()) + .thenReturn(token); + + OAuth2AccessTokenWithAdditionalInfo result = tokenProvider.getToken(); + + Assertions.assertSame(token, result); + Mockito.verify(oAuthClient) + .getToken(); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClientTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClientTest.java new file mode 100644 index 0000000000..07adea18c5 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/ResilientCloudControllerClientTest.java @@ -0,0 +1,307 @@ +package org.cloudfoundry.multiapps.controller.client; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.client.facade.UploadStatusCallback; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudDomain; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudPackage; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudSpace; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; +import org.cloudfoundry.multiapps.controller.client.facade.rest.CloudControllerRestClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; + +class ResilientCloudControllerClientTest { + + private CloudControllerRestClient restClient; + private ResilientCloudControllerClient client; + + @BeforeEach + void setUp() { + restClient = Mockito.mock(CloudControllerRestClient.class); + client = new ResilientCloudControllerClient(restClient); + } + + @Test + void testGetTargetIsExecutedWithoutRetryWrapping() { + CloudSpace space = Mockito.mock(CloudSpace.class); + Mockito.when(restClient.getTarget()) + .thenReturn(space); + + Assertions.assertSame(space, client.getTarget()); + Mockito.verify(restClient) + .getTarget(); + } + + @Test + void testVoidMethodRetriesOnTransientBadGateway() { + Mockito.doThrow(new CloudOperationException(HttpStatus.BAD_GATEWAY)) + .doNothing() + .when(restClient) + .addDomain("example.com"); + + client.addDomain("example.com"); + + Mockito.verify(restClient, Mockito.times(2)) + .addDomain("example.com"); + } + + @Test + void testValueReturningMethodRetriesOnTransientBadGateway() { + CloudApplication app = Mockito.mock(CloudApplication.class); + Mockito.when(restClient.getApplication("my-app")) + .thenThrow(new CloudOperationException(HttpStatus.BAD_GATEWAY)) + .thenReturn(app); + + Assertions.assertSame(app, client.getApplication("my-app")); + Mockito.verify(restClient, Mockito.times(2)) + .getApplication("my-app"); + } + + @Test + void testNonRetryableStatusPropagatesImmediately() { + Mockito.when(restClient.getApplication("missing")) + .thenThrow(new CloudOperationException(HttpStatus.UNAUTHORIZED)); + + CloudOperationException thrown = Assertions.assertThrows(CloudOperationException.class, () -> client.getApplication("missing")); + + Assertions.assertEquals(HttpStatus.UNAUTHORIZED, thrown.getStatusCode()); + Mockito.verify(restClient, Mockito.times(1)) + .getApplication("missing"); + } + + @Test + void testGetApplicationsIgnoresNotFound() { + Mockito.when(restClient.getApplications()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getApplications() + .isEmpty()); + Mockito.verify(restClient, Mockito.times(2)) + .getApplications(); + } + + @Test + void testGetDomainsIgnoresNotFound() { + Mockito.when(restClient.getDomains()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List. of()); + + Assertions.assertTrue(client.getDomains() + .isEmpty()); + Mockito.verify(restClient, Mockito.times(2)) + .getDomains(); + } + + @Test + void testRoutesByDomainIgnoresNotFound() { + Mockito.when(restClient.getRoutes("example.com")) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getRoutes("example.com") + .isEmpty()); + Mockito.verify(restClient, Mockito.times(2)) + .getRoutes("example.com"); + } + + @Test + void testGetStackByNameDelegates() { + CloudStack stack = Mockito.mock(CloudStack.class); + Mockito.when(restClient.getStack("cflinuxfs4")) + .thenReturn(stack); + + Assertions.assertSame(stack, client.getStack("cflinuxfs4")); + } + + @Test + void testGetStackByNameAndRequiredFlagDelegates() { + CloudStack stack = Mockito.mock(CloudStack.class); + Mockito.when(restClient.getStack("cflinuxfs4", true)) + .thenReturn(stack); + + Assertions.assertSame(stack, client.getStack("cflinuxfs4", true)); + } + + @Test + void testRunTaskDelegatesAndRetries() { + Mockito.when(restClient.runTask(ArgumentMatchers.eq("my-app"), ArgumentMatchers.any())) + .thenThrow(new CloudOperationException(HttpStatus.BAD_GATEWAY)) + .thenReturn(null); + + client.runTask("my-app", null); + + Mockito.verify(restClient, Mockito.times(2)) + .runTask(ArgumentMatchers.eq("my-app"), ArgumentMatchers.any()); + } + + @Test + void testCancelTaskDelegates() { + UUID taskGuid = UUID.randomUUID(); + + client.cancelTask(taskGuid); + + Mockito.verify(restClient) + .cancelTask(taskGuid); + } + + @Test + void testRenameDelegates() { + client.rename("old", "new"); + + Mockito.verify(restClient) + .rename("old", "new"); + } + + @Test + void testStartApplicationDelegates() { + client.startApplication("my-app"); + + Mockito.verify(restClient) + .startApplication("my-app"); + } + + @Test + void testStopApplicationDelegates() { + client.stopApplication("my-app"); + + Mockito.verify(restClient) + .stopApplication("my-app"); + } + + @Test + void testRestartApplicationDelegates() { + client.restartApplication("my-app"); + + Mockito.verify(restClient) + .restartApplication("my-app"); + } + + @Test + void testBindServiceInstanceReturnsOptional() { + Mockito.when(restClient.bindServiceInstance("binding", "app", "service")) + .thenReturn(Optional.of("job-1")); + + Assertions.assertEquals(Optional.of("job-1"), client.bindServiceInstance("binding", "app", "service")); + } + + @Test + void testGetApplicationSshEnabledRetriesAndReturnsBoolean() { + UUID guid = UUID.randomUUID(); + Mockito.when(restClient.getApplicationSshEnabled(guid)) + .thenThrow(new CloudOperationException(HttpStatus.BAD_GATEWAY)) + .thenReturn(true); + + Assertions.assertTrue(client.getApplicationSshEnabled(guid)); + Mockito.verify(restClient, Mockito.times(2)) + .getApplicationSshEnabled(guid); + } + + @Test + void testAsyncUploadWithOverrideTimeoutUsesPlainRetry() { + Path file = Paths.get("/tmp/some.mtar"); + UploadStatusCallback callback = Mockito.mock(UploadStatusCallback.class); + Duration override = Duration.ofMinutes(2); + CloudPackage pkg = Mockito.mock(CloudPackage.class); + Mockito.when(restClient.asyncUploadApplication("my-app", file, callback, override)) + .thenReturn(pkg); + + Assertions.assertSame(pkg, client.asyncUploadApplicationWithExponentialBackoff("my-app", file, callback, override)); + Mockito.verify(restClient) + .asyncUploadApplication("my-app", file, callback, override); + } + + @Test + void testAsyncUploadWithoutOverrideTimeoutUsesExponentialBackoff() { + Path file = Paths.get("/tmp/some.mtar"); + UploadStatusCallback callback = Mockito.mock(UploadStatusCallback.class); + CloudPackage pkg = Mockito.mock(CloudPackage.class); + Mockito.when(restClient.asyncUploadApplication(ArgumentMatchers.eq("my-app"), + ArgumentMatchers.eq(file), + ArgumentMatchers.eq(callback), + ArgumentMatchers.any(Duration.class))) + .thenReturn(pkg); + + Assertions.assertSame(pkg, client.asyncUploadApplicationWithExponentialBackoff("my-app", file, callback, null)); + Mockito.verify(restClient) + .asyncUploadApplication(ArgumentMatchers.eq("my-app"), + ArgumentMatchers.eq(file), + ArgumentMatchers.eq(callback), + ArgumentMatchers.any(Duration.class)); + } + + @Test + void testGetUploadStatusDelegates() { + UUID packageGuid = UUID.randomUUID(); + + client.getUploadStatus(packageGuid); + + Mockito.verify(restClient) + .getUploadStatus(packageGuid); + } + + @Test + void testDeleteOrphanedRoutesIgnoresNotFound() { + Mockito.doThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .doNothing() + .when(restClient) + .deleteOrphanedRoutes(); + + client.deleteOrphanedRoutes(); + + Mockito.verify(restClient, Mockito.times(2)) + .deleteOrphanedRoutes(); + } + + @Test + void testGetServiceBrokersIgnoresNotFound() { + Mockito.when(restClient.getServiceBrokers()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getServiceBrokers() + .isEmpty()); + } + + @Test + void testGetEventsIgnoresNotFound() { + Mockito.when(restClient.getEvents()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getEvents() + .isEmpty()); + } + + @Test + void testGetServiceOfferingsIgnoresNotFound() { + Mockito.when(restClient.getServiceOfferings()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getServiceOfferings() + .isEmpty()); + } + + @Test + void testGetStacksIgnoresNotFound() { + Mockito.when(restClient.getStacks()) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)) + .thenReturn(List.of()); + + Assertions.assertTrue(client.getStacks() + .isEmpty()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerExceptionTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerExceptionTest.java new file mode 100644 index 0000000000..90a408228d --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudControllerExceptionTest.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class CloudControllerExceptionTest { + + @Test + void testStatusOnlyConstructorDecoratesMessage() { + CloudControllerException e = new CloudControllerException(HttpStatus.NOT_FOUND); + + Assertions.assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode()); + Assertions.assertEquals("Controller operation failed: 404 Not Found", e.getMessage()); + } + + @Test + void testStatusAndStatusTextConstructor() { + CloudControllerException e = new CloudControllerException(HttpStatus.BAD_REQUEST, "Bad Request"); + + Assertions.assertEquals("Bad Request", e.getStatusText()); + Assertions.assertEquals("Controller operation failed: 400 Bad Request", e.getMessage()); + } + + @Test + void testStatusStatusTextDescriptionConstructor() { + CloudControllerException e = new CloudControllerException(HttpStatus.UNAUTHORIZED, "Unauthorized", "expired"); + + Assertions.assertEquals("expired", e.getDescription()); + Assertions.assertEquals("Controller operation failed: 401 Unauthorized: expired", e.getMessage()); + } + + @Test + void testWrappingConstructorPreservesFieldsAndCause() { + CloudOperationException source = new CloudOperationException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "boom"); + + CloudControllerException e = new CloudControllerException(source); + + Assertions.assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, e.getStatusCode()); + Assertions.assertEquals("Internal Server Error", e.getStatusText()); + Assertions.assertEquals("boom", e.getDescription()); + Assertions.assertSame(source, e.getCause()); + Assertions.assertEquals("Controller operation failed: 500 Internal Server Error: boom", e.getMessage()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentialsTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentialsTest.java new file mode 100644 index 0000000000..af1c177b2e --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudCredentialsTest.java @@ -0,0 +1,96 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import java.util.stream.Stream; + +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class CloudCredentialsTest { + + @Mock + private OAuth2AccessTokenWithAdditionalInfo token; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + static Stream emailPasswordConstructors() { + return Stream.of(Arguments.of(new CloudCredentials("alice@example.com", "secret"), + "alice@example.com", + "secret", + "cf", + "", + null), + Arguments.of(new CloudCredentials("a", "b", "client-x"), "a", "b", "client-x", "", null), + Arguments.of(new CloudCredentials("a", "b", "cid", "csecret"), "a", "b", "cid", "csecret", null), + Arguments.of(new CloudCredentials("a", "b", "cid", "csecret", "uaa"), "a", "b", "cid", "csecret", "uaa")); + } + + @ParameterizedTest + @MethodSource("emailPasswordConstructors") + void testEmailPasswordConstructorsAssignAllFields(CloudCredentials credentials, String email, String password, String clientId, + String clientSecret, String origin) { + Assertions.assertEquals(email, credentials.getEmail()); + Assertions.assertEquals(password, credentials.getPassword()); + Assertions.assertEquals(clientId, credentials.getClientId()); + Assertions.assertEquals(clientSecret, credentials.getClientSecret()); + Assertions.assertEquals(origin, credentials.getOrigin()); + Assertions.assertNull(credentials.getToken()); + Assertions.assertNull(credentials.getProxyUser()); + Assertions.assertFalse(credentials.isProxyUserSet()); + Assertions.assertTrue(credentials.isRefreshable()); + } + + @Test + void testTokenAndRefreshableConstructorRespectsRefreshableFlag() { + CloudCredentials refreshable = new CloudCredentials(token); + CloudCredentials nonRefreshable = new CloudCredentials(token, false); + + Assertions.assertSame(token, refreshable.getToken()); + Assertions.assertTrue(refreshable.isRefreshable()); + Assertions.assertSame(token, nonRefreshable.getToken()); + Assertions.assertFalse(nonRefreshable.isRefreshable()); + } + + @Test + void testTokenWithClientCredentialsConstructorsAssignClientId() { + CloudCredentials withClientId = new CloudCredentials(token, "client-y"); + CloudCredentials withClientIdAndSecret = new CloudCredentials(token, "client-y", "shh"); + + Assertions.assertEquals("client-y", withClientId.getClientId()); + Assertions.assertEquals("client-y", withClientIdAndSecret.getClientId()); + Assertions.assertEquals("shh", withClientIdAndSecret.getClientSecret()); + } + + @Test + void testProxyForUserCopiesEmailPasswordClientIdAndTokenAndSetsProxyUser() { + CloudCredentials base = new CloudCredentials("a@b.c", "pw", "cid"); + + CloudCredentials proxied = base.proxyForUser("admin"); + + Assertions.assertEquals("a@b.c", proxied.getEmail()); + Assertions.assertEquals("pw", proxied.getPassword()); + Assertions.assertEquals("cid", proxied.getClientId()); + Assertions.assertEquals("admin", proxied.getProxyUser()); + Assertions.assertTrue(proxied.isProxyUserSet()); + } + + @Test + void testProxyForUserWithTokenCopiesToken() { + CloudCredentials base = new CloudCredentials(token); + + CloudCredentials proxied = base.proxyForUser("admin"); + + Assertions.assertSame(token, proxied.getToken()); + Assertions.assertEquals("admin", proxied.getProxyUser()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudExceptionTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudExceptionTest.java new file mode 100644 index 0000000000..b6c51180bd --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudExceptionTest.java @@ -0,0 +1,34 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CloudExceptionTest { + + @Test + void testCauseOnlyConstructor() { + Throwable cause = new RuntimeException("inner"); + + CloudException e = new CloudException(cause); + + Assertions.assertSame(cause, e.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + Throwable cause = new RuntimeException("inner"); + + CloudException e = new CloudException("outer", cause); + + Assertions.assertEquals("outer", e.getMessage()); + Assertions.assertSame(cause, e.getCause()); + } + + @Test + void testMessageOnlyConstructor() { + CloudException e = new CloudException("only-message"); + + Assertions.assertEquals("only-message", e.getMessage()); + Assertions.assertNull(e.getCause()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationExceptionTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationExceptionTest.java new file mode 100644 index 0000000000..8319f74929 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudOperationExceptionTest.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class CloudOperationExceptionTest { + + @Test + void testStatusOnlyConstructorUsesReasonPhraseAsStatusText() { + CloudOperationException e = new CloudOperationException(HttpStatus.NOT_FOUND); + + Assertions.assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode()); + Assertions.assertEquals(HttpStatus.NOT_FOUND.getReasonPhrase(), e.getStatusText()); + Assertions.assertNull(e.getDescription()); + Assertions.assertEquals("404 Not Found", e.getMessage()); + } + + @Test + void testStatusAndStatusTextConstructor() { + CloudOperationException e = new CloudOperationException(HttpStatus.BAD_REQUEST, "Bad Request"); + + Assertions.assertEquals(HttpStatus.BAD_REQUEST, e.getStatusCode()); + Assertions.assertEquals("Bad Request", e.getStatusText()); + Assertions.assertNull(e.getDescription()); + Assertions.assertEquals("400 Bad Request", e.getMessage()); + } + + @Test + void testDescriptionConstructorIncludesDescriptionInMessage() { + CloudOperationException e = new CloudOperationException(HttpStatus.UNAUTHORIZED, "Unauthorized", "token expired"); + + Assertions.assertEquals("token expired", e.getDescription()); + Assertions.assertEquals("401 Unauthorized: token expired", e.getMessage()); + } + + @Test + void testCauseIsPropagatedToSuper() { + Throwable cause = new RuntimeException("boom"); + + CloudOperationException e = new CloudOperationException(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "x", cause); + + Assertions.assertSame(cause, e.getCause()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerExceptionTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerExceptionTest.java new file mode 100644 index 0000000000..d280fae0cb --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/CloudServiceBrokerExceptionTest.java @@ -0,0 +1,42 @@ +package org.cloudfoundry.multiapps.controller.client.facade; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class CloudServiceBrokerExceptionTest { + + @Test + void testStatusOnlyConstructorDecoratesMessage() { + CloudServiceBrokerException e = new CloudServiceBrokerException(HttpStatus.NOT_FOUND); + + Assertions.assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode()); + Assertions.assertEquals("Service broker operation failed: 404 Not Found", e.getMessage()); + } + + @Test + void testStatusAndStatusTextConstructor() { + CloudServiceBrokerException e = new CloudServiceBrokerException(HttpStatus.BAD_REQUEST, "Bad Request"); + + Assertions.assertEquals("Service broker operation failed: 400 Bad Request", e.getMessage()); + } + + @Test + void testStatusStatusTextDescriptionConstructor() { + CloudServiceBrokerException e = new CloudServiceBrokerException(HttpStatus.CONFLICT, "Conflict", "already exists"); + + Assertions.assertEquals("Service broker operation failed: 409 Conflict: already exists", e.getMessage()); + } + + @Test + void testWrappingConstructorPreservesFieldsAndCause() { + CloudOperationException source = new CloudOperationException(HttpStatus.UNPROCESSABLE_ENTITY, "Unprocessable Entity", "broker rejected"); + + CloudServiceBrokerException e = new CloudServiceBrokerException(source); + + Assertions.assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, e.getStatusCode()); + Assertions.assertEquals("broker rejected", e.getDescription()); + Assertions.assertSame(source, e.getCause()); + Assertions.assertEquals("Service broker operation failed: 422 Unprocessable Entity: broker rejected", e.getMessage()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java new file mode 100644 index 0000000000..f6131944b1 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/OAuthTokenProviderTest.java @@ -0,0 +1,79 @@ +package org.cloudfoundry.multiapps.controller.client.facade.adapters; + +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuthClient; +import org.cloudfoundry.reactor.ConnectionContext; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import reactor.core.publisher.Mono; + +class OAuthTokenProviderTest { + + @Mock + private OAuthClient oAuthClient; + @Mock + private OAuth2AccessTokenWithAdditionalInfo token; + @Mock + private ConnectionContext connectionContext; + + private OAuthTokenProvider provider; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + provider = new OAuthTokenProvider(oAuthClient); + } + + @Test + void testGetTokenIsLazyAndDoesNotInvokeOAuthClientUntilSubscribed() { + Mockito.when(oAuthClient.getToken()) + .thenReturn(token); + Mockito.when(token.getAuthorizationHeaderValue()) + .thenReturn("Bearer abc123"); + + Mono mono = provider.getToken(connectionContext); + + Mockito.verify(oAuthClient, Mockito.never()) + .getToken(); + + String result = mono.block(); + + Assertions.assertEquals("Bearer abc123", result); + Mockito.verify(oAuthClient) + .getToken(); + } + + @Test + void testGetTokenInvokesOAuthClientOncePerSubscription() { + Mockito.when(oAuthClient.getToken()) + .thenReturn(token); + Mockito.when(token.getAuthorizationHeaderValue()) + .thenReturn("Bearer abc123"); + + Mono mono = provider.getToken(connectionContext); + + mono.block(); + mono.block(); + + Mockito.verify(oAuthClient, Mockito.times(2)) + .getToken(); + } + + @Test + void testGetTokenPropagatesOAuthClientFailure() { + RuntimeException failure = new RuntimeException("token retrieval failed"); + Mockito.when(oAuthClient.getToken()) + .thenThrow(failure); + + Mono mono = provider.getToken(connectionContext); + + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, mono::block); + Assertions.assertSame(failure, thrown); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/util/CloudUtilTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/util/CloudUtilTest.java new file mode 100644 index 0000000000..412a26634f --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/util/CloudUtilTest.java @@ -0,0 +1,64 @@ +package org.cloudfoundry.multiapps.controller.client.facade.util; + +import java.util.Date; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CloudUtilTest { + + @Test + void testParseIntegerFromNumber() { + Assertions.assertEquals(Integer.valueOf(42), CloudUtil.parse(Integer.class, 42L)); + } + + @Test + void testParseIntegerFromString() { + Assertions.assertEquals(Integer.valueOf(7), CloudUtil.parse(Integer.class, "7")); + } + + @Test + void testParseLongFromNumber() { + Assertions.assertEquals(Long.valueOf(99L), CloudUtil.parse(Long.class, 99)); + } + + @Test + void testParseDoubleFromString() { + Assertions.assertEquals(Double.valueOf(1.5), CloudUtil.parse(Double.class, "1.5")); + } + + @Test + void testParseReturnsZeroDefaultWhenObjectIsNullForInteger() { + Assertions.assertEquals(Integer.valueOf(0), CloudUtil.parse(Integer.class, null)); + } + + @Test + void testParseReturnsZeroDefaultWhenObjectIsNullForLong() { + Assertions.assertEquals(Long.valueOf(0L), CloudUtil.parse(Long.class, null)); + } + + @Test + void testParseReturnsZeroDefaultWhenObjectIsNullForDouble() { + Assertions.assertEquals(Double.valueOf(0.0), CloudUtil.parse(Double.class, null)); + } + + @Test + void testParseReturnsNullWhenObjectIsNullForUnknownDefaultType() { + Assertions.assertNull(CloudUtil.parse(String.class, null)); + } + + @Test + void testParseStringPassesThrough() { + Assertions.assertEquals("hello", CloudUtil.parse(String.class, "hello")); + } + + @Test + void testParseDateReturnsNullForUnparsable() { + Assertions.assertNull(CloudUtil.parse(Date.class, "not-a-date")); + } + + @Test + void testParseReturnsNullOnClassCastFailure() { + Assertions.assertNull(CloudUtil.parse(Boolean.class, new Object())); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientCloudOperationExecutorTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientCloudOperationExecutorTest.java new file mode 100644 index 0000000000..42ef66f1b9 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientCloudOperationExecutorTest.java @@ -0,0 +1,76 @@ +package org.cloudfoundry.multiapps.controller.client.util; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +class ResilientCloudOperationExecutorTest { + + private ResilientCloudOperationExecutor executor; + + @BeforeEach + void setUp() { + executor = new ResilientCloudOperationExecutor().withRetryCount(3) + .withWaitTimeBetweenRetriesInMillis(0); + } + + @Test + void testRetriesOnDefaultIgnoredStatuses() { + AtomicInteger attempts = new AtomicInteger(); + Supplier operation = () -> { + if (attempts.incrementAndGet() < 2) { + throw new CloudOperationException(HttpStatus.BAD_GATEWAY); + } + return "ok"; + }; + + String result = executor.execute(operation); + + Assertions.assertEquals("ok", result); + Assertions.assertEquals(2, attempts.get()); + } + + @Test + void testThrowsImmediatelyOnNonIgnoredStatus() { + AtomicInteger attempts = new AtomicInteger(); + Supplier operation = () -> { + attempts.incrementAndGet(); + throw new CloudOperationException(HttpStatus.NOT_FOUND); + }; + + CloudOperationException thrown = Assertions.assertThrows(CloudOperationException.class, () -> executor.execute(operation)); + Assertions.assertEquals(HttpStatus.NOT_FOUND, thrown.getStatusCode()); + Assertions.assertEquals(1, attempts.get()); + } + + @Test + void testWithStatusesToIgnoreAddsAdditionalRetryableStatuses() { + AtomicInteger attempts = new AtomicInteger(); + Supplier operation = () -> { + if (attempts.incrementAndGet() < 2) { + throw new CloudOperationException(HttpStatus.NOT_FOUND); + } + return "ok"; + }; + + executor = (ResilientCloudOperationExecutor) executor.withStatusesToIgnore(HttpStatus.NOT_FOUND); + + String result = executor.execute(operation); + + Assertions.assertEquals("ok", result); + Assertions.assertEquals(2, attempts.get()); + } + + @Test + void testFluentBuildersReturnSameTypeForChaining() { + ResilientCloudOperationExecutor chained = new ResilientCloudOperationExecutor().withRetryCount(2) + .withWaitTimeBetweenRetriesInMillis(0) + .withStatusesToIgnore(HttpStatus.I_AM_A_TEAPOT); + Assertions.assertNotNull(chained); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientOperationExecutorTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientOperationExecutorTest.java new file mode 100644 index 0000000000..26e8af1902 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/ResilientOperationExecutorTest.java @@ -0,0 +1,82 @@ +package org.cloudfoundry.multiapps.controller.client.util; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ResilientOperationExecutorTest { + + private ResilientOperationExecutor executor; + + @BeforeEach + void setUp() { + executor = new ResilientOperationExecutor().withRetryCount(3) + .withWaitTimeBetweenRetriesInMillis(0); + } + + @Test + void testExecuteSupplierReturnsValueWhenOperationSucceedsFirstTry() { + Supplier operation = () -> "ok"; + + String result = executor.execute(operation); + + Assertions.assertEquals("ok", result); + } + + @Test + void testExecuteSupplierRetriesUntilOperationSucceeds() { + AtomicInteger attempts = new AtomicInteger(); + Supplier operation = () -> { + if (attempts.incrementAndGet() < 2) { + throw new RuntimeException("transient"); + } + return "ok-after-retry"; + }; + + String result = executor.execute(operation); + + Assertions.assertEquals("ok-after-retry", result); + Assertions.assertEquals(2, attempts.get()); + } + + @Test + void testExecuteSupplierPropagatesExceptionWhenRetriesExhausted() { + AtomicInteger attempts = new AtomicInteger(); + Supplier operation = () -> { + attempts.incrementAndGet(); + throw new RuntimeException("permanent"); + }; + + RuntimeException thrown = Assertions.assertThrows(RuntimeException.class, () -> executor.execute(operation)); + Assertions.assertEquals("permanent", thrown.getMessage()); + Assertions.assertEquals(3, attempts.get()); + } + + @Test + void testExecuteRunnableInvokesOperation() { + AtomicInteger attempts = new AtomicInteger(); + + executor.execute((Runnable) attempts::incrementAndGet); + + Assertions.assertEquals(1, attempts.get()); + } + + @Test + void testExecuteCheckedSupplierRetriesAndReturnsValue() throws Exception { + AtomicInteger attempts = new AtomicInteger(); + CheckedSupplier operation = () -> { + if (attempts.incrementAndGet() < 2) { + throw new Exception("checked-transient"); + } + return "ok-checked"; + }; + + String result = executor.execute(operation); + + Assertions.assertEquals("ok-checked", result); + Assertions.assertEquals(2, attempts.get()); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/TokenPropertiesTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/TokenPropertiesTest.java new file mode 100644 index 0000000000..382ac0afea --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/util/TokenPropertiesTest.java @@ -0,0 +1,82 @@ +package org.cloudfoundry.multiapps.controller.client.util; + +import java.util.HashMap; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.client.facade.oauth2.OAuth2AccessTokenWithAdditionalInfo; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class TokenPropertiesTest { + + @Mock + private OAuth2AccessTokenWithAdditionalInfo token; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testFromTokenReadsAllAdditionalInfoKeys() { + Mockito.when(token.getAdditionalInfo()) + .thenReturn(Map.of(TokenProperties.CLIENT_ID_KEY, "c1", + TokenProperties.USER_ID_KEY, "u1", + TokenProperties.USER_NAME_KEY, "alice")); + + TokenProperties props = TokenProperties.fromToken(token); + + Assertions.assertEquals("c1", props.getClientId()); + Assertions.assertEquals("u1", props.getUserId()); + Assertions.assertEquals("alice", props.getUserName()); + } + + @Test + void testFromTokenReturnsNullsWhenAllKeysMissing() { + Mockito.when(token.getAdditionalInfo()) + .thenReturn(Map.of()); + + TokenProperties props = TokenProperties.fromToken(token); + + Assertions.assertNull(props.getClientId()); + Assertions.assertNull(props.getUserId()); + Assertions.assertNull(props.getUserName()); + } + + @Test + void testFromTokenReturnsNullForIndividualMissingKeys() { + Map info = new HashMap<>(); + info.put(TokenProperties.CLIENT_ID_KEY, "c1"); + Mockito.when(token.getAdditionalInfo()) + .thenReturn(info); + + TokenProperties props = TokenProperties.fromToken(token); + + Assertions.assertEquals("c1", props.getClientId()); + Assertions.assertNull(props.getUserId()); + Assertions.assertNull(props.getUserName()); + } + + @Test + void testFromTokenThrowsWhenAdditionalInfoIsNull() { + Mockito.when(token.getAdditionalInfo()) + .thenReturn(null); + + Assertions.assertThrows(NullPointerException.class, () -> TokenProperties.fromToken(token)); + } + + @Test + void testFromTokenThrowsWhenValueIsNotAString() { + Map info = new HashMap<>(); + info.put(TokenProperties.CLIENT_ID_KEY, Integer.valueOf(42)); + Mockito.when(token.getAdditionalInfo()) + .thenReturn(info); + + Assertions.assertThrows(ClassCastException.class, () -> TokenProperties.fromToken(token)); + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ApplicationConfigurationAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ApplicationConfigurationAuditLogTest.java new file mode 100644 index 0000000000..cd892081ec --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ApplicationConfigurationAuditLogTest.java @@ -0,0 +1,37 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class ApplicationConfigurationAuditLogTest { + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private ApplicationConfigurationAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new ApplicationConfigurationAuditLog(auditLoggingFacade); + } + + @Test + void testLogEnvironmentVariableReadEmitsDataAccess() { + auditLog.logEnvironmentVariableRead("MY_ENV", "the-value", "space-guid"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + Assertions.assertEquals("space-guid", captor.getValue() + .getSpaceId()); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuthenticationAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuthenticationAuditLogTest.java new file mode 100644 index 0000000000..8503c768ca --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/AuthenticationAuditLogTest.java @@ -0,0 +1,51 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class AuthenticationAuditLogTest { + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private AuthenticationAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new AuthenticationAuditLog(auditLoggingFacade); + } + + @Test + void testLogFetchTokenAttemptEmitsSecurityIncident() { + auditLog.logFetchTokenAttempt("client-1", "space-guid", "service-1"); + + AuditLogConfiguration captured = captureSecurityIncident(); + Assertions.assertEquals("client-1", captured.getUserId()); + Assertions.assertEquals("space-guid", captured.getSpaceId()); + } + + @Test + void testLogFailedToFetchTokenAttemptEmitsSecurityIncident() { + auditLog.logFailedToFetchTokenAttempt("client-1", "space-guid", "service-1"); + + AuditLogConfiguration captured = captureSecurityIncident(); + Assertions.assertEquals("client-1", captured.getUserId()); + Assertions.assertEquals("space-guid", captured.getSpaceId()); + } + + private AuditLogConfiguration captureSecurityIncident() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logSecurityIncident(captor.capture()); + return captor.getValue(); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationEntryServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationEntryServiceAuditLogTest.java new file mode 100644 index 0000000000..bf457c0f56 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationEntryServiceAuditLogTest.java @@ -0,0 +1,83 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.cloudfoundry.multiapps.mta.model.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class ConfigurationEntryServiceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space-guid"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private ConfigurationEntryServiceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new ConfigurationEntryServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogAddConfigurationEntryEmitsConfigurationCreate() { + ConfigurationEntry entry = new ConfigurationEntry("nid", "pid", Version.parseVersion("1.2.3"), "ns", + new CloudTarget("the-org", "the-space"), "content", java.util.List.of(), + SPACE_ID, "content-id"); + + auditLog.logAddConfigurationEntry(USERNAME, SPACE_ID, entry); + + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ConfigurationChangeActions.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), actionCaptor.capture()); + Assertions.assertEquals(ConfigurationChangeActions.CONFIGURATION_CREATE, actionCaptor.getValue()); + + AuditLogConfiguration captured = configCaptor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "providerId", "pid")); + Assertions.assertTrue(containsParameter(captured, "providerNid", "nid")); + Assertions.assertTrue(containsParameter(captured, "providerVersion", "1.2.3")); + Assertions.assertTrue(containsParameter(captured, "providerNamespace", "ns")); + Assertions.assertTrue(containsParameter(captured, "providerTarget", "the-org/the-space")); + Assertions.assertTrue(containsParameter(captured, "providerContent", "content")); + Assertions.assertTrue(containsParameter(captured, "providerContentId", "content-id")); + } + + @Test + void testLogUpdateConfigurationEntryDelegatesToFourArgFacadeMethod() { + ConfigurationEntry oldEntry = new ConfigurationEntry("old-id", Version.parseVersion("1.0.0")); + ConfigurationEntry newEntry = new ConfigurationEntry("new-id", Version.parseVersion("2.0.0")); + + auditLog.logUpdateConfigurationEntry(USERNAME, SPACE_ID, oldEntry, newEntry); + + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), Mockito.eq(ConfigurationChangeActions.CONFIGURATION_UPDATE), + Mockito.eq(oldEntry), Mockito.eq(newEntry)); + Assertions.assertEquals(USERNAME, configCaptor.getValue() + .getUserId()); + Assertions.assertEquals(SPACE_ID, configCaptor.getValue() + .getSpaceId()); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationSubscriptionServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationSubscriptionServiceAuditLogTest.java new file mode 100644 index 0000000000..454d6dd85d --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/ConfigurationSubscriptionServiceAuditLogTest.java @@ -0,0 +1,75 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class ConfigurationSubscriptionServiceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space-guid"; + private static final String MTA_ID = "my-mta"; + private static final String APP_NAME = "app"; + private static final long SUBSCRIPTION_ID = 17L; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private ConfigurationSubscriptionServiceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new ConfigurationSubscriptionServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogAddConfigurationSubscriptionEmitsConfigurationCreate() { + ConfigurationSubscription subscription = new ConfigurationSubscription(SUBSCRIPTION_ID, MTA_ID, SPACE_ID, APP_NAME, null, null, + null, null, null); + + auditLog.logAddConfigurationSubscription(USERNAME, SPACE_ID, subscription); + + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ConfigurationChangeActions.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), actionCaptor.capture()); + Assertions.assertEquals(ConfigurationChangeActions.CONFIGURATION_CREATE, actionCaptor.getValue()); + + AuditLogConfiguration captured = configCaptor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "applicationId", APP_NAME)); + Assertions.assertTrue(containsParameter(captured, "mtaId", MTA_ID)); + Assertions.assertTrue(containsParameter(captured, "subscriptionId", String.valueOf(SUBSCRIPTION_ID))); + } + + @Test + void testLogUpdateConfigurationSubscriptionDelegatesToFourArgFacadeMethod() { + ConfigurationSubscription oldSub = new ConfigurationSubscription(1L, MTA_ID, SPACE_ID, "old-app", null, null, null, null, null); + ConfigurationSubscription newSub = new ConfigurationSubscription(2L, MTA_ID, SPACE_ID, "new-app", null, null, null, null, null); + + auditLog.logUpdateConfigurationSubscription(USERNAME, SPACE_ID, oldSub, newSub); + + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(Mockito.any(AuditLogConfiguration.class), + Mockito.eq(ConfigurationChangeActions.CONFIGURATION_UPDATE), Mockito.eq(oldSub), + Mockito.eq(newSub)); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CsrfTokenApiServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CsrfTokenApiServiceAuditLogTest.java new file mode 100644 index 0000000000..aea5a7481c --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/CsrfTokenApiServiceAuditLogTest.java @@ -0,0 +1,39 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class CsrfTokenApiServiceAuditLogTest { + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private CsrfTokenApiServiceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new CsrfTokenApiServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogGetInfoEmitsDataAccessForUser() { + auditLog.logGetInfo("alice"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + Assertions.assertEquals("alice", captor.getValue() + .getUserId()); + Assertions.assertEquals("", captor.getValue() + .getSpaceId()); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FilesApiServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FilesApiServiceAuditLogTest.java new file mode 100644 index 0000000000..f6b0bc89aa --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FilesApiServiceAuditLogTest.java @@ -0,0 +1,110 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.math.BigInteger; + +import org.cloudfoundry.multiapps.controller.api.model.FileMetadata; +import org.cloudfoundry.multiapps.controller.api.model.ImmutableFileMetadata; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class FilesApiServiceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space-guid"; + private static final String NAMESPACE = "ns"; + private static final String JOB_ID = "job-1"; + private static final String FILE_URL = "https://example.invalid/foo.mtar"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private FilesApiServiceAuditLog filesApiServiceAuditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + filesApiServiceAuditLog = new FilesApiServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogGetFilesEmitsDataAccessWithNamespace() { + filesApiServiceAuditLog.logGetFiles(USERNAME, SPACE_ID, NAMESPACE); + + AuditLogConfiguration captured = captureDataAccess(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "namespace", NAMESPACE)); + } + + @Test + void testLogUploadFileEmitsConfigurationCreateWithFileMetadata() { + FileMetadata metadata = ImmutableFileMetadata.builder() + .id("file-1") + .name("foo.mtar") + .size(BigInteger.valueOf(2048L)) + .digest("abc") + .digestAlgorithm("SHA-256") + .space(SPACE_ID) + .namespace(NAMESPACE) + .build(); + + filesApiServiceAuditLog.logUploadFile(USERNAME, SPACE_ID, metadata); + + AuditLogConfiguration captured = captureConfigurationChange(ConfigurationChangeActions.CONFIGURATION_CREATE); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertTrue(containsParameter(captured, "fileId", "file-1")); + Assertions.assertTrue(containsParameter(captured, "digest", "abc")); + Assertions.assertTrue(containsParameter(captured, "digestAlgorithm", "SHA-256")); + Assertions.assertTrue(containsParameter(captured, "size", "2048")); + Assertions.assertTrue(containsParameter(captured, "namespace", NAMESPACE)); + } + + @Test + void testLogStartUploadFromUrlEmitsConfigurationCreateWithFileUrl() { + filesApiServiceAuditLog.logStartUploadFromUrl(USERNAME, SPACE_ID, FILE_URL); + + AuditLogConfiguration captured = captureConfigurationChange(ConfigurationChangeActions.CONFIGURATION_CREATE); + Assertions.assertTrue(containsParameter(captured, "fileUrl", FILE_URL)); + } + + @Test + void testLogGetUploadFromUrlJobEmitsDataAccessWithNamespaceAndJobId() { + filesApiServiceAuditLog.logGetUploadFromUrlJob(USERNAME, SPACE_ID, NAMESPACE, JOB_ID); + + AuditLogConfiguration captured = captureDataAccess(); + Assertions.assertTrue(containsParameter(captured, "namespace", NAMESPACE)); + Assertions.assertTrue(containsParameter(captured, "jobId", JOB_ID)); + } + + private AuditLogConfiguration captureDataAccess() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + return captor.getValue(); + } + + private AuditLogConfiguration captureConfigurationChange(ConfigurationChangeActions expectedAction) { + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ConfigurationChangeActions.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), actionCaptor.capture()); + Assertions.assertEquals(expectedAction, actionCaptor.getValue()); + return configCaptor.getValue(); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlmpResourceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlmpResourceAuditLogTest.java new file mode 100644 index 0000000000..36829f862f --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlmpResourceAuditLogTest.java @@ -0,0 +1,78 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class FlowableSlmpResourceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space"; + private static final String ACTION = "do-thing"; + private static final String CONFIGURATION = "the-config"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private FlowableSlmpResourceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new FlowableSlmpResourceAuditLog(auditLoggingFacade); + } + + @Test + void testAuditLogConfigurationChangeForwardsActionAndCapturedFields() { + auditLog.auditLogConfigurationChange(USERNAME, SPACE_ID, ACTION, CONFIGURATION, + ConfigurationChangeActions.CONFIGURATION_CREATE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(captor.capture(), Mockito.eq(ConfigurationChangeActions.CONFIGURATION_CREATE)); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertEquals(CONFIGURATION, captured.getConfigurationName()); + } + + @Test + void testAuditLogActionPerformedForwardsCapturedFieldsToDataAccess() { + auditLog.auditLogActionPerformed(USERNAME, SPACE_ID, ACTION, CONFIGURATION); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertEquals(CONFIGURATION, captured.getConfigurationName()); + } + + @Test + void testAuditLogActionPerformedWithParametersIncludesParameters() { + auditLog.auditLogActionPerformed(USERNAME, SPACE_ID, ACTION, CONFIGURATION, Map.of("key", "value")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertTrue(captured.getConfigurationIdentifiers() + .stream() + .anyMatch(id -> "key".equals(id.getIdentifierName()) && "value".equals(id.getIdentifierValue()))); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlppResourceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlppResourceAuditLogTest.java new file mode 100644 index 0000000000..7df908c7f7 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/FlowableSlppResourceAuditLogTest.java @@ -0,0 +1,94 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class FlowableSlppResourceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space"; + private static final String ACTION = "do-thing"; + private static final String CONFIGURATION = "the-config"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private FlowableSlppResourceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new FlowableSlppResourceAuditLog(auditLoggingFacade); + } + + @Test + void testAuditLogConfigurationChangeForwardsActionAndCapturedFields() { + auditLog.auditLogConfigurationChange(USERNAME, SPACE_ID, ACTION, CONFIGURATION, + ConfigurationChangeActions.CONFIGURATION_DELETE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(captor.capture(), Mockito.eq(ConfigurationChangeActions.CONFIGURATION_DELETE)); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertEquals(CONFIGURATION, captured.getConfigurationName()); + } + + @Test + void testAuditLogConfigurationChangeWithParametersIncludesParameters() { + auditLog.auditLogConfigurationChange(USERNAME, SPACE_ID, ACTION, CONFIGURATION, Map.of("k", "v"), + ConfigurationChangeActions.CONFIGURATION_UPDATE); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(captor.capture(), Mockito.eq(ConfigurationChangeActions.CONFIGURATION_UPDATE)); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertTrue(captured.getConfigurationIdentifiers() + .stream() + .anyMatch(id -> "k".equals(id.getIdentifierName()) && "v".equals(id.getIdentifierValue()))); + } + + @Test + void testAuditLogActionPerformedForwardsCapturedFieldsToDataAccess() { + auditLog.auditLogActionPerformed(USERNAME, SPACE_ID, ACTION, CONFIGURATION); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertEquals(CONFIGURATION, captured.getConfigurationName()); + } + + @Test + void testAuditLogActionPerformedWithParametersIncludesParameters() { + auditLog.auditLogActionPerformed(USERNAME, SPACE_ID, ACTION, CONFIGURATION, Map.of("k", "v")); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + AuditLogConfiguration captured = captor.getValue(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(ACTION, captured.getPerformedAction()); + Assertions.assertTrue(captured.getConfigurationIdentifiers() + .stream() + .anyMatch(id -> "k".equals(id.getIdentifierName()) && "v".equals(id.getIdentifierValue()))); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/InfoApiServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/InfoApiServiceAuditLogTest.java new file mode 100644 index 0000000000..d00181413a --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/InfoApiServiceAuditLogTest.java @@ -0,0 +1,37 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class InfoApiServiceAuditLogTest { + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private InfoApiServiceAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new InfoApiServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogGetInfoEmitsDataAccessForUser() { + auditLog.logGetInfo("alice"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + Assertions.assertEquals("alice", captor.getValue() + .getUserId()); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/LoginAttemptAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/LoginAttemptAuditLogTest.java new file mode 100644 index 0000000000..fef7346958 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/LoginAttemptAuditLogTest.java @@ -0,0 +1,41 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class LoginAttemptAuditLogTest { + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private LoginAttemptAuditLog auditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + auditLog = new LoginAttemptAuditLog(auditLoggingFacade); + } + + @Test + void testLogLoginAttemptEmitsSecurityIncident() { + auditLog.logLoginAttempt("alice", "space-guid", "User {0} attempted login on space {1}", "the-config"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logSecurityIncident(captor.capture()); + Assertions.assertEquals("alice", captor.getValue() + .getUserId()); + Assertions.assertEquals("space-guid", captor.getValue() + .getSpaceId()); + Assertions.assertEquals("User alice attempted login on space space-guid", captor.getValue() + .getPerformedAction()); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtaConfigurationPurgerAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtaConfigurationPurgerAuditLogTest.java new file mode 100644 index 0000000000..cd10bcf23a --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtaConfigurationPurgerAuditLogTest.java @@ -0,0 +1,147 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import java.time.LocalDateTime; + +import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; +import org.cloudfoundry.multiapps.controller.api.model.Operation; +import org.cloudfoundry.multiapps.controller.api.model.ProcessType; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.cloudfoundry.multiapps.controller.persistence.dto.BackupDescriptor; +import org.cloudfoundry.multiapps.controller.persistence.dto.ImmutableBackupDescriptor; +import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationSubscription; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class MtaConfigurationPurgerAuditLogTest { + + private static final String SPACE_GUID = "space-guid"; + private static final String MTA_ID = "my-mta"; + private static final String APP_NAME = "app"; + private static final long SUBSCRIPTION_ID = 17L; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private MtaConfigurationPurgerAuditLog purgerAuditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + purgerAuditLog = new MtaConfigurationPurgerAuditLog(auditLoggingFacade); + } + + @Test + void testLogDeleteSubscriptionWithSubscriptionEmitsConfigurationDelete() { + ConfigurationSubscription subscription = new ConfigurationSubscription(SUBSCRIPTION_ID, MTA_ID, SPACE_GUID, APP_NAME, null, null, + null, null, null); + + purgerAuditLog.logDeleteSubscription(SPACE_GUID, subscription); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertEquals(SPACE_GUID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "applicationId", APP_NAME)); + Assertions.assertTrue(containsParameter(captured, "mtaId", MTA_ID)); + Assertions.assertTrue(containsParameter(captured, "subscriptionId", String.valueOf(SUBSCRIPTION_ID))); + } + + @Test + void testLogDeleteSubscriptionWithoutSubscriptionEmitsConfigurationDelete() { + purgerAuditLog.logDeleteSubscription(SPACE_GUID); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertEquals(SPACE_GUID, captured.getSpaceId()); + Assertions.assertFalse(containsParameter(captured, "subscriptionId", String.valueOf(SUBSCRIPTION_ID))); + } + + @Test + void testLogDeleteEntryWithEntryEmitsAllProviderIdentifiers() { + CloudTarget targetSpace = new CloudTarget("my-org", "my-space"); + ConfigurationEntry entry = new ConfigurationEntry("nid", "pid", Version.parseVersion("1.0.0"), "ns", targetSpace, + "content", java.util.List.of(), SPACE_GUID, "content-id"); + + purgerAuditLog.logDeleteEntry(SPACE_GUID, entry); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertTrue(containsParameter(captured, "providerId", "pid")); + Assertions.assertTrue(containsParameter(captured, "providerNid", "nid")); + Assertions.assertTrue(containsParameter(captured, "providerVersion", "1.0.0")); + Assertions.assertTrue(containsParameter(captured, "providerNamespace", "ns")); + Assertions.assertTrue(containsParameter(captured, "providerTarget", "my-org/my-space")); + Assertions.assertTrue(containsParameter(captured, "providerContent", "content")); + Assertions.assertTrue(containsParameter(captured, "providerContentId", "content-id")); + } + + @Test + void testLogDeleteEntryWithoutEntryEmitsConfigurationDelete() { + purgerAuditLog.logDeleteEntry(SPACE_GUID); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertEquals(SPACE_GUID, captured.getSpaceId()); + Assertions.assertFalse(containsParameter(captured, "providerId", "pid")); + } + + @Test + void testLogDeleteOperationEmitsOperationIdentifiers() { + Operation operation = ImmutableOperation.builder() + .processId("p-1") + .processType(ProcessType.DEPLOY) + .user("alice") + .state(Operation.State.FINISHED) + .build(); + + purgerAuditLog.logDeleteOperation(SPACE_GUID, operation); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertEquals("alice", captured.getUserId()); + Assertions.assertEquals(SPACE_GUID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "processType", ProcessType.DEPLOY.toString())); + Assertions.assertTrue(containsParameter(captured, "state", Operation.State.FINISHED.toString())); + } + + @Test + void testLogDeleteBackupDescriptorEmitsMtaAndTimestampIdentifiers() { + LocalDateTime timestamp = LocalDateTime.of(2026, 1, 2, 3, 4, 5); + BackupDescriptor descriptor = ImmutableBackupDescriptor.builder() + .mtaId(MTA_ID) + .mtaVersion("1.2.3") + .spaceId(SPACE_GUID) + .timestamp(timestamp) + .descriptor(DeploymentDescriptor.createV3()) + .build(); + + purgerAuditLog.logDeleteBackupDescriptor(SPACE_GUID, descriptor); + + AuditLogConfiguration captured = captureDeleteConfig(); + Assertions.assertEquals(SPACE_GUID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "mtaId", MTA_ID)); + Assertions.assertTrue(containsParameter(captured, "storedAt", timestamp.toString())); + } + + private AuditLogConfiguration captureDeleteConfig() { + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ConfigurationChangeActions.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), actionCaptor.capture()); + Assertions.assertEquals(ConfigurationChangeActions.CONFIGURATION_DELETE, actionCaptor.getValue()); + return configCaptor.getValue(); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtasApiServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtasApiServiceAuditLogTest.java new file mode 100644 index 0000000000..192421f5e3 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/MtasApiServiceAuditLogTest.java @@ -0,0 +1,72 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class MtasApiServiceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space-guid"; + private static final String MTA_ID = "my-mta"; + private static final String NAMESPACE = "ns"; + private static final String NAME = "the-name"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private MtasApiServiceAuditLog mtasApiServiceAuditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + mtasApiServiceAuditLog = new MtasApiServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogGetMtasWithoutFiltersEmitsDataAccess() { + mtasApiServiceAuditLog.logGetMtas(USERNAME, SPACE_ID); + + AuditLogConfiguration captured = captureDataAccess(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + } + + @Test + void testLogGetMtaEmitsDataAccessWithMtaName() { + mtasApiServiceAuditLog.logGetMta(USERNAME, SPACE_ID, MTA_ID); + + AuditLogConfiguration captured = captureDataAccess(); + Assertions.assertTrue(containsParameter(captured, "mtaName", MTA_ID)); + } + + @Test + void testLogGetMtasWithFiltersEmitsDataAccessWithNameAndNamespace() { + mtasApiServiceAuditLog.logGetMtas(USERNAME, SPACE_ID, NAMESPACE, NAME); + + AuditLogConfiguration captured = captureDataAccess(); + Assertions.assertTrue(containsParameter(captured, "namespace", NAMESPACE)); + Assertions.assertTrue(containsParameter(captured, "name", NAME)); + } + + private AuditLogConfiguration captureDataAccess() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + return captor.getValue(); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/OperationsApiServiceAuditLogTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/OperationsApiServiceAuditLogTest.java new file mode 100644 index 0000000000..38351f0eaf --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/auditlogging/OperationsApiServiceAuditLogTest.java @@ -0,0 +1,126 @@ +package org.cloudfoundry.multiapps.controller.core.auditlogging; + +import org.cloudfoundry.multiapps.controller.api.model.ImmutableOperation; +import org.cloudfoundry.multiapps.controller.api.model.Operation; +import org.cloudfoundry.multiapps.controller.api.model.ProcessType; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.AuditLogConfiguration; +import org.cloudfoundry.multiapps.controller.core.auditlogging.model.ConfigurationChangeActions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class OperationsApiServiceAuditLogTest { + + private static final String USERNAME = "alice"; + private static final String SPACE_ID = "space-guid"; + private static final String OPERATION_ID = "op-1"; + private static final String MTA_ID = "my-mta"; + private static final String PROCESS_ID = "process-1"; + + @Mock + private AuditLoggingFacade auditLoggingFacade; + + private OperationsApiServiceAuditLog operationsApiServiceAuditLog; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + operationsApiServiceAuditLog = new OperationsApiServiceAuditLog(auditLoggingFacade); + } + + @Test + void testLogGetOperationsLogsDataAccess() { + operationsApiServiceAuditLog.logGetOperations(USERNAME, SPACE_ID, MTA_ID); + + AuditLogConfiguration captured = captureDataAccessConfig(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertEquals(SPACE_ID, captured.getSpaceId()); + Assertions.assertTrue(containsParameter(captured, "mtaId", MTA_ID)); + } + + @Test + void testLogGetOperationActionsLogsDataAccessWithOperationId() { + operationsApiServiceAuditLog.logGetOperationActions(USERNAME, SPACE_ID, OPERATION_ID); + + AuditLogConfiguration captured = captureDataAccessConfig(); + Assertions.assertEquals(USERNAME, captured.getUserId()); + Assertions.assertTrue(containsParameter(captured, "operationId", OPERATION_ID)); + } + + @Test + void testLogExecuteOperationActionLogsConfigurationCreate() { + operationsApiServiceAuditLog.logExecuteOperationAction(USERNAME, SPACE_ID, OPERATION_ID, "abort"); + + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + ArgumentCaptor actionCaptor = ArgumentCaptor.forClass(ConfigurationChangeActions.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), actionCaptor.capture()); + Assertions.assertEquals(ConfigurationChangeActions.CONFIGURATION_CREATE, actionCaptor.getValue()); + Assertions.assertTrue(containsParameter(configCaptor.getValue(), "actionId", "abort")); + Assertions.assertTrue(containsParameter(configCaptor.getValue(), "operationId", OPERATION_ID)); + } + + @Test + void testLogGetOperationLogsLogsDataAccess() { + operationsApiServiceAuditLog.logGetOperationLogs(USERNAME, SPACE_ID, OPERATION_ID); + + AuditLogConfiguration captured = captureDataAccessConfig(); + Assertions.assertTrue(containsParameter(captured, "operationId", OPERATION_ID)); + } + + @Test + void testLogGetOperationLogContentLogsDataAccessWithLogId() { + operationsApiServiceAuditLog.logGetOperationLogContent(USERNAME, SPACE_ID, OPERATION_ID, "log-1"); + + AuditLogConfiguration captured = captureDataAccessConfig(); + Assertions.assertTrue(containsParameter(captured, "operationId", OPERATION_ID)); + Assertions.assertTrue(containsParameter(captured, "logId", "log-1")); + } + + @Test + void testLogStartOperationLogsConfigurationCreateWithProcessDetails() { + Operation operation = ImmutableOperation.builder() + .processId(PROCESS_ID) + .processType(ProcessType.DEPLOY) + .mtaId(MTA_ID) + .build(); + + operationsApiServiceAuditLog.logStartOperation(USERNAME, SPACE_ID, operation); + + ArgumentCaptor configCaptor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logConfigurationChangeAuditLog(configCaptor.capture(), Mockito.eq(ConfigurationChangeActions.CONFIGURATION_CREATE)); + Assertions.assertTrue(containsParameter(configCaptor.getValue(), "processId", PROCESS_ID)); + Assertions.assertTrue(containsParameter(configCaptor.getValue(), "mtaId", MTA_ID)); + Assertions.assertTrue(containsParameter(configCaptor.getValue(), "processType", ProcessType.DEPLOY.getName())); + } + + @Test + void testLogGetOperationLogsDataAccessWithEmbed() { + operationsApiServiceAuditLog.logGetOperation(USERNAME, SPACE_ID, OPERATION_ID, "messages"); + + AuditLogConfiguration captured = captureDataAccessConfig(); + Assertions.assertTrue(containsParameter(captured, "operationId", OPERATION_ID)); + Assertions.assertTrue(containsParameter(captured, "embed", "messages")); + } + + private AuditLogConfiguration captureDataAccessConfig() { + ArgumentCaptor captor = ArgumentCaptor.forClass(AuditLogConfiguration.class); + Mockito.verify(auditLoggingFacade) + .logDataAccessAuditLog(captor.capture()); + return captor.getValue(); + } + + private boolean containsParameter(AuditLogConfiguration configuration, String key, String value) { + return configuration.getConfigurationIdentifiers() + .stream() + .anyMatch(identifier -> key.equals(identifier.getIdentifierName()) + && value.equals(identifier.getIdentifierValue())); + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/util/DeployedAfterModulesContentValidatorTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/util/DeployedAfterModulesContentValidatorTest.java new file mode 100644 index 0000000000..11844b27c6 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/cf/util/DeployedAfterModulesContentValidatorTest.java @@ -0,0 +1,153 @@ +package org.cloudfoundry.multiapps.controller.core.cf.util; + +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.common.ContentException; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; + +class DeployedAfterModulesContentValidatorTest { + + @Mock + private CloudControllerClient client; + @Mock + private UserMessageLogger userMessageLogger; + @Mock + private ModuleToDeployHelper moduleToDeployHelper; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testV2ModuleWithUnknownDependencyPasses() { + Module v2Module = Module.createV2() + .setName("legacy"); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, List.of(v2Module)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(v2Module))); + Mockito.verifyNoInteractions(client); + } + + @Test + void testModuleWithoutDeployedAfterPasses() { + Module module = v3Module("a", null); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, List.of(module)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(module))); + } + + @Test + void testDependencyInDeploymentSetPasses() { + Module dep = v3Module("dep", List.of()); + Module module = v3Module("app", List.of("dep")); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, + List.of(module, dep)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(module, dep))); + Mockito.verifyNoInteractions(client); + } + + @Test + void testNonApplicationDependencyOutsideDeploymentEmitsWarningAndPasses() { + Module nonApp = v3Module("non-app", List.of()); + Module module = v3Module("app", List.of("non-app")); + Mockito.when(moduleToDeployHelper.isApplication(nonApp)) + .thenReturn(false); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, + List.of(module, nonApp)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(module))); + Mockito.verify(userMessageLogger) + .warn(Mockito.contains("non-app")); + Mockito.verifyNoInteractions(client); + } + + @Test + void testApplicationDependencyExistingInCfPasses() { + Module externalApp = v3Module("external", List.of()); + Module module = v3Module("app", List.of("external")); + Mockito.when(moduleToDeployHelper.isApplication(externalApp)) + .thenReturn(true); + Mockito.when(client.getApplicationGuid("external")) + .thenReturn(UUID.randomUUID()); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, + List.of(module, externalApp)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(module))); + } + + @Test + void testApplicationDependencyMissingFromCfThrowsContentException() { + Module externalApp = v3Module("missing", List.of()); + Module module = v3Module("app", List.of("missing")); + Mockito.when(moduleToDeployHelper.isApplication(externalApp)) + .thenReturn(true); + Mockito.when(client.getApplicationGuid("missing")) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, + List.of(module, externalApp)); + + ContentException exception = Assertions.assertThrows(ContentException.class, () -> validator.validate(List.of(module))); + Assertions.assertTrue(exception.getMessage() + .contains("app")); + } + + @Test + void testDependencyAbsentFromArchiveTriggersCfLookup() { + // Dependency name not in allMtaModules and not in deployment set -> doesAppExist is the only signal. + Module module = v3Module("app", List.of("standalone")); + Mockito.when(client.getApplicationGuid("standalone")) + .thenReturn(UUID.randomUUID()); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, List.of(module)); + + Assertions.assertDoesNotThrow(() -> validator.validate(List.of(module))); + } + + @Test + void testMultipleUnresolvedDependenciesAreReportedTogether() { + Module a = v3Module("a", List.of("missing-1")); + Module b = v3Module("b", List.of("missing-2")); + Mockito.when(client.getApplicationGuid(Mockito.anyString())) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + DeployedAfterModulesContentValidator validator = new DeployedAfterModulesContentValidator(client, userMessageLogger, + moduleToDeployHelper, List.of(a, b)); + + ContentException exception = Assertions.assertThrows(ContentException.class, () -> validator.validate(List.of(a, b))); + Assertions.assertTrue(exception.getMessage() + .contains("a")); + Assertions.assertTrue(exception.getMessage() + .contains("b")); + } + + private Module v3Module(String name, List deployedAfter) { + Module module = Module.createV3() + .setName(name) + .setType("application"); + if (deployedAfter != null) { + module.setDeployedAfter(deployedAfter); + } + return module; + } + +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaDescriptorMergerTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaDescriptorMergerTest.java new file mode 100644 index 0000000000..3a223e2fe3 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/MtaDescriptorMergerTest.java @@ -0,0 +1,100 @@ +package org.cloudfoundry.multiapps.controller.core.helpers; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.core.cf.CloudHandlerFactory; +import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; +import org.cloudfoundry.multiapps.controller.core.validators.parameters.v2.DescriptorParametersCompatibilityValidator; +import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorMerger; +import org.cloudfoundry.multiapps.mta.handlers.v2.DescriptorValidator; +import org.cloudfoundry.multiapps.mta.mergers.PlatformMerger; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.ExtensionDescriptor; +import org.cloudfoundry.multiapps.mta.model.Platform; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class MtaDescriptorMergerTest { + + @Mock + private CloudHandlerFactory handlerFactory; + @Mock + private Platform platform; + @Mock + private UserMessageLogger userMessageLogger; + @Mock + private DescriptorValidator validator; + @Mock + private DescriptorMerger descriptorMerger; + @Mock + private PlatformMerger platformMerger; + @Mock + private DescriptorParametersCompatibilityValidator compatibilityValidator; + + private DeploymentDescriptor inputDescriptor; + private DeploymentDescriptor mergedDescriptor; + private DeploymentDescriptor compatibleDescriptor; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + inputDescriptor = DeploymentDescriptor.createV3(); + mergedDescriptor = DeploymentDescriptor.createV3(); + compatibleDescriptor = DeploymentDescriptor.createV3(); + + Mockito.when(handlerFactory.getDescriptorValidator()) + .thenReturn(validator); + Mockito.when(handlerFactory.getDescriptorMerger()) + .thenReturn(descriptorMerger); + Mockito.when(handlerFactory.getPlatformMerger(platform)) + .thenReturn(platformMerger); + } + + @Test + void testMergeRunsValidatorsMergerAndPlatformMerger() { + List extensionDescriptors = List.of(); + Mockito.when(descriptorMerger.merge(inputDescriptor, extensionDescriptors)) + .thenReturn(mergedDescriptor); + Mockito.when(handlerFactory.getDescriptorParametersCompatibilityValidator(mergedDescriptor, null)) + .thenReturn(compatibilityValidator); + Mockito.when(compatibilityValidator.validate()) + .thenReturn(compatibleDescriptor); + + MtaDescriptorMerger merger = new MtaDescriptorMerger(handlerFactory, platform); + DeploymentDescriptor result = merger.merge(inputDescriptor, extensionDescriptors, List.of()); + + Assertions.assertSame(compatibleDescriptor, result); + Mockito.verify(validator) + .validateDeploymentDescriptor(inputDescriptor, platform); + Mockito.verify(validator) + .validateExtensionDescriptors(extensionDescriptors, inputDescriptor); + Mockito.verify(validator) + .validateMergedDescriptor(mergedDescriptor); + Mockito.verify(platformMerger) + .mergeInto(mergedDescriptor); + } + + @Test + void testMergeWithUserMessageLoggerLogsDebug() { + List extensionDescriptors = List.of(); + Mockito.when(descriptorMerger.merge(inputDescriptor, extensionDescriptors)) + .thenReturn(mergedDescriptor); + Mockito.when(handlerFactory.getDescriptorParametersCompatibilityValidator(mergedDescriptor, userMessageLogger)) + .thenReturn(compatibilityValidator); + Mockito.when(compatibilityValidator.validate()) + .thenReturn(compatibleDescriptor); + + MtaDescriptorMerger merger = new MtaDescriptorMerger(handlerFactory, platform, userMessageLogger); + DeploymentDescriptor result = merger.merge(inputDescriptor, extensionDescriptors, List.of("password")); + + Assertions.assertSame(compatibleDescriptor, result); + Mockito.verify(userMessageLogger) + .debug(Mockito.anyString(), Mockito.any(Object[].class)); + } + +} diff --git a/multiapps-controller-database-migration/src/test/java/org/cloudfoundry/multiapps/controller/database/migration/client/DatabaseQueryClientTest.java b/multiapps-controller-database-migration/src/test/java/org/cloudfoundry/multiapps/controller/database/migration/client/DatabaseQueryClientTest.java new file mode 100644 index 0000000000..1df048380f --- /dev/null +++ b/multiapps-controller-database-migration/src/test/java/org/cloudfoundry/multiapps/controller/database/migration/client/DatabaseQueryClientTest.java @@ -0,0 +1,255 @@ +package org.cloudfoundry.multiapps.controller.database.migration.client; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.List; + +import org.cloudfoundry.multiapps.controller.database.migration.metadata.DatabaseTableColumnMetadata; +import org.cloudfoundry.multiapps.controller.database.migration.metadata.DatabaseTableData; +import org.cloudfoundry.multiapps.controller.database.migration.metadata.DatabaseTableRowData; +import org.cloudfoundry.multiapps.controller.database.migration.metadata.ImmutableDatabaseTableColumnMetadata; +import org.cloudfoundry.multiapps.controller.database.migration.metadata.ImmutableDatabaseTableData; +import org.cloudfoundry.multiapps.controller.database.migration.metadata.ImmutableDatabaseTableRowData; +import org.cloudfoundry.multiapps.controller.persistence.query.SqlQuery; +import org.cloudfoundry.multiapps.controller.persistence.util.SqlQueryExecutor; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class DatabaseQueryClientTest { + + private static final String TEST_SEQUENCE = "test_seq"; + private static final String TEST_TABLE = "test_table"; + + @Mock + private SqlQueryExecutor sqlQueryExecutor; + @Mock + private Connection connection; + @Mock + private PreparedStatement preparedStatement; + @Mock + private ResultSet resultSet; + @Mock + private ResultSetMetaData resultSetMetaData; + + private DatabaseQueryClient databaseQueryClient; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + databaseQueryClient = new DatabaseQueryClient(sqlQueryExecutor); + } + + @Test + void testGetLastSequenceValueReturnsValueWhenResultSetHasRow() throws SQLException { + Mockito.when(connection.prepareStatement(Mockito.contains(TEST_SEQUENCE))) + .thenReturn(preparedStatement); + Mockito.when(preparedStatement.executeQuery()) + .thenReturn(resultSet); + Mockito.when(resultSet.next()) + .thenReturn(true); + Mockito.when(resultSet.getLong(1)) + .thenReturn(42L); + Mockito.when(sqlQueryExecutor.executeWithAutoCommit(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + long result = databaseQueryClient.getLastSequenceValue(TEST_SEQUENCE); + + Assertions.assertEquals(42L, result); + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(connection) + .prepareStatement(sqlCaptor.capture()); + Assertions.assertTrue(sqlCaptor.getValue() + .contains(TEST_SEQUENCE)); + } + + @Test + void testGetLastSequenceValueReturnsZeroWhenResultSetIsEmpty() throws SQLException { + Mockito.when(connection.prepareStatement(Mockito.anyString())) + .thenReturn(preparedStatement); + Mockito.when(preparedStatement.executeQuery()) + .thenReturn(resultSet); + Mockito.when(resultSet.next()) + .thenReturn(false); + Mockito.when(sqlQueryExecutor.executeWithAutoCommit(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + long result = databaseQueryClient.getLastSequenceValue(TEST_SEQUENCE); + + Assertions.assertEquals(0L, result); + } + + @Test + void testUpdateSequenceExecutesSetvalQuery() throws SQLException { + Mockito.when(connection.prepareStatement(Mockito.anyString())) + .thenReturn(preparedStatement); + Mockito.when(sqlQueryExecutor.executeWithAutoCommit(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + databaseQueryClient.updateSequence(TEST_SEQUENCE, 100L); + + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + Mockito.verify(connection) + .prepareStatement(sqlCaptor.capture()); + Assertions.assertTrue(sqlCaptor.getValue() + .contains("setval")); + Assertions.assertTrue(sqlCaptor.getValue() + .contains(TEST_SEQUENCE)); + Assertions.assertTrue(sqlCaptor.getValue() + .contains("100")); + Mockito.verify(preparedStatement) + .executeQuery(); + } + + @Test + void testExtractTableDataBuildsTableDataFromResultSet() throws SQLException { + Mockito.when(connection.prepareStatement(Mockito.contains(TEST_TABLE))) + .thenReturn(preparedStatement); + Mockito.when(preparedStatement.executeQuery()) + .thenReturn(resultSet); + Mockito.when(resultSet.getMetaData()) + .thenReturn(resultSetMetaData); + Mockito.when(resultSetMetaData.getColumnCount()) + .thenReturn(2); + Mockito.when(resultSetMetaData.getColumnName(1)) + .thenReturn("id"); + Mockito.when(resultSetMetaData.getColumnName(2)) + .thenReturn("name"); + Mockito.when(resultSetMetaData.getColumnTypeName(1)) + .thenReturn("BIGINT"); + Mockito.when(resultSetMetaData.getColumnTypeName(2)) + .thenReturn("VARCHAR"); + Mockito.when(resultSet.next()) + .thenReturn(true, true, false); + Mockito.when(resultSet.getObject("id")) + .thenReturn(1L, 2L); + Mockito.when(resultSet.getObject("name")) + .thenReturn("alice", "bob"); + Mockito.when(sqlQueryExecutor.executeWithAutoCommit(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + DatabaseTableData tableData = databaseQueryClient.extractTableData(TEST_TABLE); + + Assertions.assertEquals(TEST_TABLE, tableData.getTableName()); + List columns = tableData.getTableColumnsMetadata(); + Assertions.assertEquals(2, columns.size()); + Assertions.assertEquals("id", columns.get(0) + .getColumnName()); + Assertions.assertEquals("BIGINT", columns.get(0) + .getColumnType()); + Assertions.assertEquals("name", columns.get(1) + .getColumnName()); + Assertions.assertEquals("VARCHAR", columns.get(1) + .getColumnType()); + + List rows = tableData.getTableRowsData(); + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals(1L, rows.get(0) + .getValues() + .get("id")); + Assertions.assertEquals("alice", rows.get(0) + .getValues() + .get("name")); + Assertions.assertEquals(2L, rows.get(1) + .getValues() + .get("id")); + Assertions.assertEquals("bob", rows.get(1) + .getValues() + .get("name")); + } + + @Test + void testExtractTableDataReturnsEmptyRowsWhenResultSetEmpty() throws SQLException { + Mockito.when(connection.prepareStatement(Mockito.anyString())) + .thenReturn(preparedStatement); + Mockito.when(preparedStatement.executeQuery()) + .thenReturn(resultSet); + Mockito.when(resultSet.getMetaData()) + .thenReturn(resultSetMetaData); + Mockito.when(resultSetMetaData.getColumnCount()) + .thenReturn(1); + Mockito.when(resultSetMetaData.getColumnName(1)) + .thenReturn("id"); + Mockito.when(resultSetMetaData.getColumnTypeName(1)) + .thenReturn("BIGINT"); + Mockito.when(resultSet.next()) + .thenReturn(false); + Mockito.when(sqlQueryExecutor.executeWithAutoCommit(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + DatabaseTableData tableData = databaseQueryClient.extractTableData(TEST_TABLE); + + Assertions.assertEquals(1, tableData.getTableColumnsMetadata() + .size()); + Assertions.assertTrue(tableData.getTableRowsData() + .isEmpty()); + } + + @Test + void testWriteDataToDataSourcePopulatesAndExecutesInsertForEveryRow() throws SQLException { + DatabaseTableColumnMetadata idColumn = ImmutableDatabaseTableColumnMetadata.builder() + .columnName("id") + .columnType("int8") + .build(); + DatabaseTableColumnMetadata nameColumn = ImmutableDatabaseTableColumnMetadata.builder() + .columnName("name") + .columnType("varchar") + .build(); + DatabaseTableRowData rowOne = ImmutableDatabaseTableRowData.builder() + .putValue("id", 1L) + .putValue("name", "alice") + .build(); + DatabaseTableRowData rowTwo = ImmutableDatabaseTableRowData.builder() + .putValue("id", 2L) + .putValue("name", "bob") + .build(); + DatabaseTableData sourceTableData = ImmutableDatabaseTableData.builder() + .tableName(TEST_TABLE) + .addTableColumnsMetadata(idColumn, nameColumn) + .addTableRowsData(rowOne, rowTwo) + .build(); + String insertQuery = "INSERT INTO test_table (id, name) VALUES (?, ?)"; + + Mockito.when(connection.prepareStatement(insertQuery)) + .thenReturn(preparedStatement); + Mockito.when(sqlQueryExecutor.execute(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + databaseQueryClient.writeDataToDataSource(insertQuery, sourceTableData); + + Mockito.verify(connection, Mockito.times(2)) + .prepareStatement(insertQuery); + Mockito.verify(preparedStatement, Mockito.times(2)) + .executeUpdate(); + } + + @Test + void testWriteDataToDataSourceWithEmptyRowsDoesNotCallPrepareStatement() throws SQLException { + DatabaseTableData emptyTableData = ImmutableDatabaseTableData.builder() + .tableName(TEST_TABLE) + .build(); + Mockito.when(sqlQueryExecutor.execute(Mockito.> any())) + .thenAnswer(invocation -> invocation.> getArgument(0) + .execute(connection)); + + databaseQueryClient.writeDataToDataSource("INSERT INTO test_table VALUES ()", emptyTableData); + + Mockito.verify(connection, Mockito.never()) + .prepareStatement(Mockito.anyString()); + } + +} \ No newline at end of file diff --git a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageException.java b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageException.java index f3da41f199..b53b6e9749 100644 --- a/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageException.java +++ b/multiapps-controller-persistence/src/main/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageException.java @@ -15,6 +15,6 @@ public OperationLogStorageException(String message) { } public OperationLogStorageException(String message, Throwable cause) { - super(message, cause); + super(cause, message); } } diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/OrderDirectionTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/OrderDirectionTest.java new file mode 100644 index 0000000000..f29b6f4199 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/OrderDirectionTest.java @@ -0,0 +1,22 @@ +package org.cloudfoundry.multiapps.controller.persistence; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class OrderDirectionTest { + + @Test + void testValuesContainsBothDirections() { + OrderDirection[] values = OrderDirection.values(); + + Assertions.assertEquals(2, values.length); + Assertions.assertEquals(OrderDirection.ASCENDING, values[0]); + Assertions.assertEquals(OrderDirection.DESCENDING, values[1]); + } + + @Test + void testValueOfRoundTrip() { + Assertions.assertEquals(OrderDirection.ASCENDING, OrderDirection.valueOf("ASCENDING")); + Assertions.assertEquals(OrderDirection.DESCENDING, OrderDirection.valueOf("DESCENDING")); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/CloudTargetTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/CloudTargetTest.java new file mode 100644 index 0000000000..3f625b1f8d --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/CloudTargetTest.java @@ -0,0 +1,93 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class CloudTargetTest { + + private static final String ORG = "my-org"; + private static final String SPACE = "my-space"; + + @Test + void testNoArgConstructorLeavesFieldsNull() { + CloudTarget target = new CloudTarget(); + + Assertions.assertNull(target.getOrganizationName()); + Assertions.assertNull(target.getSpaceName()); + } + + @Test + void testTwoArgConstructorSetsFields() { + CloudTarget target = new CloudTarget(ORG, SPACE); + + Assertions.assertEquals(ORG, target.getOrganizationName()); + Assertions.assertEquals(SPACE, target.getSpaceName()); + } + + @Test + void testSetters() { + CloudTarget target = new CloudTarget(); + + target.setOrganizationName(ORG); + target.setSpaceName(SPACE); + + Assertions.assertEquals(ORG, target.getOrganizationName()); + Assertions.assertEquals(SPACE, target.getSpaceName()); + } + + @Test + void testEqualsAndHashCodeForEquivalentValues() { + CloudTarget a = new CloudTarget(ORG, SPACE); + CloudTarget b = new CloudTarget(ORG, SPACE); + + Assertions.assertEquals(a, b); + Assertions.assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void testEqualsReturnsTrueForSameInstance() { + CloudTarget target = new CloudTarget(ORG, SPACE); + + Assertions.assertEquals(target, target); + } + + @Test + void testEqualsReturnsFalseForDifferentOrg() { + CloudTarget a = new CloudTarget(ORG, SPACE); + CloudTarget b = new CloudTarget("other-org", SPACE); + + Assertions.assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseForDifferentSpace() { + CloudTarget a = new CloudTarget(ORG, SPACE); + CloudTarget b = new CloudTarget(ORG, "other-space"); + + Assertions.assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseForNull() { + CloudTarget target = new CloudTarget(ORG, SPACE); + + Assertions.assertNotEquals(target, null); + } + + @Test + void testEqualsReturnsFalseForOtherType() { + CloudTarget target = new CloudTarget(ORG, SPACE); + + Assertions.assertNotEquals(target, "not-a-cloud-target"); + } + + @Test + void testToStringIncludesBothFields() { + CloudTarget target = new CloudTarget(ORG, SPACE); + + String result = target.toString(); + + Assertions.assertTrue(result.contains(ORG)); + Assertions.assertTrue(result.contains(SPACE)); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/FileInfoTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/FileInfoTest.java new file mode 100644 index 0000000000..5811b54dc5 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/FileInfoTest.java @@ -0,0 +1,64 @@ +package org.cloudfoundry.multiapps.controller.persistence.model; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class FileInfoTest { + + private Path tempDir; + + @BeforeEach + void setUp() throws IOException { + tempDir = Files.createTempDirectory("file-info-test"); + } + + @AfterEach + void tearDown() throws IOException { + try (var stream = Files.walk(tempDir)) { + stream.sorted((a, b) -> b.getNameCount() - a.getNameCount()) + .forEach(p -> p.toFile() + .delete()); + } + } + + @Test + void testGetInputStreamReadsFromUnderlyingFile() throws Exception { + Path file = tempDir.resolve("payload.bin"); + Files.writeString(file, "hello"); + + FileInfo info = ImmutableFileInfo.builder() + .size(BigInteger.valueOf(5)) + .digest("digest") + .digestAlgorithm("SHA-256") + .file(file.toFile()) + .build(); + + try (InputStream in = info.getInputStream()) { + Assertions.assertEquals("hello", new String(in.readAllBytes())); + } + } + + @Test + void testGetInputStreamWrapsFileNotFoundInFileStorageException() { + File missing = tempDir.resolve("does-not-exist").toFile(); + + FileInfo info = ImmutableFileInfo.builder() + .size(BigInteger.ZERO) + .digest("digest") + .digestAlgorithm("SHA-256") + .file(missing) + .build(); + + Assertions.assertThrows(FileStorageException.class, info::getInputStream); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonDeserializerTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonDeserializerTest.java new file mode 100644 index 0000000000..a58be12464 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonDeserializerTest.java @@ -0,0 +1,81 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.adapter; + +import java.io.IOException; + +import org.cloudfoundry.multiapps.common.ParsingException; +import org.cloudfoundry.multiapps.mta.model.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; + +class VersionJsonDeserializerTest { + + @Mock + private JsonParser parser; + @Mock + private ObjectCodec codec; + @Mock + private DeserializationContext context; + + private VersionJsonDeserializer deserializer; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + deserializer = new VersionJsonDeserializer(); + Mockito.when(parser.getCodec()) + .thenReturn(codec); + } + + @Test + void testDeserializeReadsStringAndParsesVersion() throws IOException { + Mockito.when(codec.readValue(parser, String.class)) + .thenReturn("1.2.3"); + + Version result = deserializer.deserialize(parser, context); + + Assertions.assertEquals(Version.parseVersion("1.2.3"), result); + } + + @Test + void testDeserializeThrowsForNullValue() throws IOException { + Mockito.when(codec.readValue(parser, String.class)) + .thenReturn(null); + + Assertions.assertThrows(ParsingException.class, () -> deserializer.deserialize(parser, context)); + } + + @Test + void testDeserializeThrowsForEmptyValue() throws IOException { + Mockito.when(codec.readValue(parser, String.class)) + .thenReturn(""); + + Assertions.assertThrows(ParsingException.class, () -> deserializer.deserialize(parser, context)); + } + + @Test + void testDeserializeThrowsForUnparseableValue() throws IOException { + Mockito.when(codec.readValue(parser, String.class)) + .thenReturn("not-a-version"); + + Assertions.assertThrows(ParsingException.class, () -> deserializer.deserialize(parser, context)); + } + + @Test + void testDeserializePropagatesIOExceptionFromCodec() throws IOException { + IOException failure = new IOException("read failed"); + Mockito.when(codec.readValue(parser, String.class)) + .thenThrow(failure); + + IOException thrown = Assertions.assertThrows(IOException.class, () -> deserializer.deserialize(parser, context)); + Assertions.assertSame(failure, thrown); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonSerializerTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonSerializerTest.java new file mode 100644 index 0000000000..a5ae3505b3 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/adapter/VersionJsonSerializerTest.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.adapter; + +import java.io.IOException; + +import org.cloudfoundry.multiapps.mta.model.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; + +class VersionJsonSerializerTest { + + @Mock + private JsonGenerator generator; + @Mock + private SerializerProvider serializerProvider; + + private VersionJsonSerializer serializer; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + serializer = new VersionJsonSerializer(); + } + + @Test + void testSerializeWritesStringForNonNullVersion() throws IOException { + Version version = Version.parseVersion("1.2.3"); + + serializer.serialize(version, generator, serializerProvider); + + Mockito.verify(generator) + .writeString(version.toString()); + } + + @Test + void testSerializeWritesNullForNullVersion() throws IOException { + serializer.serialize(null, generator, serializerProvider); + + Mockito.verify(generator) + .writeNull(); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/ContentFilterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/ContentFilterTest.java new file mode 100644 index 0000000000..cd48d00399 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/ContentFilterTest.java @@ -0,0 +1,49 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.filters; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ContentFilterTest { + + private final ContentFilter filter = new ContentFilter(); + + @Test + void testEmptyRequiredPropertiesMatchesAnything() { + Assertions.assertTrue(filter.test(null, Collections.emptyMap())); + Assertions.assertTrue(filter.test("{\"a\":1}", Collections.emptyMap())); + } + + @Test + void testNullContentDoesNotMatchNonEmptyRequirements() { + Assertions.assertFalse(filter.test(null, Map.of("a", 1))); + } + + @Test + void testInvalidJsonContentDoesNotMatch() { + Assertions.assertFalse(filter.test("not-json", Map.of("a", 1))); + } + + @Test + void testAllRequiredPropertiesMatch() { + String content = "{\"a\":1,\"b\":\"two\"}"; + + Assertions.assertTrue(filter.test(content, Map.of("a", 1, "b", "two"))); + } + + @Test + void testMissingPropertyFailsMatch() { + String content = "{\"a\":1}"; + + Assertions.assertFalse(filter.test(content, Map.of("a", 1, "b", "two"))); + } + + @Test + void testWrongValueFailsMatch() { + String content = "{\"a\":1}"; + + Assertions.assertFalse(filter.test(content, Map.of("a", 2))); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/TargetWildcardFilterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/TargetWildcardFilterTest.java new file mode 100644 index 0000000000..e08bed786e --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/TargetWildcardFilterTest.java @@ -0,0 +1,51 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.filters; + +import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class TargetWildcardFilterTest { + + private final TargetWildcardFilter filter = new TargetWildcardFilter(); + private final CloudTarget actual = new CloudTarget("my-org", "my-space"); + + @Test + void testNullRequestedTargetMatches() { + Assertions.assertTrue(filter.test(actual, null)); + } + + @Test + void testFullWildcardMatches() { + Assertions.assertTrue(filter.test(actual, new CloudTarget("*", "*"))); + } + + @Test + void testOrgWildcardMatchesWhenSpaceMatches() { + Assertions.assertTrue(filter.test(actual, new CloudTarget("*", "my-space"))); + } + + @Test + void testOrgWildcardFailsWhenSpaceDiffers() { + Assertions.assertFalse(filter.test(actual, new CloudTarget("*", "other-space"))); + } + + @Test + void testSpaceWildcardMatchesWhenOrgMatches() { + Assertions.assertTrue(filter.test(actual, new CloudTarget("my-org", "*"))); + } + + @Test + void testSpaceWildcardFailsWhenOrgDiffers() { + Assertions.assertFalse(filter.test(actual, new CloudTarget("other-org", "*"))); + } + + @Test + void testExactMatch() { + Assertions.assertTrue(filter.test(actual, new CloudTarget("my-org", "my-space"))); + } + + @Test + void testExactMismatch() { + Assertions.assertFalse(filter.test(actual, new CloudTarget("other-org", "other-space"))); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VersionFilterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VersionFilterTest.java new file mode 100644 index 0000000000..fae68f1fe9 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VersionFilterTest.java @@ -0,0 +1,45 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.filters; + +import java.util.Collections; + +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.cloudfoundry.multiapps.mta.model.Version; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class VersionFilterTest { + + private final VersionFilter filter = new VersionFilter(); + + @Test + void testNullRequirementMatchesAnything() { + ConfigurationEntry entry = newEntry(Version.parseVersion("1.0.0")); + + Assertions.assertTrue(filter.test(entry, null)); + } + + @Test + void testNullProviderVersionFailsAnyConstraint() { + ConfigurationEntry entry = newEntry(null); + + Assertions.assertFalse(filter.test(entry, "1.0.0")); + } + + @Test + void testProviderVersionMatchesRequirement() { + ConfigurationEntry entry = newEntry(Version.parseVersion("1.2.3")); + + Assertions.assertTrue(filter.test(entry, "1.2.3")); + } + + @Test + void testProviderVersionFailsRequirement() { + ConfigurationEntry entry = newEntry(Version.parseVersion("1.0.0")); + + Assertions.assertFalse(filter.test(entry, "2.0.0")); + } + + private ConfigurationEntry newEntry(Version providerVersion) { + return new ConfigurationEntry(0, "nid", "id", providerVersion, null, null, null, Collections.emptyList(), null, null); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VisibilityFilterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VisibilityFilterTest.java new file mode 100644 index 0000000000..ba13e3f083 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/model/filters/VisibilityFilterTest.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.persistence.model.filters; + +import java.util.Collections; +import java.util.List; + +import org.cloudfoundry.multiapps.controller.persistence.model.CloudTarget; +import org.cloudfoundry.multiapps.controller.persistence.model.ConfigurationEntry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class VisibilityFilterTest { + + private final VisibilityFilter filter = new VisibilityFilter(); + + @Test + void testEmptyCloudTargetsMatchesAnything() { + ConfigurationEntry entry = newEntry(null, new CloudTarget("my-org", "my-space")); + + Assertions.assertTrue(filter.test(entry, Collections.emptyList())); + } + + @Test + void testExactVisibilityMatch() { + CloudTarget target = new CloudTarget("my-org", "my-space"); + ConfigurationEntry entry = newEntry(List.of(target), new CloudTarget("provider-org", "provider-space")); + + Assertions.assertTrue(filter.test(entry, List.of(target))); + } + + @Test + void testFullWildcardVisibilityMatchesEverything() { + ConfigurationEntry entry = newEntry(List.of(new CloudTarget("*", "*")), new CloudTarget("provider-org", "provider-space")); + + Assertions.assertTrue(filter.test(entry, List.of(new CloudTarget("any-org", "any-space")))); + } + + @Test + void testOrgWildcardMatchesSameSpace() { + ConfigurationEntry entry = newEntry(List.of(new CloudTarget("*", "my-space")), new CloudTarget("provider-org", "provider-space")); + + Assertions.assertTrue(filter.test(entry, List.of(new CloudTarget("any-org", "my-space")))); + } + + @Test + void testSpaceWildcardMatchesSameOrg() { + ConfigurationEntry entry = newEntry(List.of(new CloudTarget("my-org", "*")), new CloudTarget("provider-org", "provider-space")); + + Assertions.assertTrue(filter.test(entry, List.of(new CloudTarget("my-org", "any-space")))); + } + + @Test + void testNullVisibilityFallsBackToProviderOrgWithAnySpace() { + // When visibility is null, fall back to provider's org with "*" space. + ConfigurationEntry entry = newEntry(null, new CloudTarget("provider-org", "provider-space")); + + Assertions.assertTrue(filter.test(entry, List.of(new CloudTarget("provider-org", "any-space")))); + Assertions.assertFalse(filter.test(entry, List.of(new CloudTarget("other-org", "any-space")))); + } + + @Test + void testNoMatchReturnsFalse() { + ConfigurationEntry entry = newEntry(List.of(new CloudTarget("visible-org", "visible-space")), + new CloudTarget("provider-org", "provider-space")); + + Assertions.assertFalse(filter.test(entry, List.of(new CloudTarget("other-org", "other-space")))); + } + + private ConfigurationEntry newEntry(List visibility, CloudTarget targetSpace) { + return new ConfigurationEntry(0, "nid", "id", null, null, targetSpace, null, visibility, null, null); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/AsyncUploadJobsQueryImplTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/AsyncUploadJobsQueryImplTest.java new file mode 100644 index 0000000000..dc4b1ac830 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/impl/AsyncUploadJobsQueryImplTest.java @@ -0,0 +1,185 @@ +package org.cloudfoundry.multiapps.controller.persistence.query.impl; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.criteria.CriteriaBuilder; + +import org.cloudfoundry.multiapps.controller.persistence.dto.AsyncUploadJobDto.AttributeNames; +import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry.State; +import org.cloudfoundry.multiapps.controller.persistence.query.criteria.QueryAttributeRestriction; +import org.cloudfoundry.multiapps.controller.persistence.query.criteria.QueryCriteria; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class AsyncUploadJobsQueryImplTest { + + private AsyncUploadJobsQueryImpl query; + + @BeforeEach + void setUp() { + EntityManager entityManager = Mockito.mock(EntityManager.class); + CriteriaBuilder criteriaBuilder = Mockito.mock(CriteriaBuilder.class); + Mockito.when(entityManager.getCriteriaBuilder()) + .thenReturn(criteriaBuilder); + + query = new AsyncUploadJobsQueryImpl(entityManager); + } + + @Test + void testIdAddsRestrictionAndReturnsThis() { + Assertions.assertSame(query, query.id("job-1")); + + assertSingleRestriction(AttributeNames.ID, "job-1"); + } + + @Test + void testSpaceGuidAddsRestriction() { + query.spaceGuid("space-guid"); + + assertSingleRestriction(AttributeNames.SPACE_GUID, "space-guid"); + } + + @Test + void testStateAddsRestriction() { + query.state(State.RUNNING); + + assertSingleRestriction(AttributeNames.STATE, State.RUNNING); + } + + @Test + void testNamespaceAddsRestrictionWhenNonNull() { + query.namespace("ns"); + + assertSingleRestriction(AttributeNames.NAMESPACE, "ns"); + } + + @Test + void testNamespaceIsNoopForNull() { + Assertions.assertSame(query, query.namespace(null)); + + Assertions.assertEquals(0, getRestrictions().size()); + } + + @Test + void testUserAddsRestriction() { + query.user("alice"); + + assertSingleRestriction(AttributeNames.USER, "alice"); + } + + @Test + void testUrlAddsRestriction() { + query.url("https://example.com/app.mtar"); + + assertSingleRestriction(AttributeNames.URL, "https://example.com/app.mtar"); + } + + @Test + void testWithoutFinishedAtAddsNullRestrictionForFinishedAt() { + query.withoutFinishedAt(); + + assertSingleRestrictionAttribute(AttributeNames.FINISHED_AT); + } + + @Test + void testWithStateAnyOfAddsRestrictionWithListOfStates() { + query.withStateAnyOf(State.RUNNING, State.FINISHED); + + assertSingleRestriction(AttributeNames.STATE, List.of(State.RUNNING, State.FINISHED)); + } + + @Test + void testAddedBeforeAddsRestriction() { + LocalDateTime before = LocalDateTime.parse("2026-05-01T10:00:00"); + + query.addedBefore(before); + + assertSingleRestriction(AttributeNames.ADDED_AT, before); + } + + @Test + void testStartedBeforeAddsRestriction() { + LocalDateTime before = LocalDateTime.parse("2026-05-01T10:00:00"); + + query.startedBefore(before); + + assertSingleRestriction(AttributeNames.STARTED_AT, before); + } + + @Test + void testWithoutStartedAtAddsRestriction() { + query.withoutStartedAt(); + + assertSingleRestrictionAttribute(AttributeNames.STARTED_AT); + } + + @Test + void testWithoutAddedAtAddsRestriction() { + query.withoutAddedAt(); + + assertSingleRestrictionAttribute(AttributeNames.ADDED_AT); + } + + @Test + void testInstanceIndexAddsRestriction() { + query.instanceIndex(3); + + assertSingleRestriction(AttributeNames.INSTANCE_INDEX, 3); + } + + @Test + void testWithFileIdsAddsRestriction() { + List fileIds = List.of("f1", "f2"); + + query.withFileIds(fileIds); + + assertSingleRestriction(AttributeNames.FILE_ID, fileIds); + } + + @Test + void testFluentBuilderCombinesMultipleRestrictions() { + query.id("job-1") + .spaceGuid("space-guid") + .user("alice"); + + Assertions.assertEquals(3, getRestrictions().size()); + } + + private void assertSingleRestriction(String expectedAttribute, Object expectedValue) { + Set> restrictions = getRestrictions(); + Assertions.assertEquals(1, restrictions.size()); + QueryAttributeRestriction restriction = restrictions.iterator() + .next(); + Assertions.assertEquals(expectedAttribute, restriction.getAttribute()); + Assertions.assertEquals(expectedValue, restriction.getValue()); + } + + private void assertSingleRestrictionAttribute(String expectedAttribute) { + Set> restrictions = getRestrictions(); + Assertions.assertEquals(1, restrictions.size()); + QueryAttributeRestriction restriction = restrictions.iterator() + .next(); + Assertions.assertEquals(expectedAttribute, restriction.getAttribute()); + } + + @SuppressWarnings("unchecked") + private Set> getRestrictions() { + try { + Field criteriaField = AsyncUploadJobsQueryImpl.class.getDeclaredField("queryCriteria"); + criteriaField.setAccessible(true); + QueryCriteria criteria = (QueryCriteria) criteriaField.get(query); + + Field restrictionsField = QueryCriteria.class.getDeclaredField("attributeRestrictions"); + restrictionsField.setAccessible(true); + return (Set>) restrictionsField.get(criteria); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/options/StreamFetchingOptionsTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/options/StreamFetchingOptionsTest.java new file mode 100644 index 0000000000..9707338ae5 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/query/options/StreamFetchingOptionsTest.java @@ -0,0 +1,32 @@ +package org.cloudfoundry.multiapps.controller.persistence.query.options; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class StreamFetchingOptionsTest { + + @Test + void testRecordExposesAccessors() { + StreamFetchingOptions options = new StreamFetchingOptions(10L, 200L); + + Assertions.assertEquals(10L, options.startOffset()); + Assertions.assertEquals(200L, options.endOffset()); + } + + @Test + void testRecordEqualsAndHashCodeFromComponents() { + StreamFetchingOptions a = new StreamFetchingOptions(0L, 100L); + StreamFetchingOptions b = new StreamFetchingOptions(0L, 100L); + + Assertions.assertEquals(a, b); + Assertions.assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void testRecordsWithDifferentOffsetsAreNotEqual() { + StreamFetchingOptions a = new StreamFetchingOptions(0L, 100L); + StreamFetchingOptions b = new StreamFetchingOptions(0L, 200L); + + Assertions.assertNotEquals(a, b); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/FileStorageExceptionTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/FileStorageExceptionTest.java new file mode 100644 index 0000000000..18ce9d8c99 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/FileStorageExceptionTest.java @@ -0,0 +1,34 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class FileStorageExceptionTest { + + @Test + void testCauseConstructor() { + Throwable cause = new RuntimeException("boom"); + + FileStorageException e = new FileStorageException(cause); + + Assertions.assertSame(cause, e.getCause()); + } + + @Test + void testMessageConstructor() { + FileStorageException e = new FileStorageException("disk full"); + + Assertions.assertEquals("disk full", e.getMessage()); + Assertions.assertNull(e.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + Throwable cause = new RuntimeException("boom"); + + FileStorageException e = new FileStorageException("disk full", cause); + + Assertions.assertEquals("disk full", e.getMessage()); + Assertions.assertSame(cause, e.getCause()); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/NullProcessLoggerTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/NullProcessLoggerTest.java new file mode 100644 index 0000000000..93f5cb1027 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/NullProcessLoggerTest.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class NullProcessLoggerTest { + + private static final String SPACE_ID = "space-1"; + private static final String PROCESS_ID = "process-1"; + private static final String ACTIVITY_ID = "activity-1"; + + @Test + void testInfoIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.info("anything")); + } + + @Test + void testDebugIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.debug("anything")); + } + + @Test + void testErrorIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.error("anything")); + } + + @Test + void testErrorWithThrowableIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.error("anything", new RuntimeException())); + } + + @Test + void testTraceIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.trace("anything")); + } + + @Test + void testWarnIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.warn("anything")); + } + + @Test + void testWarnWithThrowableIsSafeToCall() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(() -> logger.warn("anything", new RuntimeException())); + } + + @Test + void testLogNullCorrelationIdDoesNotThrow() { + NullProcessLogger logger = newLogger(); + + Assertions.assertDoesNotThrow(logger::logNullCorrelationId); + } + + private NullProcessLogger newLogger() { + return new NullProcessLogger(SPACE_ID, PROCESS_ID, ACTIVITY_ID); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageExceptionTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageExceptionTest.java new file mode 100644 index 0000000000..6dfe85e238 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/services/OperationLogStorageExceptionTest.java @@ -0,0 +1,41 @@ +package org.cloudfoundry.multiapps.controller.persistence.services; + +import org.cloudfoundry.multiapps.common.SLException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class OperationLogStorageExceptionTest { + + @Test + void testIsSlException() { + Assertions.assertTrue(SLException.class.isAssignableFrom(OperationLogStorageException.class)); + } + + @Test + void testCauseConstructorPreservesCauseAndUsesItsMessage() { + Throwable cause = new RuntimeException("boom"); + + OperationLogStorageException e = new OperationLogStorageException(cause); + + Assertions.assertSame(cause, e.getCause()); + Assertions.assertEquals("boom", e.getMessage()); + } + + @Test + void testMessageConstructor() { + OperationLogStorageException e = new OperationLogStorageException("storage failed"); + + Assertions.assertEquals("storage failed", e.getMessage()); + Assertions.assertNull(e.getCause()); + } + + @Test + void testMessageAndCauseConstructorPreservesMessageAndCause() { + Throwable cause = new RuntimeException("boom"); + + OperationLogStorageException e = new OperationLogStorageException("storage failed", cause); + + Assertions.assertEquals("storage failed", e.getMessage()); + Assertions.assertSame(cause, e.getCause()); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ClientKeyConfigurationHandlerTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ClientKeyConfigurationHandlerTest.java new file mode 100644 index 0000000000..7a345b3c04 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ClientKeyConfigurationHandlerTest.java @@ -0,0 +1,91 @@ +package org.cloudfoundry.multiapps.controller.persistence.util; + +import java.io.StringWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.cloudfoundry.multiapps.common.SLException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ClientKeyConfigurationHandlerTest { + + private final ClientKeyConfigurationHandler handler = new ClientKeyConfigurationHandler(); + private Path tempDir; + + @BeforeEach + void setUp() throws Exception { + tempDir = Files.createTempDirectory("client-key-test"); + } + + @AfterEach + void tearDown() throws Exception { + try (var stream = Files.walk(tempDir)) { + stream.sorted((a, b) -> b.getNameCount() - a.getNameCount()) + .forEach(p -> p.toFile() + .delete()); + } + } + + @Test + void testCreateEncodedKeyFileWritesEncodedBytes() throws Exception { + String pem = generateRsaKeyPairPem(); + Path target = tempDir.resolve("key.bin"); + + Path result = handler.createEncodedKeyFile(pem, target.toString()); + + Assertions.assertEquals(target, result); + Assertions.assertTrue(Files.exists(result)); + Assertions.assertTrue(Files.size(result) > 0); + } + + @Test + void testCreateEncodedKeyFileThrowsForNonKeyPairPem() throws Exception { + String pem = generatePublicKeyPem(); + Path target = tempDir.resolve("key.bin"); + + Assertions.assertThrows(IllegalArgumentException.class, + () -> handler.createEncodedKeyFile(pem, target.toString())); + } + + @Test + void testCreateEncodedKeyFileWrapsIoExceptionInSlExceptionForUnwritablePath() throws Exception { + String pem = generateRsaKeyPairPem(); + Path nonExistentDirectory = tempDir.resolve("does-not-exist") + .resolve("key.bin"); + + SLException thrown = Assertions.assertThrows(SLException.class, + () -> handler.createEncodedKeyFile(pem, nonExistentDirectory.toString())); + Assertions.assertTrue(thrown.getMessage() + .contains(nonExistentDirectory.toString())); + } + + private String generateRsaKeyPairPem() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + StringWriter writer = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { + pemWriter.writeObject(keyPair); + } + return writer.toString(); + } + + private String generatePublicKeyPem() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + StringWriter writer = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { + pemWriter.writeObject(keyPair.getPublic()); + } + return writer.toString(); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ConfigurationEntriesUtilTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ConfigurationEntriesUtilTest.java new file mode 100644 index 0000000000..bc19699b78 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/ConfigurationEntriesUtilTest.java @@ -0,0 +1,31 @@ +package org.cloudfoundry.multiapps.controller.persistence.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ConfigurationEntriesUtilTest { + + @Test + void testDefaultProviderNamespaceIsEmpty() { + Assertions.assertTrue(ConfigurationEntriesUtil.providerNamespaceIsEmpty("default", true)); + Assertions.assertTrue(ConfigurationEntriesUtil.providerNamespaceIsEmpty("default", false)); + } + + @Test + void testNullIsEmptyOnlyWhenConsiderNullAsEmpty() { + Assertions.assertTrue(ConfigurationEntriesUtil.providerNamespaceIsEmpty(null, true)); + Assertions.assertFalse(ConfigurationEntriesUtil.providerNamespaceIsEmpty(null, false)); + } + + @Test + void testNonDefaultNonNullIsNotEmpty() { + Assertions.assertFalse(ConfigurationEntriesUtil.providerNamespaceIsEmpty("custom", true)); + Assertions.assertFalse(ConfigurationEntriesUtil.providerNamespaceIsEmpty("custom", false)); + } + + @Test + void testEmptyStringIsNotConsideredEmpty() { + Assertions.assertFalse(ConfigurationEntriesUtil.providerNamespaceIsEmpty("", true)); + Assertions.assertFalse(ConfigurationEntriesUtil.providerNamespaceIsEmpty("", false)); + } +} diff --git a/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/PEMToEncodedKeyConverterTest.java b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/PEMToEncodedKeyConverterTest.java new file mode 100644 index 0000000000..d5ab37b0e9 --- /dev/null +++ b/multiapps-controller-persistence/src/test/java/org/cloudfoundry/multiapps/controller/persistence/util/PEMToEncodedKeyConverterTest.java @@ -0,0 +1,65 @@ +package org.cloudfoundry.multiapps.controller.persistence.util; + +import java.io.StringReader; +import java.io.StringWriter; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class PEMToEncodedKeyConverterTest { + + private final PEMToEncodedKeyConverter converter = new PEMToEncodedKeyConverter(); + + @Test + void testReturnsPrivateKeyEncodingForPEMKeyPair() throws Exception { + String pem = generateRsaKeyPairPem(); + + try (PEMParser parser = new PEMParser(new StringReader(pem))) { + byte[] result = converter.getPrivateEncodedKey(parser); + + Assertions.assertNotNull(result); + Assertions.assertTrue(result.length > 0); + } + } + + @Test + void testThrowsIllegalArgumentExceptionWithInvalidKeyFormatMessageForPublicKey() throws Exception { + String pem = generatePublicKeyPem(); + + try (PEMParser parser = new PEMParser(new StringReader(pem))) { + IllegalArgumentException thrown = Assertions.assertThrows(IllegalArgumentException.class, + () -> converter.getPrivateEncodedKey(parser)); + Assertions.assertTrue(thrown.getMessage() + .startsWith("Invalid key format:"), + () -> "Unexpected message: " + thrown.getMessage()); + } + } + + private String generatePublicKeyPem() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + StringWriter writer = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { + pemWriter.writeObject(keyPair.getPublic()); + } + return writer.toString(); + } + + private String generateRsaKeyPairPem() throws Exception { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); + KeyPair keyPair = generator.generateKeyPair(); + + StringWriter writer = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { + pemWriter.writeObject(keyPair); + } + return writer.toString(); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java new file mode 100644 index 0000000000..27fd087ce1 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/client/LoggingCloudControllerClientTest.java @@ -0,0 +1,228 @@ +package org.cloudfoundry.multiapps.controller.process.client; + +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.multiapps.controller.client.facade.ApplicationServicesUpdateCallback; +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudApplication; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceBroker; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceInstance; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudServiceKey; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudStack; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudTask; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.cloudfoundry.multiapps.controller.core.security.serialization.DynamicSecureSerialization; +import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; +import org.cloudfoundry.multiapps.controller.process.Messages; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class LoggingCloudControllerClientTest { + + @Mock + private CloudControllerClient delegate; + @Mock + private UserMessageLogger logger; + @Mock + private DynamicSecureSerialization dynamicSecureSerialization; + + private LoggingCloudControllerClient client; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + client = new LoggingCloudControllerClient(delegate, logger, dynamicSecureSerialization); + } + + @Test + void testGetTargetIsAPureDelegateWithoutLogging() { + client.getTarget(); + + Mockito.verify(delegate) + .getTarget(); + Mockito.verifyNoInteractions(logger); + } + + @Test + void testAddDomainLogsAndDelegates() { + client.addDomain("example.com"); + + Mockito.verify(logger) + .debug(Messages.ADDING_DOMAIN_0, "example.com"); + Mockito.verify(delegate) + .addDomain("example.com"); + } + + @Test + void testBindServiceInstanceWithParametersUsesSecureSerialization() { + Map parameters = Map.of("key", "secret-value"); + ApplicationServicesUpdateCallback callback = Mockito.mock(ApplicationServicesUpdateCallback.class); + Mockito.when(dynamicSecureSerialization.toJson(parameters)) + .thenReturn("[redacted]"); + + client.bindServiceInstance("binding", "app", "service", parameters, callback); + + Mockito.verify(dynamicSecureSerialization) + .toJson(parameters); + Mockito.verify(logger) + .debug(Messages.BINDING_SERVICE_INSTANCE_0_TO_APPLICATION_1_WITH_PARAMETERS_2, "service", "app", "[redacted]"); + Mockito.verify(delegate) + .bindServiceInstance("binding", "app", "service", parameters, callback); + } + + @Test + void testCreateServiceInstanceSerializesPayloadBeforeLogging() { + CloudServiceInstance instance = Mockito.mock(CloudServiceInstance.class); + Mockito.when(dynamicSecureSerialization.toJson(instance)) + .thenReturn("[redacted-instance]"); + + client.createServiceInstance(instance); + + Mockito.verify(logger) + .debug(Messages.CREATING_SERVICE_INSTANCE_0, "[redacted-instance]"); + Mockito.verify(delegate) + .createServiceInstance(instance); + } + + @Test + void testCreateServiceBrokerSerializesPayloadBeforeLogging() { + CloudServiceBroker broker = Mockito.mock(CloudServiceBroker.class); + Mockito.when(dynamicSecureSerialization.toJson(broker)) + .thenReturn("[redacted-broker]"); + + client.createServiceBroker(broker); + + Mockito.verify(logger) + .debug(Messages.CREATING_SERVICE_BROKER_0, "[redacted-broker]"); + Mockito.verify(delegate) + .createServiceBroker(broker); + } + + @Test + void testCreateServiceKeyByModelSerializesCredentialsNotKeyName() { + CloudServiceKey keyModel = Mockito.mock(CloudServiceKey.class); + Map credentials = Map.of("password", "shhh"); + Mockito.when(keyModel.getName()) + .thenReturn("my-key"); + Mockito.when(keyModel.getCredentials()) + .thenReturn(credentials); + Mockito.when(dynamicSecureSerialization.toJson(credentials)) + .thenReturn("[redacted-credentials]"); + + client.createServiceKey(keyModel, "my-service"); + + Mockito.verify(dynamicSecureSerialization) + .toJson(credentials); + Mockito.verify(logger) + .debug(Messages.CREATING_SERVICE_KEY_0_FOR_SERVICE_INSTANCE_1_WITH_PARAMETERS_2, "my-key", "my-service", "[redacted-credentials]"); + } + + @Test + void testRunTaskSerializesTask() { + CloudTask task = Mockito.mock(CloudTask.class); + Mockito.when(dynamicSecureSerialization.toJson(task)) + .thenReturn("[redacted-task]"); + + client.runTask("my-app", task); + + Mockito.verify(logger) + .debug(Messages.RUNNING_TASK_1_ON_APPLICATION_0, "my-app", "[redacted-task]"); + Mockito.verify(delegate) + .runTask("my-app", task); + } + + @Test + void testUpdateApplicationStagingSerializesStaging() { + Staging staging = Mockito.mock(Staging.class); + Mockito.when(dynamicSecureSerialization.toJson(staging)) + .thenReturn("[redacted-staging]"); + + client.updateApplicationStaging("my-app", staging); + + Mockito.verify(logger) + .debug(Messages.UPDATING_STAGING_OF_APPLICATION_0_TO_1, "my-app", "[redacted-staging]"); + } + + @Test + void testUpdateApplicationMetadataSerializesMetadata() { + UUID guid = UUID.randomUUID(); + Metadata metadata = Mockito.mock(Metadata.class); + Mockito.when(dynamicSecureSerialization.toJson(metadata)) + .thenReturn("[redacted-metadata]"); + + client.updateApplicationMetadata(guid, metadata); + + Mockito.verify(logger) + .debug(Messages.UPDATING_METADATA_OF_APPLICATION_0_TO_1, guid, "[redacted-metadata]"); + } + + @Test + void testUpdateServiceBrokerSerializesPayload() { + CloudServiceBroker broker = Mockito.mock(CloudServiceBroker.class); + Mockito.when(dynamicSecureSerialization.toJson(broker)) + .thenReturn("[redacted-broker]"); + Mockito.when(delegate.updateServiceBroker(broker)) + .thenReturn("job-1"); + + Assertions.assertEquals("job-1", client.updateServiceBroker(broker)); + + Mockito.verify(logger) + .debug(Messages.UPDATING_SERVICE_BROKER_TO_0, "[redacted-broker]"); + } + + @Test + void testGetApplicationByNameAndRequiredFlagDelegatesAndLogsSameMessageAsNoRequiredOverload() { + CloudApplication app = Mockito.mock(CloudApplication.class); + Mockito.when(delegate.getApplication("my-app", true)) + .thenReturn(app); + + Assertions.assertSame(app, client.getApplication("my-app", true)); + + Mockito.verify(logger) + .debug(Messages.GETTING_APPLICATION_0, "my-app"); + Mockito.verify(delegate) + .getApplication("my-app", true); + } + + @Test + void testDeleteServiceBindingByGuidLogsBindingGuid() { + UUID bindingGuid = UUID.fromString("11111111-1111-1111-1111-111111111111"); + Mockito.when(delegate.deleteServiceBinding(bindingGuid)) + .thenReturn(Optional.of("job-2")); + + Assertions.assertEquals(Optional.of("job-2"), client.deleteServiceBinding(bindingGuid)); + + Mockito.verify(logger) + .debug(Messages.DELETING_SERVICE_BINDING_0, bindingGuid.toString()); + } + + @Test + void testGetStackPassesRequiredFlagToDelegate() { + CloudStack stack = Mockito.mock(CloudStack.class); + Mockito.when(delegate.getStack("cflinuxfs4", true)) + .thenReturn(stack); + + Assertions.assertSame(stack, client.getStack("cflinuxfs4", true)); + + Mockito.verify(logger) + .debug(Messages.GETTING_STACK_0, "cflinuxfs4"); + } + + @Test + void testRenameLogsBothNamesAndDelegates() { + client.rename("old-name", "new-name"); + + Mockito.verify(logger) + .debug(Messages.RENAMING_APPLICATION_0_TO_1, "old-name", "new-name"); + Mockito.verify(delegate) + .rename("old-name", "new-name"); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacadeAdditionalTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacadeAdditionalTest.java new file mode 100644 index 0000000000..603c97b3e5 --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/flowable/FlowableFacadeAdditionalTest.java @@ -0,0 +1,388 @@ +package org.cloudfoundry.multiapps.controller.process.flowable; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.persistence.Constants; +import org.flowable.common.engine.api.FlowableOptimisticLockingException; +import org.flowable.engine.HistoryService; +import org.flowable.engine.ManagementService; +import org.flowable.engine.ProcessEngine; +import org.flowable.engine.ProcessEngineConfiguration; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricActivityInstanceQuery; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.history.HistoricProcessInstanceQuery; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ExecutionQuery; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceQuery; +import org.flowable.job.service.impl.asyncexecutor.DefaultAsyncJobExecutor; +import org.flowable.variable.api.history.HistoricVariableInstance; +import org.flowable.variable.api.history.HistoricVariableInstanceQuery; +import org.flowable.variable.api.persistence.entity.VariableInstance; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class FlowableFacadeAdditionalTest { + + private static final String PROCESS_INSTANCE_ID = "process-1"; + private static final String EXECUTION_ID = "execution-1"; + + @Mock + private ProcessEngine processEngine; + @Mock + private ProcessEngineConfiguration processEngineConfiguration; + @Mock + private DefaultAsyncJobExecutor asyncExecutor; + @Mock + private RuntimeService runtimeService; + @Mock + private ManagementService managementService; + @Mock + private HistoryService historyService; + @Mock + private ExecutionQuery executionQuery; + @Mock + private ProcessInstanceQuery processInstanceQuery; + @Mock + private HistoricProcessInstanceQuery historicProcessInstanceQuery; + @Mock + private HistoricVariableInstanceQuery historicVariableInstanceQuery; + @Mock + private HistoricActivityInstanceQuery historicActivityInstanceQuery; + + private FlowableFacade facade; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + + Mockito.when(processEngine.getRuntimeService()) + .thenReturn(runtimeService); + Mockito.when(processEngine.getManagementService()) + .thenReturn(managementService); + Mockito.when(processEngine.getHistoryService()) + .thenReturn(historyService); + Mockito.when(processEngine.getProcessEngineConfiguration()) + .thenReturn(processEngineConfiguration); + Mockito.when(processEngineConfiguration.getAsyncExecutor()) + .thenReturn(asyncExecutor); + + facade = new FlowableFacade(processEngine); + } + + @Test + void testStartProcessDelegatesToRuntimeService() { + ProcessInstance instance = Mockito.mock(ProcessInstance.class); + Map variables = Map.of("k", "v"); + Mockito.when(runtimeService.startProcessInstanceByKey("deploy", variables)) + .thenReturn(instance); + + Assertions.assertSame(instance, facade.startProcess("deploy", variables)); + } + + @Test + void testGetProcessInstanceIdReadsCorrelationIdVariable() { + VariableInstance variableInstance = Mockito.mock(VariableInstance.class); + Mockito.when(variableInstance.getTextValue()) + .thenReturn("correlation-1"); + Mockito.when(runtimeService.getVariableInstance(EXECUTION_ID, Constants.CORRELATION_ID)) + .thenReturn(variableInstance); + + Assertions.assertEquals("correlation-1", facade.getProcessInstanceId(EXECUTION_ID)); + } + + @Test + void testGetCurrentTaskIdReturnsNullWhenAbsentInRuntimeAndHistory() { + Mockito.when(runtimeService.getVariableInstance(ArgumentMatchers.eq(EXECUTION_ID), ArgumentMatchers.eq(Constants.TASK_ID))) + .thenReturn(null); + mockHistoricVariableInstanceQuery(null); + + Assertions.assertNull(facade.getCurrentTaskId(EXECUTION_ID)); + } + + @Test + void testGetSubprocessInstanceIdFallsBackToHistoricVariable() { + Mockito.when(runtimeService.getVariableInstance(ArgumentMatchers.eq(EXECUTION_ID), ArgumentMatchers.anyString())) + .thenReturn(null); + HistoricVariableInstance historicInstance = Mockito.mock(HistoricVariableInstance.class); + Mockito.when(historicInstance.getValue()) + .thenReturn("subprocess-1"); + mockHistoricVariableInstanceQuery(historicInstance); + + Assertions.assertEquals("subprocess-1", facade.getSubprocessInstanceId(EXECUTION_ID)); + } + + @Test + void testGetProcessInstanceQueriesByProcessInstanceId() { + ProcessInstance instance = Mockito.mock(ProcessInstance.class); + Mockito.when(runtimeService.createProcessInstanceQuery()) + .thenReturn(processInstanceQuery); + Mockito.when(processInstanceQuery.processInstanceId(PROCESS_INSTANCE_ID)) + .thenReturn(processInstanceQuery); + Mockito.when(processInstanceQuery.singleResult()) + .thenReturn(instance); + + Assertions.assertSame(instance, facade.getProcessInstance(PROCESS_INSTANCE_ID)); + } + + @Test + void testHasDeadLetterJobsReturnsFalseWhenNoExecutions() { + mockExecutionQueryReturning(List.of()); + + Assertions.assertFalse(facade.hasDeadLetterJobs(PROCESS_INSTANCE_ID)); + } + + @Test + void testGetHistoricSubProcessIdsExcludesCorrelationId() { + HistoricVariableInstance v1 = Mockito.mock(HistoricVariableInstance.class); + Mockito.when(v1.getProcessInstanceId()) + .thenReturn("correlation-1"); + HistoricVariableInstance v2 = Mockito.mock(HistoricVariableInstance.class); + Mockito.when(v2.getProcessInstanceId()) + .thenReturn("subprocess-1"); + Mockito.when(historyService.createHistoricVariableInstanceQuery()) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.variableValueEquals(Constants.CORRELATION_ID, "correlation-1")) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.orderByProcessInstanceId()) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.asc()) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.list()) + .thenReturn(List.of(v1, v2)); + + List result = facade.getHistoricSubProcessIds("correlation-1"); + + Assertions.assertEquals(List.of("subprocess-1"), result); + } + + @Test + void testGetHistoricProcessByIdQueriesHistoryService() { + HistoricProcessInstance instance = Mockito.mock(HistoricProcessInstance.class); + Mockito.when(historyService.createHistoricProcessInstanceQuery()) + .thenReturn(historicProcessInstanceQuery); + Mockito.when(historicProcessInstanceQuery.processInstanceId(PROCESS_INSTANCE_ID)) + .thenReturn(historicProcessInstanceQuery); + Mockito.when(historicProcessInstanceQuery.singleResult()) + .thenReturn(instance); + + Assertions.assertSame(instance, facade.getHistoricProcessById(PROCESS_INSTANCE_ID)); + } + + @Test + void testGetHistoricVariableInstanceQueriesByProcessAndVariableName() { + HistoricVariableInstance variable = Mockito.mock(HistoricVariableInstance.class); + Mockito.when(historyService.createHistoricVariableInstanceQuery()) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.processInstanceId(PROCESS_INSTANCE_ID)) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.variableName("foo")) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.singleResult()) + .thenReturn(variable); + + Assertions.assertSame(variable, facade.getHistoricVariableInstance(PROCESS_INSTANCE_ID, "foo")); + } + + @Test + void testTriggerForwardsToRuntimeService() { + Map vars = Map.of("k", "v"); + + facade.trigger(EXECUTION_ID, vars); + + Mockito.verify(runtimeService) + .trigger(EXECUTION_ID, vars); + } + + @Test + void testDeleteProcessInstanceCallsRuntimeServiceOnce() { + facade.deleteProcessInstance(PROCESS_INSTANCE_ID, "user requested abort"); + + Mockito.verify(runtimeService) + .deleteProcessInstance(PROCESS_INSTANCE_ID, "user requested abort"); + } + + @Test + void testDeleteProcessInstanceRetriesAfterOptimisticLockingException() { + Mockito.doThrow(new FlowableOptimisticLockingException("conflict")) + .doNothing() + .when(runtimeService) + .deleteProcessInstance(PROCESS_INSTANCE_ID, "abort"); + + facade.deleteProcessInstance(PROCESS_INSTANCE_ID, "abort"); + + Mockito.verify(runtimeService, Mockito.times(2)) + .deleteProcessInstance(PROCESS_INSTANCE_ID, "abort"); + } + + @Test + void testDeleteProcessInstanceThrowsAfterDeadlinePastWithOptimisticLocking() { + Mockito.doThrow(new FlowableOptimisticLockingException("conflict")) + .when(runtimeService) + .deleteProcessInstance(PROCESS_INSTANCE_ID, "abort"); + FlowableFacade facadeWithImmediateDeadline = new FlowableFacade(processEngine) { + @Override + protected boolean isPastDeadline(long deadline) { + return true; + } + }; + + Assertions.assertThrows(IllegalStateException.class, + () -> facadeWithImmediateDeadline.deleteProcessInstance(PROCESS_INSTANCE_ID, "abort")); + } + + @Test + void testIsProcessInstanceAtReceiveTaskReturnsFalseWhenNoExecutions() { + mockExecutionQueryReturning(List.of()); + + Assertions.assertFalse(facade.isProcessInstanceAtReceiveTask(PROCESS_INSTANCE_ID)); + } + + @Test + void testFindExecutionsAtReceiveTaskFiltersByActiveActivity() { + Execution active = Mockito.mock(Execution.class); + Mockito.when(active.getActivityId()) + .thenReturn("act-1"); + Mockito.when(active.getId()) + .thenReturn("exec-active"); + Execution inactive = Mockito.mock(Execution.class); + Mockito.when(inactive.getActivityId()) + .thenReturn(null); + mockExecutionQueryReturning(List.of(active, inactive)); + + Mockito.when(historyService.createHistoricActivityInstanceQuery()) + .thenReturn(historicActivityInstanceQuery); + Mockito.when(historicActivityInstanceQuery.activityId("act-1")) + .thenReturn(historicActivityInstanceQuery); + Mockito.when(historicActivityInstanceQuery.executionId("exec-active")) + .thenReturn(historicActivityInstanceQuery); + Mockito.when(historicActivityInstanceQuery.activityType("receiveTask")) + .thenReturn(historicActivityInstanceQuery); + HistoricActivityInstance receivedActivity = Mockito.mock(HistoricActivityInstance.class); + Mockito.when(historicActivityInstanceQuery.list()) + .thenReturn(List.of(receivedActivity)); + + List result = facade.findExecutionsAtReceiveTask(PROCESS_INSTANCE_ID); + + Assertions.assertEquals(List.of(active), result); + } + + @Test + void testGetActiveProcessExecutionsFiltersOutNullActivityId() { + Execution active = Mockito.mock(Execution.class); + Mockito.when(active.getActivityId()) + .thenReturn("act-1"); + Execution inactive = Mockito.mock(Execution.class); + Mockito.when(inactive.getActivityId()) + .thenReturn(null); + mockExecutionQueryReturning(List.of(active, inactive)); + + List result = facade.getActiveProcessExecutions(PROCESS_INSTANCE_ID); + + Assertions.assertEquals(List.of(active), result); + } + + @Test + void testSuspendProcessInstanceForwardsToRuntimeService() { + facade.suspendProcessInstance(PROCESS_INSTANCE_ID); + + Mockito.verify(runtimeService) + .suspendProcessInstanceById(PROCESS_INSTANCE_ID); + } + + @Test + void testIsJobExecutorActiveReadsAsyncExecutor() { + Mockito.when(asyncExecutor.isActive()) + .thenReturn(true); + + Assertions.assertTrue(facade.isJobExecutorActive()); + } + + @Test + void testGetProcessEngineReturnsConstructorArgument() { + Assertions.assertSame(processEngine, facade.getProcessEngine()); + } + + @Test + void testFindAllRunningProcessInstanceStartedBeforeDelegatesToProcessInstanceQuery() { + LocalDateTime before = LocalDateTime.parse("2026-05-01T10:00:00"); + Mockito.when(runtimeService.createProcessInstanceQuery()) + .thenReturn(processInstanceQuery); + Mockito.when(processInstanceQuery.excludeSubprocesses(true)) + .thenReturn(processInstanceQuery); + Mockito.when(processInstanceQuery.startedBefore(ArgumentMatchers.any(Date.class))) + .thenReturn(processInstanceQuery); + ProcessInstance instance = Mockito.mock(ProcessInstance.class); + Mockito.when(processInstanceQuery.list()) + .thenReturn(List.of(instance)); + + Assertions.assertEquals(List.of(instance), facade.findAllRunningProcessInstanceStartedBefore(before)); + } + + @Test + void testGetParentExecutionReturnsSingleResult() { + Execution parent = Mockito.mock(Execution.class); + Mockito.when(runtimeService.createExecutionQuery()) + .thenReturn(executionQuery); + Mockito.when(executionQuery.executionId("parent-id")) + .thenReturn(executionQuery); + Mockito.when(executionQuery.singleResult()) + .thenReturn(parent); + + Assertions.assertSame(parent, facade.getParentExecution("parent-id")); + } + + @Test + void testSetVariableInParentProcessUsesParentExecutionsSuperExecutionId() { + DelegateExecution childExecution = Mockito.mock(DelegateExecution.class); + Mockito.when(childExecution.getParentId()) + .thenReturn("parent-id"); + + Execution parent = Mockito.mock(Execution.class); + Mockito.when(parent.getSuperExecutionId()) + .thenReturn("super-id"); + Mockito.when(runtimeService.createExecutionQuery()) + .thenReturn(executionQuery); + Mockito.when(executionQuery.executionId("parent-id")) + .thenReturn(executionQuery); + Mockito.when(executionQuery.singleResult()) + .thenReturn(parent); + + facade.setVariableInParentProcess(childExecution, "varName", "varValue"); + + Mockito.verify(runtimeService) + .setVariable("super-id", "varName", "varValue"); + } + + private void mockExecutionQueryReturning(List executions) { + Mockito.when(runtimeService.createExecutionQuery()) + .thenReturn(executionQuery); + Mockito.when(executionQuery.rootProcessInstanceId(ArgumentMatchers.anyString())) + .thenReturn(executionQuery); + Mockito.when(executionQuery.list()) + .thenReturn(executions); + } + + private void mockHistoricVariableInstanceQuery(HistoricVariableInstance result) { + Mockito.when(historyService.createHistoricVariableInstanceQuery()) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.executionId(ArgumentMatchers.anyString())) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.variableName(ArgumentMatchers.anyString())) + .thenReturn(historicVariableInstanceQuery); + Mockito.when(historicVariableInstanceQuery.singleResult()) + .thenReturn(result); + } +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidatorTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidatorTest.java new file mode 100644 index 0000000000..2ba4312ded --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/HookPhasesConfigValidatorTest.java @@ -0,0 +1,121 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.core.model.HookPhase; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Hook; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class HookPhasesConfigValidatorTest { + + private final HookPhasesConfigValidator validator = new HookPhasesConfigValidator(); + + @Test + void testValidatePassesWhenDescriptorHasNoModules() { + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + descriptor.setModules(List.of()); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + @Test + void testValidatePassesWhenModuleHasNoHooks() { + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of())); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + @Test + void testValidatePassesWhenHookHasNoPhasesConfigParameter() { + Hook hook = hook("my-hook", List.of("deploy.application.before-start"), Map.of()); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + @Test + void testValidatePassesWhenPhasesConfigHasUniquePhases() { + List> phasesConfig = List.of(Map.of("phase", "deploy.application.before-start", "target-app", "live"), + Map.of("phase", "deploy.application.after-start", "target-app", "idle")); + Hook hook = hook("my-hook", List.of("deploy.application.before-start"), + Map.of(SupportedParameters.PHASES_CONFIG, phasesConfig)); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + @Test + void testValidateThrowsWhenPhasesConfigIsNotAList() { + Hook hook = hook("bad-hook", List.of("deploy.application.before-start"), + Map.of(SupportedParameters.PHASES_CONFIG, "not-a-list")); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + SLException exception = Assertions.assertThrows(SLException.class, () -> validator.validate(descriptor)); + Assertions.assertTrue(exception.getMessage() + .contains("bad-hook")); + Assertions.assertTrue(exception.getMessage() + .contains("phases-config")); + } + + @Test + void testValidateThrowsWhenPhasesConfigContainsDuplicatePhase() { + List> phasesConfig = List.of(Map.of("phase", "deploy.application.before-start", "target-app", "live"), + Map.of("phase", "deploy.application.before-start", "target-app", "idle")); + Hook hook = hook("dup-hook", List.of("deploy.application.before-start"), + Map.of(SupportedParameters.PHASES_CONFIG, phasesConfig)); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + SLException exception = Assertions.assertThrows(SLException.class, () -> validator.validate(descriptor)); + Assertions.assertTrue(exception.getMessage() + .contains("deploy.application.before-start")); + Assertions.assertTrue(exception.getMessage() + .contains("dup-hook")); + } + + @Test + void testValidateIgnoresEntriesWithoutPhaseKey() { + List> phasesConfig = List.of(Map.of("target-app", "live"), Map.of("target-app", "idle")); + Hook hook = hook("no-phase-key", List.of("deploy.application.before-start"), + Map.of(SupportedParameters.PHASES_CONFIG, phasesConfig)); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + @Test + void testValidatePassesAndDoesNotThrowWhenDeprecatedPhaseUsed() { + Hook hook = hook("deprecated-hook", List.of(HookPhase.BLUE_GREEN_APPLICATION_BEFORE_UNMAP_ROUTES_IDLE.getValue()), + Map.of()); + DeploymentDescriptor descriptor = descriptorWithModule(moduleWithHooks(List.of(hook))); + + Assertions.assertDoesNotThrow(() -> validator.validate(descriptor)); + } + + private DeploymentDescriptor descriptorWithModule(Module module) { + DeploymentDescriptor descriptor = DeploymentDescriptor.createV3(); + descriptor.setModules(List.of(module)); + return descriptor; + } + + private Module moduleWithHooks(List hooks) { + Module module = Module.createV3() + .setName("module-1") + .setType("application"); + module.setHooks(hooks); + return module; + } + + private Hook hook(String name, List phases, Map parameters) { + return Hook.createV3() + .setName(name) + .setPhases(phases) + .setParameters(parameters); + } + +} \ No newline at end of file diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LockOwnerReleaserTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LockOwnerReleaserTest.java new file mode 100644 index 0000000000..98e89b481d --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/LockOwnerReleaserTest.java @@ -0,0 +1,93 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.List; + +import org.cloudfoundry.multiapps.controller.process.flowable.commands.ClearJobLockOwnersCmd; +import org.flowable.common.engine.impl.interceptor.Command; +import org.flowable.common.engine.impl.interceptor.CommandExecutor; +import org.flowable.engine.ManagementService; +import org.flowable.engine.ProcessEngine; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.cmd.ClearProcessInstanceLockTimesCmd; +import org.flowable.job.api.Job; +import org.flowable.job.api.JobQuery; +import org.flowable.job.service.JobServiceConfiguration; +import org.flowable.job.service.impl.asyncexecutor.AsyncExecutor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +class LockOwnerReleaserTest { + + private static final String LOCK_OWNER = "owner-1"; + + @Mock + private ProcessEngine processEngine; + @Mock + private ProcessEngineConfigurationImpl processEngineConfiguration; + @Mock + private CommandExecutor commandExecutor; + @Mock + private AsyncExecutor asyncExecutor; + @Mock + private JobServiceConfiguration jobServiceConfiguration; + @Mock + private ManagementService managementService; + @Mock + private JobQuery jobQuery; + @Mock + private Job job; + + private LockOwnerReleaser lockOwnerReleaser; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + Mockito.when(processEngine.getProcessEngineConfiguration()) + .thenReturn(processEngineConfiguration); + Mockito.when(processEngineConfiguration.getCommandExecutor()) + .thenReturn(commandExecutor); + Mockito.when(processEngineConfiguration.getAsyncExecutor()) + .thenReturn(asyncExecutor); + Mockito.when(asyncExecutor.getJobServiceConfiguration()) + .thenReturn(jobServiceConfiguration); + Mockito.when(processEngine.getManagementService()) + .thenReturn(managementService); + Mockito.when(managementService.createJobQuery()) + .thenReturn(jobQuery); + Mockito.when(jobQuery.lockOwner(LOCK_OWNER)) + .thenReturn(jobQuery); + + lockOwnerReleaser = new LockOwnerReleaser(processEngine); + } + + @Test + void testReleaseClearsProcessInstanceLockTimesAndJobLockOwnersWhenStaleJobsExist() { + Mockito.when(jobQuery.list()) + .thenReturn(List.of(job)); + + lockOwnerReleaser.release(LOCK_OWNER); + + Mockito.verify(commandExecutor) + .execute(Mockito.any(ClearProcessInstanceLockTimesCmd.class)); + Mockito.verify(managementService) + .executeCommand(Mockito.any(ClearJobLockOwnersCmd.class)); + } + + @Test + void testReleaseSkipsClearJobLockOwnersWhenNoStaleJobs() { + Mockito.when(jobQuery.list()) + .thenReturn(List.of()); + + lockOwnerReleaser.release(LOCK_OWNER); + + Mockito.verify(commandExecutor) + .execute(Mockito.any(ClearProcessInstanceLockTimesCmd.class)); + Mockito.verify(managementService, Mockito.never()) + .executeCommand(Mockito.> any()); + } + +} diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDependencyCheckerTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDependencyCheckerTest.java new file mode 100644 index 0000000000..6ab51faccb --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/ModuleDependencyCheckerTest.java @@ -0,0 +1,173 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.List; +import java.util.UUID; + +import org.cloudfoundry.multiapps.controller.client.facade.CloudControllerClient; +import org.cloudfoundry.multiapps.controller.client.facade.CloudOperationException; +import org.cloudfoundry.multiapps.controller.core.helpers.ModuleToDeployHelper; +import org.cloudfoundry.multiapps.controller.core.util.UserMessageLogger; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; + +class ModuleDependencyCheckerTest { + + @Mock + private CloudControllerClient client; + @Mock + private UserMessageLogger userMessageLogger; + @Mock + private ModuleToDeployHelper moduleToDeployHelper; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testV2ModuleIsAlwaysSatisfied() { + Module v2Module = Module.createV2() + .setName("legacy-module"); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, List.of(v2Module), + List.of(v2Module), List.of()); + + Assertions.assertTrue(checker.areAllDependenciesSatisfied(v2Module)); + Mockito.verifyNoInteractions(client); + } + + @Test + void testModuleWithoutDeployedAfterIsSatisfied() { + Module module = v3Module("a").setDeployedAfter(List.of()); + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, List.of(module), + List.of(module), List.of()); + + Assertions.assertTrue(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testDependencyAlreadyDeployedSatisfiesModule() { + Module dependency = v3Module("dep"); + Module module = v3Module("app").setDeployedAfter(List.of("dep")); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, dependency), List.of(module, dependency), + List.of(dependency)); + + Assertions.assertTrue(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testDependencyNotForDeploymentIsTreatedAsProcessed() { + Module dependency = v3Module("not-deployed"); + Module module = v3Module("app").setDeployedAfter(List.of("not-deployed")); + + // dependency is in the descriptor but NOT in modulesForDeployment + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, dependency), List.of(module), List.of()); + + Assertions.assertTrue(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testDependencyForDeploymentButNotYetCompletedIsNotSatisfied() { + Module dependency = v3Module("dep"); + Module module = v3Module("app").setDeployedAfter(List.of("dep")); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, dependency), List.of(module, dependency), + List.of()); + + Assertions.assertFalse(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testNonDeploymentDependencyExistingInCfSatisfiesModule() { + // To reach the CF lookup branch, we need at least one for-deployment-not-yet-deployed dep + // that forces fallthrough into areAllDependenciesAlreadyPresent. + Module externalDep = v3Module("external"); + Module pendingDep = v3Module("pending"); + Module module = v3Module("app").setDeployedAfter(List.of("external", "pending")); + Mockito.when(moduleToDeployHelper.isApplication(externalDep)) + .thenReturn(true); + Mockito.when(client.getApplicationGuid("external")) + .thenReturn(UUID.randomUUID()); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, externalDep, pendingDep), + List.of(module, pendingDep), List.of(pendingDep)); + + // pending is in alreadyDeployed and modulesNotYetDeployed is empty; external resolves via CF. + Assertions.assertTrue(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testNonDeploymentDependencyMissingFromCfIsNotSatisfied() { + Module externalDep = v3Module("missing"); + Module pendingDep = v3Module("pending"); + Module module = v3Module("app").setDeployedAfter(List.of("missing", "pending")); + Mockito.when(moduleToDeployHelper.isApplication(externalDep)) + .thenReturn(true); + Mockito.when(client.getApplicationGuid("missing")) + .thenThrow(new CloudOperationException(HttpStatus.NOT_FOUND)); + + // pending is for-deployment but not yet deployed -> forces fallthrough to areAllDependenciesAlreadyPresent. + // external is not-for-deployment and CF says it doesn't exist -> not satisfied. + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, externalDep, pendingDep), + List.of(module, pendingDep), List.of()); + + Assertions.assertFalse(checker.areAllDependenciesSatisfied(module)); + } + + @Test + void testNonApplicationDependencyEmitsWarningAndIsSatisfied() { + Module nonApp = v3Module("non-app"); + Module pendingDep = v3Module("pending"); + Module module = v3Module("app").setDeployedAfter(List.of("non-app", "pending")); + Mockito.when(moduleToDeployHelper.isApplication(nonApp)) + .thenReturn(false); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(module, nonApp, pendingDep), + List.of(module, pendingDep), List.of()); + + // pending is for-deployment but not deployed -> forces fallthrough; non-app is not-for-deployment but not an + // application, so isDependencyPresent returns true after warning. modulesNotYetDeployed = [pending] -> false. + Assertions.assertFalse(checker.areAllDependenciesSatisfied(module)); + Mockito.verify(userMessageLogger) + .warn(Mockito.contains("non-app")); + Mockito.verifyNoInteractions(client); + } + + @Test + void testGettersExposeComputedSets() { + Module deployed = v3Module("d"); + Module forDeployment = v3Module("f"); + Module notForDeployment = v3Module("n"); + + ModuleDependencyChecker checker = new ModuleDependencyChecker(client, userMessageLogger, moduleToDeployHelper, + List.of(deployed, forDeployment, notForDeployment), + List.of(deployed, forDeployment), List.of(deployed)); + + Assertions.assertEquals(java.util.Set.of("d", "f"), checker.getModulesForDeployment()); + Assertions.assertEquals(java.util.Set.of("n"), checker.getModulesNotForDeployment()); + Assertions.assertEquals(java.util.Set.of("d"), checker.getAlreadyDeployedModules()); + } + + private Module v3Module(String name) { + Module module = Module.createV3() + .setName(name) + .setType("application"); + module.setDeployedAfter(List.of()); + return module; + } + +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationHealthResourceTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationHealthResourceTest.java new file mode 100644 index 0000000000..a98e5a117b --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/ApplicationHealthResourceTest.java @@ -0,0 +1,41 @@ +package org.cloudfoundry.multiapps.controller.web.resources; + +import org.cloudfoundry.multiapps.controller.core.application.health.ApplicationHealthCalculator; +import org.cloudfoundry.multiapps.controller.core.application.health.model.ApplicationHealthResult; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; + +class ApplicationHealthResourceTest { + + @Mock + private ApplicationHealthCalculator applicationHealthCalculator; + @Mock + private ApplicationHealthResult applicationHealthResult; + + private ApplicationHealthResource resource; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + resource = new ApplicationHealthResource(applicationHealthCalculator); + } + + @Test + void testCalculateApplicationHealthDelegatesToCalculator() { + ResponseEntity expected = ResponseEntity.ok(applicationHealthResult); + Mockito.when(applicationHealthCalculator.calculateApplicationHealth()) + .thenReturn(expected); + + ResponseEntity result = resource.calculateApplicationHealth(); + + Assertions.assertSame(expected, result); + Mockito.verify(applicationHealthCalculator) + .calculateApplicationHealth(); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/CsrfTokenResourceTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/CsrfTokenResourceTest.java new file mode 100644 index 0000000000..feef6b7538 --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/CsrfTokenResourceTest.java @@ -0,0 +1,25 @@ +package org.cloudfoundry.multiapps.controller.web.resources; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +class CsrfTokenResourceTest { + + private CsrfTokenResource resource; + + @BeforeEach + void setUp() { + resource = new CsrfTokenResource(); + } + + @Test + void testGetCsrfTokenReturnsNoContent() { + ResponseEntity response = resource.getCsrfToken(); + + Assertions.assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode()); + Assertions.assertNull(response.getBody()); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/PingResourceTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/PingResourceTest.java new file mode 100644 index 0000000000..e89f641e37 --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/resources/PingResourceTest.java @@ -0,0 +1,25 @@ +package org.cloudfoundry.multiapps.controller.web.resources; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +class PingResourceTest { + + private PingResource resource; + + @BeforeEach + void setUp() { + resource = new PingResource(); + } + + @Test + void testPingReturnsPongWithOkStatus() { + ResponseEntity response = resource.ping(); + + Assertions.assertEquals(HttpStatus.OK, response.getStatusCode()); + Assertions.assertEquals("pong", response.getBody()); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/security/SpaceWithUserTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/security/SpaceWithUserTest.java new file mode 100644 index 0000000000..bd4c4fad44 --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/security/SpaceWithUserTest.java @@ -0,0 +1,35 @@ +package org.cloudfoundry.multiapps.controller.web.security; + +import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SpaceWithUserTest { + + private static final UUID USER_GUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID SPACE_GUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); + + @Test + void testEqualsReturnsFalseForDifferentUserGuid() { + SpaceWithUser a = new SpaceWithUser(USER_GUID, SPACE_GUID); + SpaceWithUser b = new SpaceWithUser(UUID.randomUUID(), SPACE_GUID); + + Assertions.assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseForDifferentSpaceGuid() { + SpaceWithUser a = new SpaceWithUser(USER_GUID, SPACE_GUID); + SpaceWithUser b = new SpaceWithUser(USER_GUID, UUID.randomUUID()); + + Assertions.assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseForOtherType() { + SpaceWithUser sut = new SpaceWithUser(USER_GUID, SPACE_GUID); + + Assertions.assertNotEquals(sut, "not-a-space"); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/client/FileFromUrlDataTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/client/FileFromUrlDataTest.java new file mode 100644 index 0000000000..85098f73e7 --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/client/FileFromUrlDataTest.java @@ -0,0 +1,35 @@ +package org.cloudfoundry.multiapps.controller.web.upload.client; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.net.URI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class FileFromUrlDataTest { + + @Test + void testRecordExposesAllAccessors() { + InputStream stream = new ByteArrayInputStream(new byte[] { 1, 2, 3 }); + URI uri = URI.create("https://example.com/foo.mtar"); + + FileFromUrlData data = new FileFromUrlData(stream, uri, 1234L); + + Assertions.assertSame(stream, data.fileInputStream()); + Assertions.assertEquals(uri, data.uri()); + Assertions.assertEquals(1234L, data.fileSize()); + } + + @Test + void testRecordEqualsAndHashCodeFromComponents() { + InputStream stream = new ByteArrayInputStream(new byte[] { 1 }); + URI uri = URI.create("https://example.com/x"); + + FileFromUrlData a = new FileFromUrlData(stream, uri, 10L); + FileFromUrlData b = new FileFromUrlData(stream, uri, 10L); + + Assertions.assertEquals(a, b); + Assertions.assertEquals(a.hashCode(), b.hashCode()); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/exception/RejectedAsyncUploadJobExceptionTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/exception/RejectedAsyncUploadJobExceptionTest.java new file mode 100644 index 0000000000..7cee24ca44 --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/upload/exception/RejectedAsyncUploadJobExceptionTest.java @@ -0,0 +1,27 @@ +package org.cloudfoundry.multiapps.controller.web.upload.exception; + +import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class RejectedAsyncUploadJobExceptionTest { + + @Mock + private AsyncUploadJobEntry jobEntry; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testGetAsyncUploadJobEntryReturnsConstructorValue() { + RejectedAsyncUploadJobException e = new RejectedAsyncUploadJobException(jobEntry); + + Assertions.assertSame(jobEntry, e.getAsyncUploadJobEntry()); + } +} diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/util/ServletUtilTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/util/ServletUtilTest.java new file mode 100644 index 0000000000..4aa29bf10b --- /dev/null +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/util/ServletUtilTest.java @@ -0,0 +1,95 @@ +package org.cloudfoundry.multiapps.controller.web.util; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; + +import jakarta.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.web.servlet.HandlerMapping; + +class ServletUtilTest { + + @Mock + private ServletRequest servletRequest; + @Mock + private HttpServletRequest httpServletRequest; + @Mock + private HttpServletResponse httpServletResponse; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this) + .close(); + } + + @Test + void testGetPathVariablesReturnsMapFromRequestAttribute() { + Map pathVariables = Map.of("id", "42"); + Mockito.when(servletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)) + .thenReturn(pathVariables); + + Map result = ServletUtil.getPathVariables(servletRequest); + + Assertions.assertEquals(pathVariables, result); + } + + @Test + void testGetPathVariableLooksUpByName() { + Mockito.when(servletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE)) + .thenReturn(Map.of("operationId", "op-123")); + + String result = ServletUtil.getPathVariable(servletRequest, "operationId"); + + Assertions.assertEquals("op-123", result); + } + + @Test + void testDecodeUriReadsAndDecodesRequestURI() { + Mockito.when(httpServletRequest.getRequestURI()) + .thenReturn("/path/with%20space"); + + String result = ServletUtil.decodeUri(httpServletRequest); + + Assertions.assertEquals("/path/with space", result); + } + + @Test + void testDecodeHandlesPercentEncodedString() { + Assertions.assertEquals("hello world", ServletUtil.decode("hello%20world")); + } + + @Test + void testRemoveInvalidForwardSlashesCollapsesRepeatedSlashes() { + Assertions.assertEquals("/a/b/c", ServletUtil.removeInvalidForwardSlashes("/a//b///c")); + } + + @Test + void testRemoveInvalidForwardSlashesLeavesSingleSlashesUnchanged() { + Assertions.assertEquals("/a/b", ServletUtil.removeInvalidForwardSlashes("/a/b")); + } + + @Test + void testSendWritesBodyAndStatus() throws IOException { + StringWriter buffer = new StringWriter(); + Mockito.when(httpServletResponse.getWriter()) + .thenReturn(new PrintWriter(buffer)); + + ServletUtil.send(httpServletResponse, 418, "I'm a teapot"); + + Mockito.verify(httpServletResponse) + .setStatus(418); + Mockito.verify(httpServletResponse) + .setCharacterEncoding("UTF-8"); + Assertions.assertEquals("I'm a teapot", buffer.toString()); + } +} diff --git a/sonar-project.properties b/sonar-project.properties index 7db668a931..e66de56b4c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,7 +3,19 @@ sonar.organization=cloudfoundry sonar.projectName="MultiApps Controller Parent" +sonar.exclusions=\ + **/target/generated-sources/**,\ + **/target/generated-test-sources/**,\ + multiapps-controller-coverage/** + sonar.coverage.exclusions=\ + multiapps-controller-core-test/**,\ + multiapps-controller-persistence-test/**,\ + multiapps-controller-api/**,\ + **/target/generated-sources/**,\ + **/Immutable*.java,\ + **/Application.java,\ + **/*Bootstrap.java,\ **/DatabaseConfiguration.java,\ **/FlowableConfiguration.java,\ **/FlowableServicesConfiguration.java,\