diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/Automaton.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/Automaton.java index 6ccf31a2..2c2d3b0d 100644 --- a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/Automaton.java +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/Automaton.java @@ -4,6 +4,10 @@ import com.yanchware.fractal.sdk.configuration.EnvVarSdkConfiguration; import com.yanchware.fractal.sdk.configuration.SdkConfiguration; import com.yanchware.fractal.sdk.configuration.instantiation.InstantiationConfiguration; +import com.yanchware.fractal.sdk.domain.accounts.AccountAggregate; +import com.yanchware.fractal.sdk.domain.accounts.AccountsFactory; +import com.yanchware.fractal.sdk.domain.accounts.OrganizationAggregate; +import com.yanchware.fractal.sdk.domain.accounts.OrganizationFactory; import com.yanchware.fractal.sdk.domain.blueprint.BlueprintFactory; import com.yanchware.fractal.sdk.domain.environment.EnvironmentAggregate; import com.yanchware.fractal.sdk.domain.environment.EnvironmentIdValue; @@ -40,6 +44,8 @@ public class Automaton { private static BlueprintFactory blueprintFactory; private static LiveSystemsFactory liveSystemFactory; private static EnvironmentsFactory environmentsFactory; + private static AccountsFactory accountsFactory; + private static OrganizationFactory organizationFactory; private static RetryRegistry serviceRetryRegistry; private Automaton(HttpClient httpClient, SdkConfiguration sdkConfiguration) { @@ -47,6 +53,8 @@ private Automaton(HttpClient httpClient, SdkConfiguration sdkConfiguration) { Automaton.blueprintFactory = new BlueprintFactory(httpClient, sdkConfiguration, Automaton.serviceRetryRegistry); Automaton.liveSystemFactory = new LiveSystemsFactory(httpClient, sdkConfiguration, Automaton.serviceRetryRegistry); Automaton.environmentsFactory = new EnvironmentsFactory(httpClient, sdkConfiguration, Automaton.serviceRetryRegistry); + Automaton.accountsFactory = new AccountsFactory(httpClient, sdkConfiguration, Automaton.serviceRetryRegistry); + Automaton.organizationFactory = new OrganizationFactory(httpClient, sdkConfiguration, Automaton.serviceRetryRegistry); } /** @@ -81,6 +89,24 @@ public LiveSystemsFactory.LiveSystemBuilder getLiveSystemBuilder() { } /** + * Get builder for Account Aggregate + * + * @return + */ + public AccountsFactory.AccountBuilder getAccountBuilder() { + return accountsFactory.builder(); + } + + /** + * Get builder for Organization Aggregate + * + * @return + */ + public OrganizationFactory.OrganizationBuilder getOrganizationBuilder() { + return organizationFactory.builder(); + } + + /** * Instantiates the given environment. * * @param environment the environment to be instantiated @@ -91,6 +117,27 @@ public void instantiate(EnvironmentAggregate environment) throws InstantiatorExc } /** + * Instantiates the given account. + * + * @param accounts the account to be instantiated + * @throws InstantiatorException if an error occurs + */ + public void instantiate(AccountAggregate accounts) throws InstantiatorException { + accounts.createOrUpdate(); + } + + /** + * Instantiates the given organization aggregate by reconciling organizational Resource Groups. + * + * @param organization the organization to be instantiated + * @throws InstantiatorException if an error occurs + */ + public void instantiate(OrganizationAggregate organization) throws InstantiatorException { + organization.createOrUpdate(); + } + + + /** * Instantiates the given list of live systems. * * @param liveSystems the list of live systems to be instantiated diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/Constants.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/Constants.java index abbb62db..e9080216 100644 --- a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/Constants.java +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/Constants.java @@ -4,6 +4,7 @@ public class Constants { public static final String LIVESYSTEM_ENDPOINT_KEY = "LIVESYSTEM_ENDPOINT"; public static final String BLUEPRINT_ENDPOINT_KEY = "BLUEPRINT_ENDPOINT"; public static final String ENVIRONMENTS_ENDPOINT_KEY = "ENVIRONMENTS_ENDPOINT"; + public static final String ACCOUNTS_ENDPOINT_KEY = "ACCOUNTS_ENDPOINT"; public static final String GIT_COMMIT_ID_KEY = "commitId"; public static final String X_CLIENT_ID_HEADER = "X-ClientID"; diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/EnvVarSdkConfiguration.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/EnvVarSdkConfiguration.java index 85a07fb2..ea68a1a5 100644 --- a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/EnvVarSdkConfiguration.java +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/EnvVarSdkConfiguration.java @@ -29,6 +29,10 @@ public class EnvVarSdkConfiguration implements SdkConfiguration { */ public final URI DEFAULT_ENVIRONMENTS_ENDPOINT; + /** + * The default URI for the accounts endpoint. + */ + public final URI DEFAULT_ACCOUNTS_ENDPOINT; /** * Constructs an instance of {@link EnvVarSdkConfiguration}. * @@ -38,6 +42,7 @@ public EnvVarSdkConfiguration() throws URISyntaxException { DEFAULT_LIVESYSTEM_ENDPOINT = new URI("https://api.fractal.cloud/livesystems"); DEFAULT_BLUEPRINT_ENDPOINT = new URI("https://api.fractal.cloud/blueprints"); DEFAULT_ENVIRONMENTS_ENDPOINT = new URI("https://api.fractal.cloud/environments"); + DEFAULT_ACCOUNTS_ENDPOINT = new URI("http://localhost:5001/accounts"); } /** @@ -129,6 +134,14 @@ public URI getEnvironmentsEndpoint() { return checkAndReturnUri(ENVIRONMENTS_ENDPOINT_KEY, DEFAULT_ENVIRONMENTS_ENDPOINT); } + /** + * Gets the URI of the accounts endpoint from environment variables or default. + * @return the URI of the accounts endpoint + */ + @Override + public URI getAccountsEndpoint(){ + return checkAndReturnUri(ACCOUNTS_ENDPOINT_KEY, DEFAULT_ACCOUNTS_ENDPOINT); + } /** * Gets the AWS Access Key ID, part of the temporary credentials of an AWS role, from environment variables. * The environment variable key is {@value com.yanchware.fractal.sdk.configuration.Constants#AWS_ACCESS_KEY_ID_KEY}. diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/SdkConfiguration.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/SdkConfiguration.java index 0d6218ac..b550f8f9 100644 --- a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/SdkConfiguration.java +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/configuration/SdkConfiguration.java @@ -56,6 +56,13 @@ public interface SdkConfiguration { */ URI getEnvironmentsEndpoint(); + /** + * Gets the URI of the accounts endpoint. + * + * @return the URI of the accounts endpoint + */ + URI getAccountsEndpoint(); + /** * Gets the AWS Access Key ID. * diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountAggregate.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountAggregate.java new file mode 100644 index 00000000..0f9c6b44 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountAggregate.java @@ -0,0 +1,72 @@ +package com.yanchware.fractal.sdk.domain.accounts; + +import com.yanchware.fractal.sdk.domain.accounts.service.AccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.PersonalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import lombok.AccessLevel; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Setter(AccessLevel.PROTECTED) +public class AccountAggregate { + + private final AccountsService service; + + public record PersonalResourceGroup(String shortName, String displayName) { + } + + private final List personalResourceGroups = new ArrayList<>(); + + public AccountAggregate(RestAccountsService restAccountsService) { + this.service = restAccountsService; + } + + public void addPersonalResourceGroup(String shortName, String displayName) { + int idx = -1; + for (int i = 0; i < personalResourceGroups.size(); i++) { + if (personalResourceGroups.get(i).shortName().equals(shortName)) { + idx = i; + break; + } + } + if (idx >= 0) { + var existing = personalResourceGroups.get(idx); + if (!existing.displayName().equals(displayName)) { + personalResourceGroups.set(idx, new PersonalResourceGroup(shortName, displayName)); + } + return; + } + personalResourceGroups.add(new PersonalResourceGroup(shortName, displayName)); + } + + + public void createOrUpdate() throws InstantiatorException { + if (personalResourceGroups.isEmpty()) { + log.info("No personal resource groups to manage."); + return; + } + + int created = 0; + for (var rg : personalResourceGroups) { + var shortName = rg.shortName(); + var displayName = rg.displayName(); + + PersonalResourceGroupResponse existing = service.getPersonalResourceGroupByShortName(shortName); + + if (existing == null) { + log.info("Creating Personal Resource Group [shortName: '{}', displayName: '{}']", shortName, displayName); + var response = service.upsertPersonalResourceGroup(shortName, displayName); + log.debug("Upserted Personal Resource Group id: {}", response.Id()); + created++; + } else { + log.info("Personal Resource Group [shortName: '{}'] already exists. Skipping.", shortName); + } + } + log.info("Created {} new Personal Resource Group(s).", created); + } +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountsFactory.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountsFactory.java new file mode 100644 index 00000000..33386cd3 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/AccountsFactory.java @@ -0,0 +1,48 @@ +package com.yanchware.fractal.sdk.domain.accounts; + +import com.yanchware.fractal.sdk.configuration.SdkConfiguration; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import io.github.resilience4j.retry.RetryRegistry; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.net.http.HttpClient; + +@Slf4j +@AllArgsConstructor +public class AccountsFactory { + private final HttpClient client; + private final SdkConfiguration sdkConfiguration; + private final RetryRegistry retryRegistry; + + public AccountBuilder builder() { + return new AccountBuilder(client, sdkConfiguration, retryRegistry); + } + + public static class AccountBuilder { + private final AccountAggregate aggregate; + private final AccountBuilder builder; + + public AccountBuilder(HttpClient client, SdkConfiguration sdkConfiguration, RetryRegistry retryRegistry) { + aggregate = create(client, sdkConfiguration, retryRegistry); + builder = getBuilder(); + } + + protected AccountAggregate create(HttpClient client, SdkConfiguration sdkConfiguration, RetryRegistry retryRegistry) { + return new AccountAggregate(new RestAccountsService(client, sdkConfiguration, retryRegistry)); + } + + protected AccountsFactory.AccountBuilder getBuilder() { + return this; + } + + public AccountBuilder withResourceGroup(String shortName, String displayName) { + aggregate.addPersonalResourceGroup(shortName, displayName); + return builder; + } + + public AccountAggregate build() { + return aggregate; + } + } +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/EntityStatus.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/EntityStatus.java new file mode 100644 index 00000000..ba5f8807 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/EntityStatus.java @@ -0,0 +1,37 @@ +package com.yanchware.fractal.sdk.domain.accounts; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum EntityStatus { + UNKNOWN("Unknown"), + ACTIVE("Active"), + DELETING("Deleting"); + + private final String value; + + EntityStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } + + @JsonCreator + public static EntityStatus fromString(String s) { + if (s == null) return null; + for (var e : values()) { + if (e.value.equalsIgnoreCase(s) || e.name().equalsIgnoreCase(s)) { + return e; + } + } + throw new IllegalArgumentException("Invalid EntityStatus: " + s); + } +} \ No newline at end of file diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationAggregate.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationAggregate.java new file mode 100644 index 00000000..fecd4ad6 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationAggregate.java @@ -0,0 +1,79 @@ +package com.yanchware.fractal.sdk.domain.accounts; + +import com.yanchware.fractal.sdk.domain.accounts.service.AccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.OrganizationalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import lombok.AccessLevel; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Setter(AccessLevel.PROTECTED) +public class OrganizationAggregate { + + private final AccountsService service; + + public record OrganizationalResourceGroup(UUID organizationId, String shortName, String displayName) {} + + private final List organizationalResourceGroups = new ArrayList<>(); + + public OrganizationAggregate(RestAccountsService restAccountsService) { + this.service = restAccountsService; + } + + public void addOrganizationalResourceGroup(UUID organizationId, String shortName, String displayName) { + int idx = -1; + for (int i = 0; i < organizationalResourceGroups.size(); i++) { + var item = organizationalResourceGroups.get(i); + if (item.organizationId().equals(organizationId) && item.shortName().equals(shortName)) { + idx = i; + break; + } + } + if (idx >= 0) { + var existing = organizationalResourceGroups.get(idx); + if (!existing.displayName().equals(displayName)) { + organizationalResourceGroups.set(idx, new OrganizationalResourceGroup(organizationId, shortName, displayName)); + } + return; + } + organizationalResourceGroups.add(new OrganizationalResourceGroup(organizationId, shortName, displayName)); + } + + public void createOrUpdate() throws InstantiatorException { + if (organizationalResourceGroups.isEmpty()) { + log.info("No organizational resource groups to manage."); + return; + } + + int created = 0; + for (var rg : organizationalResourceGroups) { + var organizationId = rg.organizationId(); + var shortName = rg.shortName(); + var displayName = rg.displayName(); + + OrganizationalResourceGroupResponse existing = + service.getOrganizationalResourceGroupByShortName(organizationId, shortName); + + if (existing == null) { + log.info("Creating Organizational Resource Group [organizationId: '{}', shortName: '{}', displayName: '{}']", + organizationId, shortName, displayName); + + var response = service.upsertOrganizationalResourceGroup(organizationId, shortName, displayName); + if (response != null) { + log.debug("Upserted Organizational Resource Group id: {}", response.Id()); + } + created++; + } else { + log.info("Organizational Resource Group [organizationId: '{}', shortName: '{}'] already exists. Skipping.", + organizationId, shortName); + } + } + log.info("Created {} new Organizational Resource Group(s).", created); + } +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationFactory.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationFactory.java new file mode 100644 index 00000000..f6a55f20 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/OrganizationFactory.java @@ -0,0 +1,49 @@ +package com.yanchware.fractal.sdk.domain.accounts; + +import com.yanchware.fractal.sdk.configuration.SdkConfiguration; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import io.github.resilience4j.retry.RetryRegistry; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.net.http.HttpClient; +import java.util.UUID; + +@Slf4j +@AllArgsConstructor +public class OrganizationFactory { + private final HttpClient client; + private final SdkConfiguration sdkConfiguration; + private final RetryRegistry retryRegistry; + + public OrganizationBuilder builder() { + return new OrganizationBuilder(client, sdkConfiguration, retryRegistry); + } + + public static class OrganizationBuilder { + private final OrganizationAggregate aggregate; + private final OrganizationBuilder builder; + + public OrganizationBuilder(HttpClient client, SdkConfiguration sdkConfiguration, RetryRegistry retryRegistry) { + aggregate = create(client, sdkConfiguration, retryRegistry); + builder = getBuilder(); + } + + protected OrganizationAggregate create(HttpClient client, SdkConfiguration sdkConfiguration, RetryRegistry retryRegistry) { + return new OrganizationAggregate(new RestAccountsService(client, sdkConfiguration, retryRegistry)); + } + + protected OrganizationBuilder getBuilder() { + return this; + } + + public OrganizationBuilder withResourceGroup(UUID organizationId, String shortName, String displayName) { + aggregate.addOrganizationalResourceGroup(organizationId, shortName, displayName); + return builder; + } + + public OrganizationAggregate build() { + return aggregate; + } + } +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/AccountsService.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/AccountsService.java new file mode 100644 index 00000000..3da5eaf1 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/AccountsService.java @@ -0,0 +1,17 @@ +package com.yanchware.fractal.sdk.domain.accounts.service; + +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.OrganizationalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.PersonalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; + +import java.util.UUID; + +public interface AccountsService { + OrganizationalResourceGroupResponse upsertOrganizationalResourceGroup(UUID organizationId, String shortName, String displayName) throws InstantiatorException; + + OrganizationalResourceGroupResponse getOrganizationalResourceGroupByShortName(UUID organizationId, String shortName) throws InstantiatorException; + + PersonalResourceGroupResponse upsertPersonalResourceGroup(String shortName, String displayName) throws InstantiatorException; + + PersonalResourceGroupResponse getPersonalResourceGroupByShortName(String shortName) throws InstantiatorException; +} \ No newline at end of file diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/RestAccountsService.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/RestAccountsService.java new file mode 100644 index 00000000..e90874ea --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/RestAccountsService.java @@ -0,0 +1,108 @@ +package com.yanchware.fractal.sdk.domain.accounts.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.yanchware.fractal.sdk.configuration.SdkConfiguration; +import com.yanchware.fractal.sdk.domain.Service; +import com.yanchware.fractal.sdk.domain.accounts.service.commands.UpsertPersonalResourceGroupRequest; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.OrganizationalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.PersonalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import com.yanchware.fractal.sdk.utils.HttpUtils; +import io.github.resilience4j.retry.RetryRegistry; + +import java.net.URI; +import java.net.http.HttpClient; +import java.util.UUID; + +import static com.yanchware.fractal.sdk.utils.SerializationUtils.serialize; + + +public class RestAccountsService extends Service implements AccountsService { + public RestAccountsService(HttpClient client, SdkConfiguration sdkConfiguration, RetryRegistry retryRegistry) { + super(client, sdkConfiguration, retryRegistry); + } + + @Override + public OrganizationalResourceGroupResponse upsertOrganizationalResourceGroup(UUID organizationId, String shortName, String displayName) throws InstantiatorException { + return executeRequestWithRetries( + "upsertOrganizationalResourceGroup", + shortName, + client, + retryRegistry, + HttpUtils.buildPostRequest( + getOrganizationalResourceGroupsUri(organizationId,shortName), + sdkConfiguration, + serializeSafely(new UpsertPersonalResourceGroupRequest(displayName, null, null))), + new int[]{200}, + OrganizationalResourceGroupResponse.class); + } + + @Override + public OrganizationalResourceGroupResponse getOrganizationalResourceGroupByShortName(UUID organizationId, String shortName) throws InstantiatorException { + return executeRequestWithRetries( + "getOrganizationalResourceGroupByShortName", + organizationId + "/" + shortName, + client, + retryRegistry, + HttpUtils.buildGetRequest( + getOrganizationalResourceGroupsUri(organizationId, shortName), + sdkConfiguration), + new int[]{200, 404}, + OrganizationalResourceGroupResponse.class); + } + + + @Override + public PersonalResourceGroupResponse upsertPersonalResourceGroup(String shortName, String displayName) throws InstantiatorException { + return executeRequestWithRetries( + "upsertPersonalResourceGroup", + shortName, + client, + retryRegistry, + HttpUtils.buildPostRequest( + getPersonalResourceGroupsUri(shortName), + sdkConfiguration, + serializeSafely(new UpsertPersonalResourceGroupRequest(displayName, null, null))), + new int[]{200}, + PersonalResourceGroupResponse.class); + } + + @Override + public PersonalResourceGroupResponse getPersonalResourceGroupByShortName(String shortName) throws InstantiatorException { + return executeRequestWithRetries( + "getPersonalResourceGroupByShortName", + shortName, + client, + retryRegistry, + HttpUtils.buildGetRequest( + getPersonalResourceGroupsUri(shortName), + sdkConfiguration), + new int[]{200, 404}, + PersonalResourceGroupResponse.class); + } + + private URI getPersonalResourceGroupsUri(String shortName) { + var path = String.format("%s/%s/", + sdkConfiguration.getAccountsEndpoint(), + "resourcegroups"); // or your existing segment if already correct + return URI.create(path + shortName); + } + + private URI getOrganizationalResourceGroupsUri(UUID organizationId, String shortName) { + var base = String.format("%s/%s/%s/%s/", + sdkConfiguration.getAccountsEndpoint(), + "organizations", + organizationId, + "resourcegroups"); + return URI.create(base + shortName); + } + + + private String serializeSafely(Object command) throws InstantiatorException { + try { + return serialize(command); + } catch (JsonProcessingException e) { + throw new InstantiatorException("Error serializing command because of JsonProcessing error", e); + } + } +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertOrganizationalResourceGroupRequest.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertOrganizationalResourceGroupRequest.java new file mode 100644 index 00000000..6a5a802a --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertOrganizationalResourceGroupRequest.java @@ -0,0 +1,3 @@ +package com.yanchware.fractal.sdk.domain.accounts.service.commands; + +public record UpsertOrganizationalResourceGroupRequest(String DisplayName, String Description, String Icon){ } \ No newline at end of file diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertPersonalResourceGroupRequest.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertPersonalResourceGroupRequest.java new file mode 100644 index 00000000..8666a208 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/commands/UpsertPersonalResourceGroupRequest.java @@ -0,0 +1,4 @@ +package com.yanchware.fractal.sdk.domain.accounts.service.commands; + +public record UpsertPersonalResourceGroupRequest(String DisplayName, String Description, String Icon) { +} diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/OrganizationalResourceGroupResponse.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/OrganizationalResourceGroupResponse.java new file mode 100644 index 00000000..15fa5a2b --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/OrganizationalResourceGroupResponse.java @@ -0,0 +1,18 @@ +package com.yanchware.fractal.sdk.domain.accounts.service.dtos; + +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupId; +import java.util.Collection; + +public record OrganizationalResourceGroupResponse( + ResourceGroupId Id, + String DisplayName, + String Description, + EntityStatus Status, + String Icon, + Collection Members, + Collection TeamsIds, + Collection ManagersIds, + Collection Livesystems, + Collection Fractals +) {} \ No newline at end of file diff --git a/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/PersonalResourceGroupResponse.java b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/PersonalResourceGroupResponse.java new file mode 100644 index 00000000..07e68530 --- /dev/null +++ b/fractal.sdk/src/main/java/com/yanchware/fractal/sdk/domain/accounts/service/dtos/PersonalResourceGroupResponse.java @@ -0,0 +1,16 @@ +package com.yanchware.fractal.sdk.domain.accounts.service.dtos; + +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupId; + +import java.util.Collection; + +public record PersonalResourceGroupResponse( + ResourceGroupId Id, + String DisplayName, + String Description, + EntityStatus Status, + String Icon, + Collection Livesystems, + Collection Fractals +) {} diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/AutomatonTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/AutomatonTest.java index 84a85bf0..90161c17 100644 --- a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/AutomatonTest.java +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/AutomatonTest.java @@ -5,54 +5,197 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; import com.yanchware.fractal.sdk.domain.environment.EnvironmentIdValue; import com.yanchware.fractal.sdk.domain.environment.service.dtos.EnvironmentResponse; import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; import com.yanchware.fractal.sdk.domain.livesystem.LiveSystemIdValue; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupType; import com.yanchware.fractal.sdk.utils.LocalSdkConfiguration; import org.junit.jupiter.api.Test; import java.net.URISyntaxException; import java.net.http.HttpClient; +import java.util.UUID; import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @WireMockTest public class AutomatonTest extends TestWithFixture { + private static final String SHORT_NAME = "rg-sdk"; + private static final String DISPLAY_NAME = "SDK RG"; + + @Test + public void success_when_deleteLiveSystem(WireMockRuntimeInfo wmRuntimeInfo) throws URISyntaxException, InstantiatorException, JsonProcessingException { + var httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); + Automaton.initializeAutomaton(httpClient, sdkConfiguration); + var sut = Automaton.getInstance(); - @Test - public void success_when_deleteLiveSystem(WireMockRuntimeInfo wmRuntimeInfo) throws URISyntaxException, InstantiatorException, JsonProcessingException { - var httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_2) - .build(); - var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); - Automaton.initializeAutomaton(httpClient, sdkConfiguration); - var sut = Automaton.getInstance(); - - var liveSystemIds = aListOf(LiveSystemIdValue.class); - var environmentResponse = a(EnvironmentResponse.class); - var environmentId = environmentResponse.id(); - - stubFor(get(urlPathMatching(String.format("/environments/%s/%s/%s", environmentId.type(), environmentId.ownerId(), environmentId.shortName()))) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(new ObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .writeValueAsString(environmentResponse)))); - - stubFor(delete(urlPathMatching("/livesystems/.*")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json"))); - - sut.delete(EnvironmentIdValue.fromDto(environmentResponse.id()), liveSystemIds); - - for(var liveSystemId : liveSystemIds) { - verify(deleteRequestedFor(urlPathEqualTo(String.format("/livesystems/%s", liveSystemId)))); + var liveSystemIds = aListOf(LiveSystemIdValue.class); + var environmentResponse = a(EnvironmentResponse.class); + var environmentId = environmentResponse.id(); + + stubFor(get(urlPathMatching(String.format("/environments/%s/%s/%s", environmentId.type(), environmentId.ownerId(), environmentId.shortName()))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(new ObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .writeValueAsString(environmentResponse)))); + + stubFor(delete(urlPathMatching("/livesystems/.*")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json"))); + + sut.delete(EnvironmentIdValue.fromDto(environmentResponse.id()), liveSystemIds); + + for (var liveSystemId : liveSystemIds) { + verify(deleteRequestedFor(urlPathEqualTo(String.format("/livesystems/%s", liveSystemId)))); + } + } + + @Test + public void instantiateAccount_createsPersonalResourceGroup_whenNotExists(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + var httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); + Automaton.initializeAutomaton(httpClient, sdkConfiguration); + var sut = Automaton.getInstance(); + + var getUrl = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + stubFor(get(getUrl).willReturn(aResponse().withStatus(404))); + + stubFor(post(getUrl) + .withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.PERSONAL, UUID.randomUUID(), SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + var accountAggregate = sut.getAccountBuilder() + .withResourceGroup(SHORT_NAME, DISPLAY_NAME) + .build(); + + sut.instantiate(accountAggregate); + + verify(1, getRequestedFor(getUrl)); + verify(1, postRequestedFor(getUrl).withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME)))); + } + + @Test + public void instantiateOrganization_createsOrganizationalResourceGroup_whenNotExists(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + var httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); + Automaton.initializeAutomaton(httpClient, sdkConfiguration); + var sut = Automaton.getInstance(); + + var orgId = java.util.UUID.randomUUID(); + + var path = String.format("/accounts/organizations/%s/resourcegroups/%s", orgId, SHORT_NAME); + var getUrl = urlPathMatching(path); + stubFor(get(getUrl).willReturn(aResponse().withStatus(404))); + + stubFor(post(getUrl) + .withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.ORGANIZATIONAL, UUID.randomUUID(), SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + var orgAggregate = sut.getOrganizationBuilder() + .withResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME) + .build(); + + sut.instantiate(orgAggregate); + + verify(1, getRequestedFor(getUrl)); + verify(1, postRequestedFor(getUrl).withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME)))); } - } + @Test + public void instantiateAccount_skipsWhenExists(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + var httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); + Automaton.initializeAutomaton(httpClient, sdkConfiguration); + var sut = Automaton.getInstance(); + + var getUrl = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + + // Existing RG → GET returns 200, so no POST should be issued + stubFor(get(getUrl) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.PERSONAL, UUID.randomUUID(), SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + + var accountAggregate = sut.getAccountBuilder() + .withResourceGroup(SHORT_NAME, DISPLAY_NAME) + .build(); + + sut.instantiate(accountAggregate); + + verify(1, getRequestedFor(getUrl)); + verify(0, postRequestedFor(getUrl)); + } + + @Test + public void instantiateOrganization_skipsWhenExists(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + var httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build(); + var sdkConfiguration = new LocalSdkConfiguration(wmRuntimeInfo.getHttpBaseUrl()); + Automaton.initializeAutomaton(httpClient, sdkConfiguration); + var sut = Automaton.getInstance(); + + var orgId = java.util.UUID.randomUUID(); + var path = String.format("/accounts/organizations/%s/resourcegroups/%s", orgId, SHORT_NAME); + var getUrl = urlPathMatching(path); + + // Existing org RG → GET returns 200, so no POST should be issued + stubFor(get(getUrl) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.ORGANIZATIONAL, UUID.randomUUID(), SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + + var orgAggregate = sut.getOrganizationBuilder() + .withResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME) + .build(); + + sut.instantiate(orgAggregate); + + verify(1, getRequestedFor(getUrl)); + verify(0, postRequestedFor(getUrl)); + } + + @Test + void automaton_provides_account_and_organization_builders() throws Exception { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration("http://localhost:8080"); + Automaton.initializeAutomaton(httpClient, sdkConfig); + + var automaton = Automaton.getInstance(); + + assertThat(automaton.getAccountBuilder()).isNotNull(); + assertThat(automaton.getAccountBuilder().withResourceGroup(SHORT_NAME, DISPLAY_NAME).build()).isNotNull(); + + assertThat(automaton.getOrganizationBuilder()).isNotNull(); + assertThat(automaton.getOrganizationBuilder() + .withResourceGroup(UUID.randomUUID(), SHORT_NAME, DISPLAY_NAME) + .build()).isNotNull(); + } } diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountAggregateTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountAggregateTest.java new file mode 100644 index 00000000..2d30dbef --- /dev/null +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountAggregateTest.java @@ -0,0 +1,150 @@ +package com.yanchware.fractal.sdk.domain.account; + +import com.yanchware.fractal.sdk.domain.accounts.AccountAggregate; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.PersonalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupId; +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +class AccountAggregateTest { + private static final String SHORT_NAME = "test-rg"; + private static final String DISPLAY_NAME = "Test Resource Group"; + + @Test + void createsPersonalResourceGroup_when_NotExists() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + aggregate.addPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME); + + when(mockService.getPersonalResourceGroupByShortName(eq(SHORT_NAME))).thenReturn(null); + var createdResponse = new PersonalResourceGroupResponse( + ResourceGroupId.fromString(String.format("Personal/%s/%s", UUID.randomUUID(), SHORT_NAME)), + DISPLAY_NAME, + null, + EntityStatus.ACTIVE, + null, + List.of(), + List.of() + ); + when(mockService.upsertPersonalResourceGroup(eq(SHORT_NAME), eq(DISPLAY_NAME))) + .thenReturn(createdResponse); + + + aggregate.createOrUpdate(); + + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(eq(SHORT_NAME)); + verify(mockService, times(1)).upsertPersonalResourceGroup(eq(SHORT_NAME), eq(DISPLAY_NAME)); + verifyNoMoreInteractions(mockService); + } + + @Test + void skipsUpsert_when_AlreadyExists() throws InstantiatorException { + + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + aggregate.addPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME); + + var existingResponse = new PersonalResourceGroupResponse( + ResourceGroupId.fromString(String.format("Personal/%s/%s", UUID.randomUUID(), SHORT_NAME)), + "Existing RG", + null, + EntityStatus.ACTIVE, + null, + List.of(), + List.of() + ); + + when(mockService.getPersonalResourceGroupByShortName(eq(SHORT_NAME))).thenReturn(existingResponse); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(eq(SHORT_NAME)); + verify(mockService, never()).upsertPersonalResourceGroup(anyString(), anyString()); + verifyNoMoreInteractions(mockService); + } + + @Test + void updatesDisplayName_when_AddedTwiceWithDifferentDisplayName() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + + aggregate.addPersonalResourceGroup(SHORT_NAME, "Old Name"); + aggregate.addPersonalResourceGroup(SHORT_NAME, "New Name"); + + when(mockService.getPersonalResourceGroupByShortName(eq(SHORT_NAME))).thenReturn(null); + when(mockService.upsertPersonalResourceGroup(eq(SHORT_NAME), eq("New Name"))) + .thenReturn(mock(PersonalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(eq(SHORT_NAME)); + verify(mockService, times(1)).upsertPersonalResourceGroup(eq(SHORT_NAME), eq("New Name")); + verifyNoMoreInteractions(mockService); + } + + @Test + void doesNothing_when_NoResourceGroupsStaged() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + aggregate.createOrUpdate(); + + verifyNoMoreInteractions(mockService); + } + + @Test + void createsMultipleResourceGroups_when_MultipleStaged() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + aggregate.addPersonalResourceGroup(SHORT_NAME + "a", DISPLAY_NAME + "A"); + aggregate.addPersonalResourceGroup(SHORT_NAME + "b", DISPLAY_NAME + "B"); + + when(mockService.getPersonalResourceGroupByShortName(SHORT_NAME + "a")).thenReturn(null); + when(mockService.getPersonalResourceGroupByShortName(SHORT_NAME + "b")).thenReturn(null); + + when(mockService.upsertPersonalResourceGroup(SHORT_NAME + "a", DISPLAY_NAME + "A")).thenReturn(mock(PersonalResourceGroupResponse.class)); + when(mockService.upsertPersonalResourceGroup(SHORT_NAME + "b", DISPLAY_NAME + "B")).thenReturn(mock(PersonalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(SHORT_NAME + "a"); + verify(mockService, times(1)).getPersonalResourceGroupByShortName(SHORT_NAME + "b"); + verify(mockService, times(1)).upsertPersonalResourceGroup(SHORT_NAME + "a", DISPLAY_NAME + "A"); + verify(mockService, times(1)).upsertPersonalResourceGroup(SHORT_NAME + "b", DISPLAY_NAME + "B"); + verifyNoMoreInteractions(mockService); + } + + @Test + void propagatesException_when_ServiceGetFails() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new AccountAggregate(mockService); + + aggregate.addPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME); + + when(mockService.getPersonalResourceGroupByShortName(eq(SHORT_NAME))) + .thenThrow(new InstantiatorException("error")); + + assertThatThrownBy(aggregate::createOrUpdate) + .isInstanceOf(InstantiatorException.class) + .hasMessageContaining("error"); + } +} \ No newline at end of file diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountFactoryTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountFactoryTest.java new file mode 100644 index 00000000..18eb1261 --- /dev/null +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/AccountFactoryTest.java @@ -0,0 +1,182 @@ +package com.yanchware.fractal.sdk.domain.account; + +import com.yanchware.fractal.sdk.configuration.SdkConfiguration; +import com.yanchware.fractal.sdk.domain.accounts.AccountAggregate; +import com.yanchware.fractal.sdk.domain.accounts.AccountsFactory; +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.PersonalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupId; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupType; +import com.yanchware.fractal.sdk.utils.LocalSdkConfiguration; +import io.github.resilience4j.retry.RetryRegistry; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpClient; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +public class AccountFactoryTest { + + private static final String BASE_URL = "http://localhost:8080"; + private static final String SHORT_NAME = "rg-1"; + private static final String DISPLAY_NAME = "RG One"; + + @Test + void builder_buildsAggregate_and_stagesPersonalResourceGroup() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var factory = new AccountsFactory(httpClient, sdkConfig, retry); + var aggregate = factory.builder() + .withResourceGroup(SHORT_NAME, DISPLAY_NAME) + .build(); + + assertThat(aggregate).isNotNull(); + } + + @Test + void builder_isFluent_and_returnsSameInstance() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var builder = new AccountsFactory(httpClient, sdkConfig, retry).builder(); + var returned = builder.withResourceGroup(SHORT_NAME, DISPLAY_NAME); + + assertThat(returned).isSameAs(builder); + } + + @Test + void build_returnsSameAggregate_onRepeatedBuildCalls() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var builder = new AccountsFactory(httpClient, sdkConfig, retry).builder().withResourceGroup(SHORT_NAME, DISPLAY_NAME); + var aggregate1 = builder.build(); + var aggregate2 = builder.build(); + + assertThat(aggregate1).isSameAs(aggregate2); + } + + @Test + void withResourceGroup_stages_and_createOrUpdate_callsService_when_NotExists() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + when(mockService.getPersonalResourceGroupByShortName(eq(SHORT_NAME))).thenReturn(null); + + var createdResponse = new PersonalResourceGroupResponse( + ResourceGroupId.fromString(String.format("%s/%s/%s", ResourceGroupType.PERSONAL, UUID.randomUUID(), SHORT_NAME)), + DISPLAY_NAME, + null, + EntityStatus.ACTIVE, + null, + List.of(), + List.of() + ); + when(mockService.upsertPersonalResourceGroup(eq(SHORT_NAME), eq(DISPLAY_NAME))).thenReturn(createdResponse); + + var builder = new AccountsFactory.AccountBuilder(httpClient, sdkConfig, retry) { + @Override + protected AccountAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new AccountAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(SHORT_NAME, DISPLAY_NAME) + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(eq(SHORT_NAME)); + verify(mockService, times(1)).upsertPersonalResourceGroup(eq(SHORT_NAME), eq(DISPLAY_NAME)); + verifyNoMoreInteractions(mockService); + } + + @Test + void withResourceGroup_chainMultiple_stagesAll_and_callsService() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + var shortNameA = SHORT_NAME + "-a"; + var displayA = DISPLAY_NAME + " A"; + var shortNameB = SHORT_NAME + "-b"; + var displayB = DISPLAY_NAME + " B"; + + + when(mockService.getPersonalResourceGroupByShortName(shortNameA)).thenReturn(null); + when(mockService.getPersonalResourceGroupByShortName(shortNameB)).thenReturn(null); + + when(mockService.upsertPersonalResourceGroup(shortNameA, displayA)).thenReturn(mock(PersonalResourceGroupResponse.class)); + when(mockService.upsertPersonalResourceGroup(shortNameB, displayB)).thenReturn(mock(PersonalResourceGroupResponse.class)); + + var builder = new AccountsFactory.AccountBuilder(httpClient, sdkConfig, retry) { + @Override + protected AccountAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new AccountAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(shortNameA, displayA) + .withResourceGroup(shortNameB, displayB) + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(shortNameA); + verify(mockService, times(1)).getPersonalResourceGroupByShortName(shortNameB); + verify(mockService, times(1)).upsertPersonalResourceGroup(shortNameA, displayA); + verify(mockService, times(1)).upsertPersonalResourceGroup(shortNameB, displayB); + verifyNoMoreInteractions(mockService); + } + + @Test + void withResourceGroup_duplicateShortName_updatesStagedDisplayName() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + var shortName = "rg-dup"; + var oldName = "Old"; + var newName = "New"; + + when(mockService.getPersonalResourceGroupByShortName(shortName)).thenReturn(null); + when(mockService.upsertPersonalResourceGroup(shortName, newName)).thenReturn(mock(PersonalResourceGroupResponse.class)); + + var builder = new AccountsFactory.AccountBuilder(httpClient, sdkConfig, retry) { + @Override + protected AccountAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new AccountAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(shortName, oldName) + .withResourceGroup(shortName, newName) + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getPersonalResourceGroupByShortName(shortName); + verify(mockService, times(1)).upsertPersonalResourceGroup(shortName, newName); + verifyNoMoreInteractions(mockService); + } +} diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationAggregateTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationAggregateTest.java new file mode 100644 index 00000000..c94d4a99 --- /dev/null +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationAggregateTest.java @@ -0,0 +1,123 @@ +package com.yanchware.fractal.sdk.domain.account; + +import com.yanchware.fractal.sdk.domain.accounts.OrganizationAggregate; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.OrganizationalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class OrganizationAggregateTest { + private static final String SHORT_NAME = "test-rg"; + private static final String DISPLAY_NAME = "Test Resource Group"; + + @Test + void createsOrganizationalResourceGroup_when_NotExists() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + var orgId = UUID.randomUUID(); + aggregate.addOrganizationalResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME))).thenReturn(null); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(SHORT_NAME), eq(DISPLAY_NAME))).thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(SHORT_NAME), eq(DISPLAY_NAME)); + verifyNoMoreInteractions(mockService); + } + + @Test + void skipsUpsert_when_AlreadyExists() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + var orgId = UUID.randomUUID(); + aggregate.addOrganizationalResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME))).thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME)); + verify(mockService, never()).upsertOrganizationalResourceGroup(eq(orgId), eq(SHORT_NAME), anyString()); + verifyNoMoreInteractions(mockService); + } + + @Test + void updatesDisplayName_when_AddedTwiceWithDifferentDisplayName() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + var orgId = UUID.randomUUID(); + aggregate.addOrganizationalResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + aggregate.addOrganizationalResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME + " v2"); + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME))).thenReturn(null); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(SHORT_NAME), eq(DISPLAY_NAME + " v2"))).thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(SHORT_NAME), eq(DISPLAY_NAME + " v2")); + verifyNoMoreInteractions(mockService); + } + + @Test + void doesNothing_when_NoResourceGroupsStaged() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + aggregate.createOrUpdate(); + + verifyNoMoreInteractions(mockService); + } + + @Test + void createsMultipleResourceGroups_when_MultipleStaged() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + var orgId = UUID.randomUUID(); + var shortNameA = SHORT_NAME + "-a"; + var shortNameB = SHORT_NAME + "-b"; + var displayA = DISPLAY_NAME + " A"; + var displayB = DISPLAY_NAME + " B"; + + aggregate.addOrganizationalResourceGroup(orgId, shortNameA, displayA); + aggregate.addOrganizationalResourceGroup(orgId, shortNameB, displayB); + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameA))).thenReturn(null); + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameB))).thenReturn(null); + + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameA), eq(displayA))).thenReturn(mock(OrganizationalResourceGroupResponse.class)); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameB), eq(displayB))).thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameA)); + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameB)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameA), eq(displayA)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameB), eq(displayB)); + verifyNoMoreInteractions(mockService); + } + + @Test + void propagatesException_when_ServiceGetFails() throws InstantiatorException { + var mockService = mock(com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService.class); + var aggregate = new OrganizationAggregate(mockService); + + var orgId = UUID.randomUUID(); + aggregate.addOrganizationalResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(SHORT_NAME))).thenThrow(new InstantiatorException("error")); + + assertThatThrownBy(aggregate::createOrUpdate).isInstanceOf(InstantiatorException.class).hasMessageContaining("error"); + } +} diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationFactoryTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationFactoryTest.java new file mode 100644 index 00000000..1fb65aae --- /dev/null +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/OrganizationFactoryTest.java @@ -0,0 +1,182 @@ +package com.yanchware.fractal.sdk.domain.account; + +import com.yanchware.fractal.sdk.domain.accounts.OrganizationFactory; +import com.yanchware.fractal.sdk.configuration.SdkConfiguration; +import com.yanchware.fractal.sdk.domain.accounts.OrganizationAggregate; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import com.yanchware.fractal.sdk.domain.accounts.service.dtos.OrganizationalResourceGroupResponse; +import com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException; +import com.yanchware.fractal.sdk.utils.LocalSdkConfiguration; +import io.github.resilience4j.retry.RetryRegistry; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpClient; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + + +class OrganizationFactoryTest { + private static final String BASE_URL = "http://localhost:8080"; + private static final String SHORT_NAME = "rg-1"; + private static final String DISPLAY_NAME = "RG One"; + + @Test + void builder_buildsAggregate_and_stagesOrganizationalResourceGroup() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var orgId = UUID.randomUUID(); + + var factory = new OrganizationFactory(httpClient, sdkConfig, retry); + var aggregate = factory.builder() + .withResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME) + .build(); + + assertThat(aggregate).isNotNull(); + } + + @Test + void builder_isFluent_and_returnsSameInstance() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var orgId = UUID.randomUUID(); + + var builder = new OrganizationFactory(httpClient, sdkConfig, retry).builder(); + var returned = builder.withResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + + assertThat(returned).isSameAs(builder); + } + + @Test + void build_returnsSameAggregate_onRepeatedBuildCalls() { + var httpClient = HttpClient.newHttpClient(); + var sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var orgId = UUID.randomUUID(); + var builder = new OrganizationFactory(httpClient, sdkConfig, retry).builder().withResourceGroup(orgId, SHORT_NAME, DISPLAY_NAME); + + var aggregate1 = builder.build(); + var aggregate2 = builder.build(); + + assertThat(aggregate1).isSameAs(aggregate2); + } + + @Test + void withResourceGroup_stages_and_createOrUpdate_callsService_when_NotExists() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + var orgId = UUID.randomUUID(); + var shortName = SHORT_NAME; + var displayName = DISPLAY_NAME; + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortName))).thenReturn(null); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortName), eq(displayName))) + .thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + var builder = new OrganizationFactory.OrganizationBuilder(httpClient, sdkConfig, retry) { + @Override + protected OrganizationAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new OrganizationAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(orgId, shortName, displayName) + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortName)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortName), eq(displayName)); + verifyNoMoreInteractions(mockService); + } + + @Test + void withResourceGroup_chainMultiple_stagesAll_and_callsService() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + var orgId = UUID.randomUUID(); + var shortNameA = SHORT_NAME + "-a"; + var displayA = DISPLAY_NAME + " A"; + var shortNameB = SHORT_NAME + "-b"; + var displayB = DISPLAY_NAME + " B"; + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameA))).thenReturn(null); + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameB))).thenReturn(null); + + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameA), eq(displayA))) + .thenReturn(mock(OrganizationalResourceGroupResponse.class)); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameB), eq(displayB))) + .thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + var builder = new OrganizationFactory.OrganizationBuilder(httpClient, sdkConfig, retry) { + @Override + protected OrganizationAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new OrganizationAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(orgId, shortNameA, displayA) + .withResourceGroup(orgId, shortNameB, displayB) + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameA)); + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortNameB)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameA), eq(displayA)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortNameB), eq(displayB)); + verifyNoMoreInteractions(mockService); + } + + @Test + void withResourceGroup_duplicateIdentity_updatesStagedDisplayName() throws InstantiatorException { + var httpClient = HttpClient.newHttpClient(); + SdkConfiguration sdkConfig = new LocalSdkConfiguration(BASE_URL); + var retry = RetryRegistry.ofDefaults(); + + var mockService = mock(RestAccountsService.class); + + var orgId = UUID.randomUUID(); + var shortName = SHORT_NAME; + var newDisplayName = DISPLAY_NAME + " v2"; + + when(mockService.getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortName))).thenReturn(null); + when(mockService.upsertOrganizationalResourceGroup(eq(orgId), eq(shortName), eq(newDisplayName))) + .thenReturn(mock(OrganizationalResourceGroupResponse.class)); + + var builder = new OrganizationFactory.OrganizationBuilder(httpClient, sdkConfig, retry) { + @Override + protected OrganizationAggregate create(HttpClient client, SdkConfiguration cfg, RetryRegistry reg) { + return new OrganizationAggregate(mockService); + } + }; + + var aggregate = builder + .withResourceGroup(orgId, shortName, DISPLAY_NAME) + .withResourceGroup(orgId, shortName, newDisplayName) // overwrite staged display + .build(); + + aggregate.createOrUpdate(); + + verify(mockService, times(1)).getOrganizationalResourceGroupByShortName(eq(orgId), eq(shortName)); + verify(mockService, times(1)).upsertOrganizationalResourceGroup(eq(orgId), eq(shortName), eq(newDisplayName)); + verifyNoMoreInteractions(mockService); + } +} diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/RestAccountsServiceTest.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/RestAccountsServiceTest.java new file mode 100644 index 00000000..64a0a419 --- /dev/null +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/domain/account/RestAccountsServiceTest.java @@ -0,0 +1,276 @@ +package com.yanchware.fractal.sdk.domain.account; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.yanchware.fractal.sdk.domain.accounts.EntityStatus; +import com.yanchware.fractal.sdk.domain.accounts.service.RestAccountsService; +import com.yanchware.fractal.sdk.domain.values.ResourceGroupType; +import com.yanchware.fractal.sdk.utils.LocalSdkConfiguration; +import io.github.resilience4j.retry.RetryRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.http.HttpClient; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.assertThat; + +@WireMockTest +class RestAccountsServiceTest { + private RestAccountsService accountsService; + private static final String SHORT_NAME = "rg-personal"; + private static final String DISPLAY_NAME = "Personal Resource Group"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wm) { + var httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .build(); + + var sdkConfiguration = new LocalSdkConfiguration(wm.getHttpBaseUrl()); + accountsService = new RestAccountsService(httpClient, sdkConfiguration, RetryRegistry.ofDefaults()); + } + + @Test + void upsertPersonalResourceGroup_postsToCorrectUrl_andReturnsResponse() throws Exception { + // Given + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + var accountId = UUID.randomUUID().toString(); + stubFor(post(urlPattern) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.PERSONAL, accountId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + // When + var resp = accountsService.upsertPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME); + + // Then + verify(1, postRequestedFor(urlPattern)); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void upsertPersonalResourceGroup_sendsDisplayNameInBody() throws Exception { + // Given + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + var accountId = UUID.randomUUID().toString(); + stubFor(post(urlPattern) + .withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.PERSONAL, accountId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + // When + var resp = accountsService.upsertPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME); + + // Then + verify(postRequestedFor(urlPattern).withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME)))); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void upsertPersonalResourceGroup_throwsInstantiatorException_on500() { + // Given + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + + stubFor(post(urlPattern) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"message\":\"Internal Server Error\"}"))); + + // When / Then + org.assertj.core.api.Assertions.assertThatThrownBy(() -> + accountsService.upsertPersonalResourceGroup(SHORT_NAME, DISPLAY_NAME) + ).isInstanceOf(com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException.class); + } + + @Test + void getPersonalResourceGroupByShortName_returnsResponse_on200() throws Exception { + // Given + var accountId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.PERSONAL, accountId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + // When + var resp = accountsService.getPersonalResourceGroupByShortName(SHORT_NAME); + + // Then + verify(1, getRequestedFor(urlPattern)); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void getPersonalResourceGroupByShortName_throwsInstantiatorException_on500() { + // Given + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"message\":\"Internal Server Error\"}"))); + + // When / Then + org.assertj.core.api.Assertions.assertThatThrownBy(() -> + accountsService.getPersonalResourceGroupByShortName(SHORT_NAME) + ).isInstanceOf(com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException.class); + verify(3, getRequestedFor(urlPattern)); + } + + @Test + void getPersonalResourceGroupByShortName_returnsNull_on404() throws Exception { + // Given + var urlPattern = urlPathMatching("/accounts/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern).willReturn(aResponse().withStatus(404))); + + // When + var resp = accountsService.getPersonalResourceGroupByShortName(SHORT_NAME); + + // Then + verify(1, getRequestedFor(urlPattern)); + assertThat(resp).isNull(); + } + + @Test + void upsertOrganizationalResourceGroup_postsToCorrectUrl_andReturnsResponse() throws Exception { + // Given + var organizationId = UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(post(urlPattern) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.ORGANIZATIONAL, organizationId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + // When + var resp = accountsService.upsertOrganizationalResourceGroup(organizationId, SHORT_NAME, DISPLAY_NAME); + + // Then + verify(1, postRequestedFor(urlPattern)); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void upsertOrganizationalResourceGroup_sendsDisplayNameInBody() throws Exception { + // Given + var organizationId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(post(urlPattern) + .withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME))) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.ORGANIZATIONAL, organizationId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + + // When + var resp = accountsService.upsertOrganizationalResourceGroup(organizationId, SHORT_NAME, DISPLAY_NAME); + + // Then + verify(postRequestedFor(urlPattern).withRequestBody(matchingJsonPath("$.DisplayName", equalTo(DISPLAY_NAME)))); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void upsertOrganizationalResourceGroup_throwsInstantiatorException_on500() { + // Given + var organizationId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(post(urlPattern) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"message\":\"Internal Server Error\"}"))); + + // When / Then + org.assertj.core.api.Assertions.assertThatThrownBy(() -> + accountsService.upsertOrganizationalResourceGroup(organizationId, SHORT_NAME, DISPLAY_NAME) + ).isInstanceOf(com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException.class); + } + + @Test + void getOrganizationalResourceGroupByShortName_returnsResponse_on200() throws Exception { + // Given + var organizationId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(""" + {"Id":"%s/%s/%s","DisplayName":"%s","Status":"%s"} + """.formatted(ResourceGroupType.ORGANIZATIONAL, organizationId, SHORT_NAME, DISPLAY_NAME, EntityStatus.ACTIVE)))); + + // When + var resp = accountsService.getOrganizationalResourceGroupByShortName(organizationId, SHORT_NAME); + + // Then + verify(1, getRequestedFor(urlPattern)); + assertThat(resp).isNotNull(); + assertThat(resp.DisplayName()).isEqualTo(DISPLAY_NAME); + } + + @Test + void getOrganizationalResourceGroupByShortName_throwsInstantiatorException_on500() { + // Given + var organizationId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern) + .willReturn(aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"message\":\"Internal Server Error\"}"))); + + // When / Then + org.assertj.core.api.Assertions.assertThatThrownBy(() -> + accountsService.getOrganizationalResourceGroupByShortName(organizationId, SHORT_NAME) + ).isInstanceOf(com.yanchware.fractal.sdk.domain.exceptions.InstantiatorException.class); + + verify(3, getRequestedFor(urlPattern)); + } + + @Test + void getOrganizationalResourceGroupByShortName_returnsNull_on404() throws Exception { + // Given + var organizationId = java.util.UUID.randomUUID(); + var urlPattern = urlPathMatching("/accounts/organizations/" + organizationId + "/resourcegroups/" + SHORT_NAME); + + stubFor(get(urlPattern).willReturn(aResponse().withStatus(404))); + + // When + var resp = accountsService.getOrganizationalResourceGroupByShortName(organizationId, SHORT_NAME); + + // Then + verify(1, getRequestedFor(urlPattern)); + assertThat(resp).isNull(); + } +} diff --git a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/utils/LocalSdkConfiguration.java b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/utils/LocalSdkConfiguration.java index c4314f58..06babad5 100644 --- a/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/utils/LocalSdkConfiguration.java +++ b/fractal.sdk/src/test/java/com/yanchware/fractal/sdk/utils/LocalSdkConfiguration.java @@ -49,7 +49,11 @@ public URI getLiveSystemEndpoint() { @Override public URI getEnvironmentsEndpoint() { return new URI(httpBaseUrl + "/environments"); } + @SneakyThrows @Override + public URI getAccountsEndpoint() { return new URI(httpBaseUrl + "/accounts");} + + @Override public String getAwsAccessKeyId() { return "xxx"; }