diff --git a/src/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs b/src/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs deleted file mode 100644 index 2da5692..0000000 --- a/src/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Cf; - -public record AddSpaceRoleToUserRequest( - [property: JsonPropertyName("type")] string Type, - [property: JsonPropertyName("relationships")] SpaceRoleRelationship Relationship -); - -public record SpaceRoleRelationship( - [property: JsonPropertyName("user")] RelationshipUser User, - [property: JsonPropertyName("space")] SpaceRoleSpace Space -); - -public record RelationshipUser( - [property: JsonPropertyName("data")] UserData Data -); - -public record UserData( - [property: JsonPropertyName("username")] string Username, - [property: JsonPropertyName("origin")] string Origin -); - -public record SpaceRoleSpace( - [property: JsonPropertyName("data")] SpaceRoleData Data -); - -public record SpaceRoleData( - [property: JsonPropertyName("guid")] Guid Id -); diff --git a/src/Dim.Clients/Api/Cf/CfClient.cs b/src/Dim.Clients/Api/Cf/CfClient.cs deleted file mode 100644 index 5d9ba2c..0000000 --- a/src/Dim.Clients/Api/Cf/CfClient.cs +++ /dev/null @@ -1,142 +0,0 @@ -using Dim.Clients.Api.Cf.DependencyInjection; -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using Microsoft.Extensions.Options; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Dim.Clients.Api.Cf; - -public class CfClient(ITokenService tokenService, IOptions settings) : ICfClient -{ - private readonly CfSettings _settings = settings.Value; - - public async Task CreateCloudFoundrySpace(string bpnl, Guid cfEnvironmentId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var data = new CreateSpaceRequest( - $"{bpnl}-space", - new SpaceRelationship(new SpaceOrganization(new SpaceRelationshipData(cfEnvironmentId))) - ); - - var result = await client.PostAsJsonAsync("/v3/spaces", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-cfe", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("Response must not be null"); - } - - return response.Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var data = new AddSpaceRoleToUserRequest( - type, - new SpaceRoleRelationship( - new RelationshipUser(new UserData(user, "sap.ids")), - new SpaceRoleSpace(new SpaceRoleData(spaceId)) - ) - ); - - await client.PostAsJsonAsync("/v3/roles", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("add-space-roles", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - } - - public async Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var result = await client.GetAsync("/v3/service_plans", cancellationToken) - .CatchingIntoServiceExceptionFor("get-service-plan", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("response should never be null here"); - } - - var servicePlans = response.Resources.Where(x => x.Name == servicePlanType && - x.BrokerCatalog?.BrokerCatalogMetadata?.AutoSubscription?.AppName == servicePlanName); - if (response == null || servicePlans.Count() != 1) - { - throw new ServiceException($"There must be exactly one service plan with name {servicePlanName} and type {servicePlanType}"); - } - - return servicePlans.Single().Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task CreateDimServiceInstance(Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var data = new CreateDimServiceInstance( - "managed", - "dim-instance", //TODO: clarify - new DimRelationships( - new DimSpace(new DimData(spaceId)), - new DimServicePlan(new DimData(servicePlanId))) - ); - - await client.PostAsJsonAsync("/v3/service_instances", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-dim-si", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - } - - public async Task GetServiceInstances(CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var result = await client.GetAsync("/v3/service_instances", cancellationToken) - .CatchingIntoServiceExceptionFor("get-si", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - var resources = response.Resources.Where(x => x is { Name: "dim-instance", Type: "managed" }); - if (response == null || resources.Count() != 1) - { - throw new ServiceException($"There must be exactly one service instance"); - } - - return resources.Single().Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task CreateServiceInstanceBindings(CancellationToken cancellationToken) - { - var serviceInstanceId = await GetServiceInstances(cancellationToken).ConfigureAwait(false); - var client = await tokenService.GetAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); - var data = new CreateServiceCredentialBindingRequest( - "key", - "dim-key01", - new ServiceCredentialRelationships( - new DimServiceInstance(new DimData(serviceInstanceId))) - ); - await client.PostAsJsonAsync("/v3/service_credential_bindings", data, JsonSerializerOptions.Default, cancellationToken) - .CatchingIntoServiceExceptionFor("create-si-bindings", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Cf/CreateCfeRequest.cs b/src/Dim.Clients/Api/Cf/CreateCfeRequest.cs deleted file mode 100644 index 07ace68..0000000 --- a/src/Dim.Clients/Api/Cf/CreateCfeRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Cf; - -public record CreateSpaceRequest( - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("relationships")] SpaceRelationship Relationship -); - -public record SpaceRelationship( - [property: JsonPropertyName("organization")] SpaceOrganization Organization -); - -public record SpaceOrganization( - [property: JsonPropertyName("data")] SpaceRelationshipData Data -); - -public record SpaceRelationshipData( - [property: JsonPropertyName("guid")] Guid Id -); - -public record CreateSpaceResponse( - [property: JsonPropertyName("guid")] Guid Id -); diff --git a/src/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs b/src/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs deleted file mode 100644 index f17c283..0000000 --- a/src/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Dim.Clients.Api.Cf.DependencyInjection; - -public static class CfClientServiceExtensions -{ - public static IServiceCollection AddCfClient(this IServiceCollection services, IConfigurationSection section) - { - services.AddOptions() - .Bind(section) - .ValidateOnStart(); - - var sp = services.BuildServiceProvider(); - var settings = sp.GetRequiredService>(); - services - .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) - .AddTransient(); - - return services; - } -} diff --git a/src/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs b/src/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs deleted file mode 100644 index 83167ce..0000000 --- a/src/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Dim.Clients.Token; -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Api.Cf.DependencyInjection; - -public class CfSettings : AuthSettings -{ - [Required] - public string BaseUrl { get; set; } = null!; -} diff --git a/src/Dim.Clients/Api/Cf/ICfClient.cs b/src/Dim.Clients/Api/Cf/ICfClient.cs deleted file mode 100644 index cbd7c10..0000000 --- a/src/Dim.Clients/Api/Cf/ICfClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Dim.Clients.Api.Cf; - -public interface ICfClient -{ - Task CreateCloudFoundrySpace(string bpnl, Guid cfEnvironmentId, CancellationToken cancellationToken); - Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken); - Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken); - Task CreateDimServiceInstance(Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken); - Task CreateServiceInstanceBindings(CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Cf/ServicePlanResponse.cs b/src/Dim.Clients/Api/Cf/ServicePlanResponse.cs deleted file mode 100644 index fef7960..0000000 --- a/src/Dim.Clients/Api/Cf/ServicePlanResponse.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Cf; - -public record ServicePlanResponse( - [property: JsonPropertyName("resources")] IEnumerable Resources -); - -public record ServicePlanResources( - [property: JsonPropertyName("guid")] Guid Id, - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("broker_catalog")] BrokerCatalog? BrokerCatalog -); - -public record BrokerCatalog( - [property: JsonPropertyName("id")] string Id, - [property: JsonPropertyName("metadata")] BrokerCatalogMetadata? BrokerCatalogMetadata -); - -public record BrokerCatalogMetadata( - [property: JsonPropertyName("auto_subscription")] AutoSupscription? AutoSubscription -); - -public record AutoSupscription( - [property: JsonPropertyName("app_name")] string? AppName -); - -public record ServiceInstanceResponse( - [property: JsonPropertyName("resources")] IEnumerable Resources -); - -public record ServiceInstanceResource( - [property: JsonPropertyName("guid")] Guid Id, - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("type")] string Type -); - -public record CreateServiceCredentialBindingRequest( - [property: JsonPropertyName("type")] string Type, - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("relationships")] ServiceCredentialRelationships Relationships -); - -public record ServiceCredentialRelationships( - [property: JsonPropertyName("service_instance")] DimServiceInstance ServiceInstance -); - -public record DimServiceInstance( - [property: JsonPropertyName("data")] DimData Data -); - -public record CreateDimServiceInstance( - [property: JsonPropertyName("type")] string Type, - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("relationships")] DimRelationships Relationships -); - -public record DimRelationships( - [property: JsonPropertyName("space")] DimSpace Space, - [property: JsonPropertyName("service_plan")] DimServicePlan ServicePlan -); - -public record DimServicePlan( - [property: JsonPropertyName("data")] DimData Data -); - -public record DimSpace( - [property: JsonPropertyName("data")] DimData Data -); - -public record DimData( - [property: JsonPropertyName("guid")] Guid Id -); - -public record DimServiceInstanceRequest( - [property: JsonPropertyName("guid")] Guid Id -); \ No newline at end of file diff --git a/src/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs b/src/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs deleted file mode 100644 index 4cdd659..0000000 --- a/src/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Dim; - -public record CreateCompanyIdentityRequest( - [property: JsonPropertyName("payload")] Payload Payload -); - -public record Payload( - [property: JsonPropertyName("network")] Network Network, - [property: JsonPropertyName("hostingUrl")] string HostingUrl, - [property: JsonPropertyName("services")] IEnumerable Services, - [property: JsonPropertyName("keys")] IEnumerable Keys, - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("name")] string Name -); - -public record Key( - [property: JsonPropertyName("type")] string Type -); - -public record Network( - [property: JsonPropertyName("didMethod")] string DidMethod, - [property: JsonPropertyName("type")] string Type -); - -public record CreateCompanyIdentityResponse( - [property: JsonPropertyName("did")] string Did, - [property: JsonPropertyName("companyId")] Guid CompanyId, - [property: JsonPropertyName("downloadURL")] string DownloadUrl -); diff --git a/src/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs b/src/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs deleted file mode 100644 index 5e7f919..0000000 --- a/src/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Api.Dim.DependencyInjection; - -public static class DimClientServiceExtensions -{ - public static IServiceCollection AddDimClient(this IServiceCollection services) - { - services - .AddCustomHttpClientWithAuthentication(null, null) - .AddTransient(); - - return services; - } -} diff --git a/src/Dim.Clients/Api/Dim/DimClient.cs b/src/Dim.Clients/Api/Dim/DimClient.cs deleted file mode 100644 index 04bbd7d..0000000 --- a/src/Dim.Clients/Api/Dim/DimClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Dim.Clients.Api.Dim; - -public class DimClient(ITokenService tokenService, IHttpClientFactory clientFactory) : IDimClient -{ - public async Task CreateCompanyIdentity(AuthSettings dimAuth, object baseUrl, string bpnl, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); - var data = new CreateCompanyIdentityRequest(new Payload( - new Network("web", "test"), - "https://example.org/.well-known/did.json", - Enumerable.Empty(), - new Key[] - { - new("SIGNING"), - new("SIGNING_VC"), - }, - "bpnl", - bpnl)); - var result = await client.PostAsJsonAsync($"{baseUrl}/api/v2.0.0/companyIdentities", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-company-identity", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - if (response == null) - { - throw new ServiceException("Response was empty", true); - } - - return response; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task GetDidDocument(string url, CancellationToken cancellationToken) - { - var client = clientFactory.CreateClient("didDocumentDownload"); - using var result = await client.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); - var document = await JsonDocument.ParseAsync(result, cancellationToken: cancellationToken).ConfigureAwait(false); - return document; - } -} diff --git a/src/Dim.Clients/Api/Dim/IDimClient.cs b/src/Dim.Clients/Api/Dim/IDimClient.cs deleted file mode 100644 index b8c9ae5..0000000 --- a/src/Dim.Clients/Api/Dim/IDimClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Dim.Clients.Token; -using System.Text.Json; - -namespace Dim.Clients.Api.Dim; - -public interface IDimClient -{ - Task CreateCompanyIdentity(AuthSettings dimAuth, object baseUrl, string bpnl, CancellationToken cancellationToken); - Task GetDidDocument(string url, CancellationToken cancellationToken); -} diff --git a/src/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs b/src/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs deleted file mode 100644 index 49eb38d..0000000 --- a/src/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Dim.Clients.Api.Directories.DependencyInjection; - -public static class DirectoryClientServiceExtensions -{ - public static IServiceCollection AddDirectoryClient(this IServiceCollection services, IConfigurationSection section) - { - services.AddOptions() - .Bind(section) - .ValidateOnStart(); - - var sp = services.BuildServiceProvider(); - var settings = sp.GetRequiredService>(); - services - .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs b/src/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs deleted file mode 100644 index 450a894..0000000 --- a/src/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Dim.Clients.Token; -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Api.Directories.DependencyInjection; - -public class DirectorySettings : AuthSettings -{ - [Required] - public string BaseUrl { get; set; } = null!; -} diff --git a/src/Dim.Clients/Api/Directories/DirectoryClient.cs b/src/Dim.Clients/Api/Directories/DirectoryClient.cs deleted file mode 100644 index e6b1e56..0000000 --- a/src/Dim.Clients/Api/Directories/DirectoryClient.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Dim.Clients.Api.Directories.DependencyInjection; -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using Microsoft.Extensions.Options; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Dim.Clients.Api.Directories; - -public class DirectoryClient(ITokenService tokenService, IOptions settings) - : IDirectoryClient -{ - private readonly DirectorySettings _settings = settings.Value; - - public async Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(_settings, cancellationToken).ConfigureAwait(false); - var directory = new DirectoryRequest( - description, - Enumerable.Repeat("phil.schneider@digitalnativesolutions.de", 1), - bpnl, - new Dictionary>() - { - { "cloud_management_service", new[] { "Created by API - Don't change it" } } - } - ); - - var result = await client.PostAsJsonAsync($"/accounts/v1/directories?parentId={parentId}", directory, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-directory", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("Directory response must not be null"); - } - - return response.Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } -} diff --git a/src/Dim.Clients/Api/Directories/DirectoryRequest.cs b/src/Dim.Clients/Api/Directories/DirectoryRequest.cs deleted file mode 100644 index 8eab016..0000000 --- a/src/Dim.Clients/Api/Directories/DirectoryRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Directories; - -public record DirectoryRequest( - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("directoryAdmins")] IEnumerable DirectoryAdmins, - [property: JsonPropertyName("displayName")] string DisplayName, - [property: JsonPropertyName("labels")] Dictionary> Labels -); - -public record DirectoryResponse( - [property: JsonPropertyName("guid")] Guid Id, - [property: JsonPropertyName("subdomain")] string Subdomain -); diff --git a/src/Dim.Clients/Api/Directories/IDirectoryClient.cs b/src/Dim.Clients/Api/Directories/IDirectoryClient.cs deleted file mode 100644 index 592e2a2..0000000 --- a/src/Dim.Clients/Api/Directories/IDirectoryClient.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Dim.Clients.Api.Directories; - -public interface IDirectoryClient -{ - Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken); -} diff --git a/src/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs b/src/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs deleted file mode 100644 index 94e8422..0000000 --- a/src/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Entitlements; - -public record CreateSubAccountRequest( - [property: JsonPropertyName("subaccountServicePlans")] IEnumerable SubaccountServicePlans -); - -public record SubaccountServicePlan( - [property: JsonPropertyName("assignmentInfo")] IEnumerable AssignmentInfo, - [property: JsonPropertyName("serviceName")] string ServiceName, - [property: JsonPropertyName("servicePlanName")] string ServicePlanName -); - -public record AssignmentInfo( - [property: JsonPropertyName("enable")] bool? Enabled, - [property: JsonPropertyName("amount")] int? Amount, - [property: JsonPropertyName("subaccountGUID")] Guid SubaccountId -); diff --git a/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs b/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs deleted file mode 100644 index 436caa7..0000000 --- a/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Dim.Clients.Api.Entitlements.DependencyInjection; - -public static class EntitlementClientServiceExtensions -{ - public static IServiceCollection AddEntitlementClient(this IServiceCollection services, IConfigurationSection section) - { - services.AddOptions() - .Bind(section) - .ValidateOnStart(); - - var sp = services.BuildServiceProvider(); - var settings = sp.GetRequiredService>(); - services - .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs b/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs deleted file mode 100644 index f3d2f29..0000000 --- a/src/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Api.Entitlements.DependencyInjection; - -public class EntitlementSettings -{ - [Required] - public string BaseUrl { get; set; } = null!; -} diff --git a/src/Dim.Clients/Api/Entitlements/EntitlementClient.cs b/src/Dim.Clients/Api/Entitlements/EntitlementClient.cs deleted file mode 100644 index c2f76bb..0000000 --- a/src/Dim.Clients/Api/Entitlements/EntitlementClient.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using System.Net.Http.Json; - -namespace Dim.Clients.Api.Entitlements; - -public class EntitlementClient(ITokenService tokenService) : IEntitlementClient -{ - public async Task AssignEntitlements(AuthSettings authSettings, Guid subAccountId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var data = new CreateSubAccountRequest( - new List - { - new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "cis", "local"), - new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "decentralized-identity-management-app", "standard"), - new(Enumerable.Repeat(new AssignmentInfo(null, 1, subAccountId), 1), "decentralized-identity-management", "standard"), - new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "auditlog-viewer", "free") - } - ); - - await client.PutAsJsonAsync("/entitlements/v1/subaccountServicePlans", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("assign-entitlements", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Entitlements/ISubAccountClient.cs b/src/Dim.Clients/Api/Entitlements/ISubAccountClient.cs deleted file mode 100644 index 1a10057..0000000 --- a/src/Dim.Clients/Api/Entitlements/ISubAccountClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Dim.Clients.Token; - -namespace Dim.Clients.Api.Entitlements; - -public interface IEntitlementClient -{ - Task AssignEntitlements(AuthSettings authSettings, Guid subAccountId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs b/src/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs deleted file mode 100644 index a8d4c40..0000000 --- a/src/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Api.Provisioning; - -public record CreateCfeRequest( - [property: JsonPropertyName("environmentType")] string EnvironmentType, - [property: JsonPropertyName("parameters")] Dictionary Parameters, - [property: JsonPropertyName("landscapeLabel")] string LandscapeLabel, - [property: JsonPropertyName("planName")] string PlanName, - [property: JsonPropertyName("serviceName")] string ServiceName, - [property: JsonPropertyName("user")] string User -); - -public record CreateCfeResponse( - [property: JsonPropertyName("id")] Guid Id -); \ No newline at end of file diff --git a/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs b/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs deleted file mode 100644 index 73fdab9..0000000 --- a/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Api.Provisioning.DependencyInjection; - -public static class ProvisioningClientServiceExtensions -{ - public static IServiceCollection AddProvisioningClient(this IServiceCollection services) - { - services - .AddCustomHttpClientWithAuthentication(null, null) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs b/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs deleted file mode 100644 index ede1fb2..0000000 --- a/src/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Dim.Clients.Token; -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Api.Provisioning.DependencyInjection; - -public class ProvisioningSettings : AuthSettings -{ - [Required] - public string BaseUrl { get; set; } = null!; -} diff --git a/src/Dim.Clients/Api/Provisioning/IProvisioningClient.cs b/src/Dim.Clients/Api/Provisioning/IProvisioningClient.cs deleted file mode 100644 index 8df1631..0000000 --- a/src/Dim.Clients/Api/Provisioning/IProvisioningClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Dim.Clients.Api.Services; - -namespace Dim.Clients.Api.Provisioning; - -public interface IProvisioningClient -{ - Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string bpnl, string user, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Provisioning/ProvisioningClient.cs b/src/Dim.Clients/Api/Provisioning/ProvisioningClient.cs deleted file mode 100644 index f2be08c..0000000 --- a/src/Dim.Clients/Api/Provisioning/ProvisioningClient.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Dim.Clients.Api.Services; -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Dim.Clients.Api.Provisioning; - -public class ProvisioningClient(ITokenService tokenService) : IProvisioningClient -{ - public async Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string bpnl, string user, CancellationToken cancellationToken) - { - var authSettings = new AuthSettings - { - TokenAddress = $"{authUrl}/oauth/token", - ClientId = bindingData.Credentials.Uaa.ClientId, - ClientSecret = bindingData.Credentials.Uaa.ClientSecret - }; - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var data = new CreateCfeRequest( - "cloudfoundry", - new Dictionary - { - { "instance_name", bpnl } - }, - "cf-eu10", - "standard", - "cloudfoundry", - user - ); - - var result = await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.ProvisioningServiceUrl}/provisioning/v1/environments", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-cf-env", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - if (response == null) - { - throw new ServiceException("Response was empty", true); - } - - return response.Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } -} diff --git a/src/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs b/src/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs deleted file mode 100644 index f1394b0..0000000 --- a/src/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Api.Services.DependencyInjection; - -public static class ServiceClientServiceExtensions -{ - public static IServiceCollection AddServiceClient(this IServiceCollection services) - { - services - .AddCustomHttpClientWithAuthentication(null, null) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Services/IServiceClient.cs b/src/Dim.Clients/Api/Services/IServiceClient.cs deleted file mode 100644 index 15ecb3c..0000000 --- a/src/Dim.Clients/Api/Services/IServiceClient.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Dim.Clients.Api.SubAccounts; - -namespace Dim.Clients.Api.Services; - -public interface IServiceClient -{ - Task CreateServiceInstance(ServiceManagementBindingResponse saBinding, CancellationToken cancellationToken); - Task CreateServiceBinding(ServiceManagementBindingResponse saBinding, string serviceInstanceId, CancellationToken cancellationToken); - Task GetServiceBinding(ServiceManagementBindingResponse saBinding, string serviceBindingName, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs b/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs deleted file mode 100644 index aaf3bce..0000000 --- a/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -namespace Dim.Clients.Api.SubAccounts.DependencyInjection; - -public static class SubAccountClientServiceExtensions -{ - public static IServiceCollection AddSubAccountClient(this IServiceCollection services, IConfigurationSection section) - { - services.AddOptions() - .Bind(section) - .ValidateOnStart(); - - var sp = services.BuildServiceProvider(); - var settings = sp.GetRequiredService>(); - services - .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs b/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs deleted file mode 100644 index 407283f..0000000 --- a/src/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Api.SubAccounts.DependencyInjection; - -public class SubAccountSettings -{ - [Required] - public string BaseUrl { get; set; } = null!; -} diff --git a/src/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs b/src/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs deleted file mode 100644 index d856893..0000000 --- a/src/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Dim.Clients.Token; - -namespace Dim.Clients.Api.SubAccounts; - -public interface ISubAccountClient -{ - Task CreateSubaccount(AuthSettings authSettings, string adminMail, string bpnl, string companyName, Guid directoryId, CancellationToken cancellationToken); - Task CreateServiceManagerBindings(AuthSettings authSettings, Guid subAccountId, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/SubAccounts/SubAccountClient.cs b/src/Dim.Clients/Api/SubAccounts/SubAccountClient.cs deleted file mode 100644 index 2830a50..0000000 --- a/src/Dim.Clients/Api/SubAccounts/SubAccountClient.cs +++ /dev/null @@ -1,110 +0,0 @@ -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using System.Net.Http.Json; -using System.Text.Json; - -namespace Dim.Clients.Api.SubAccounts; - -public class SubAccountClient(ITokenService tokenService) - : ISubAccountClient -{ - public async Task CreateSubaccount(AuthSettings authSettings, string adminMail, string bpnl, string companyName, Guid directoryId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var directory = new CreateSubAccountRequest( - false, - $"CX customer sub-account {companyName}", - $"{bpnl}_{companyName}", - new Dictionary> - { - { "cloud_management_service", new[] { "Created by API - Don't change it" } }, - { "bpnl", new[] { bpnl } }, - { "companyName", new[] { companyName } } - }, - "API", - directoryId, - "eu10", - Enumerable.Repeat(adminMail, 1), - "test01", - UsedForProduction.USED_FOR_PRODUCTION - ); - - var result = await client.PostAsJsonAsync("/accounts/v1/subaccounts", directory, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("Response must not be null"); - } - - return response.Id; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task CreateServiceManagerBindings(AuthSettings authSettings, Guid subAccountId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var data = new - { - name = "accessServiceManager" - }; - - var result = await client.PostAsJsonAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("Response must not be null"); - } - - return response; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } - - public async Task AssignEntitlements(AuthSettings authSettings, Guid subAccountId, CancellationToken cancellationToken) - { - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var data = new - { - name = "accessServiceManager" - }; - - var result = await client.PostAsJsonAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - try - { - var response = await result.Content - .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) - .ConfigureAwait(false); - - if (response == null) - { - throw new ServiceException("Response must not be null"); - } - - return response; - } - catch (JsonException je) - { - throw new ServiceException(je.Message); - } - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/SubAccounts/UsedForProduction.cs b/src/Dim.Clients/Api/SubAccounts/UsedForProduction.cs deleted file mode 100644 index 833fa68..0000000 --- a/src/Dim.Clients/Api/SubAccounts/UsedForProduction.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dim.Clients.Api.SubAccounts; - -public enum UsedForProduction -{ - USED_FOR_PRODUCTION = 1, - NOT_USED_FOR_PRODUCTION = 2, - UNSET = 3 -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs b/src/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs deleted file mode 100644 index e93d3b6..0000000 --- a/src/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Dim.Clients.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Api.Subscriptions.DependencyInjection; - -public static class SubscriptionClientServiceExtensions -{ - public static IServiceCollection AddSubscriptionClient(this IServiceCollection services) - { - services - .AddCustomHttpClientWithAuthentication(null, null) - .AddTransient(); - - return services; - } -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs b/src/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs deleted file mode 100644 index bd3578f..0000000 --- a/src/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Dim.Clients.Api.Services; - -namespace Dim.Clients.Api.Subscriptions; - -public interface ISubscriptionClient -{ - Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs b/src/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs deleted file mode 100644 index de52d45..0000000 --- a/src/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Dim.Clients.Api.Services; -using Dim.Clients.Extensions; -using Dim.Clients.Token; -using System.Net.Http.Json; - -namespace Dim.Clients.Api.Subscriptions; - -public class SubscriptionClient(ITokenService tokenService) : ISubscriptionClient -{ - public async Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken) - { - var authSettings = new AuthSettings - { - TokenAddress = $"{authUrl}/oauth/token", - ClientId = bindingData.Credentials.Uaa.ClientId, - ClientSecret = bindingData.Credentials.Uaa.ClientSecret - }; - var client = await tokenService.GetAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); - var data = new - { - planName = planName - }; - - await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.SaasRegistryServiceUrl}/saas-manager/v1/applications/{applicationName}/subscription", data, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("subscribe-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); - } -} diff --git a/src/Dim.Clients/Dim.Clients.csproj b/src/Dim.Clients/Dim.Clients.csproj deleted file mode 100644 index 32e0be1..0000000 --- a/src/Dim.Clients/Dim.Clients.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net8.0 - enable - enable - Dim.Clients - - - - - - - - - diff --git a/src/Dim.Clients/Extensions/HttpAsyncResponseMessageExtension.cs b/src/Dim.Clients/Extensions/HttpAsyncResponseMessageExtension.cs deleted file mode 100644 index cdbed19..0000000 --- a/src/Dim.Clients/Extensions/HttpAsyncResponseMessageExtension.cs +++ /dev/null @@ -1,67 +0,0 @@ -namespace Dim.Clients.Extensions; - -public static class HttpAsyncResponseMessageExtension -{ - public static async ValueTask CatchingIntoServiceExceptionFor(this Task response, string systemName, RecoverOptions recoverOptions = RecoverOptions.None, Func>? handleErrorResponse = null) - { - try - { - var message = await response.ConfigureAwait(false); - var requestUri = message.RequestMessage?.RequestUri?.AbsoluteUri; - if (message.IsSuccessStatusCode) - { - return message; - } - - string? errorMessage; - try - { - (var ignore, errorMessage) = (int)message.StatusCode switch - { - < 500 when handleErrorResponse != null => await handleErrorResponse(message).ConfigureAwait(false), - >= 500 => (false, await message.Content.ReadAsStringAsync().ConfigureAwait(false)), - _ => (false, null) - }; - if (ignore) - return message; - } - catch (Exception) - { - errorMessage = null; - } - - throw new ServiceException( - string.IsNullOrWhiteSpace(errorMessage) - ? $"call to external system {requestUri ?? systemName} failed with statuscode {(int)message.StatusCode}" - : $"call to external system {requestUri ?? systemName} failed with statuscode {(int)message.StatusCode} - Message: {errorMessage}", - message.StatusCode, - (recoverOptions & RecoverOptions.RESPONSE_RECEIVED) == RecoverOptions.RESPONSE_RECEIVED); - } - catch (HttpRequestException e) - { - throw e.StatusCode == null - ? new ServiceException($"call to external system {systemName} failed", e, (recoverOptions & RecoverOptions.REQUEST_EXCEPTION) == RecoverOptions.REQUEST_EXCEPTION) - : new ServiceException($"call to external system {systemName} failed with statuscode {(int)e.StatusCode.Value}", e, e.StatusCode.Value, (recoverOptions & RecoverOptions.REQUEST_EXCEPTION) == RecoverOptions.REQUEST_EXCEPTION); - } - catch (TaskCanceledException e) - { - throw new ServiceException($"call to external system {systemName} failed due to timeout", e, (recoverOptions & RecoverOptions.TIMEOUT) == RecoverOptions.TIMEOUT); - } - catch (Exception e) when (e is not ServiceException and not SystemException) - { - throw new ServiceException($"call to external system {systemName} failed", e, (recoverOptions & RecoverOptions.OTHER_EXCEPTION) == RecoverOptions.OTHER_EXCEPTION); - } - } - - [Flags] - public enum RecoverOptions - { - None = 0b_0000_0000, - RESPONSE_RECEIVED = 0b_0000_0001, - REQUEST_EXCEPTION = 0b_0000_0010, - TIMEOUT = 0b_0000_0100, - OTHER_EXCEPTION = 0b_0000_1000, - INFRASTRUCTURE = REQUEST_EXCEPTION | TIMEOUT, - ALLWAYS = RESPONSE_RECEIVED | REQUEST_EXCEPTION | TIMEOUT | OTHER_EXCEPTION - } -} diff --git a/src/Dim.Clients/Extensions/HttpClientExtensions.cs b/src/Dim.Clients/Extensions/HttpClientExtensions.cs deleted file mode 100644 index ae85e5b..0000000 --- a/src/Dim.Clients/Extensions/HttpClientExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Extensions; - -public static class HttpClientExtensions -{ - public static IServiceCollection AddCustomHttpClientWithAuthentication(this IServiceCollection services, string? baseAddress, string? authAddress) where T : class - { - services.AddHttpClient(typeof(T).Name, c => - { - if (baseAddress != null) - { - c.BaseAddress = new Uri(baseAddress); - } - }); - - services.AddHttpClient($"{typeof(T).Name}Auth", c => - { - if (authAddress != null) - { - c.BaseAddress = new Uri(authAddress); - } - }); - return services; - } -} diff --git a/src/Dim.Clients/Extensions/JsonSerializerExtensions.cs b/src/Dim.Clients/Extensions/JsonSerializerExtensions.cs deleted file mode 100644 index 70492cf..0000000 --- a/src/Dim.Clients/Extensions/JsonSerializerExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Dim.Clients.Extensions; - -public static class JsonSerializerExtensions -{ - public static readonly JsonSerializerOptions Options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - Converters = - { - new JsonStringEnumConverter(allowIntegerValues: false) - }, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; -} \ No newline at end of file diff --git a/src/Dim.Clients/Extensions/ServiceException.cs b/src/Dim.Clients/Extensions/ServiceException.cs deleted file mode 100644 index 42bc42c..0000000 --- a/src/Dim.Clients/Extensions/ServiceException.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Net; - -namespace Dim.Clients.Extensions; - -[Serializable] -public class ServiceException : Exception -{ - public HttpStatusCode? StatusCode { get; } - public bool IsRecoverable { get; } - - public ServiceException() : base() { } - - public ServiceException(string message, bool isRecoverable = false) : base(message) - { - StatusCode = null; - IsRecoverable = isRecoverable; - } - - public ServiceException(string message, HttpStatusCode httpStatusCode, bool isRecoverable = false) : base(message) - { - StatusCode = httpStatusCode; - IsRecoverable = isRecoverable; - } - - public ServiceException(string message, Exception inner, bool isRecoverable = false) : base(message, inner) - { - StatusCode = null; - IsRecoverable = isRecoverable; - } - - public ServiceException(string message, Exception inner, HttpStatusCode httpStatusCode, bool isRecoverable = false) : base(message, inner) - { - StatusCode = httpStatusCode; - IsRecoverable = isRecoverable; - } - - protected ServiceException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } -} diff --git a/src/Dim.Clients/Services/ISetupDimService.cs b/src/Dim.Clients/Services/ISetupDimService.cs deleted file mode 100644 index 42496b7..0000000 --- a/src/Dim.Clients/Services/ISetupDimService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Text.Json; - -namespace Dim.Clients.Services; - -public interface ISetupDimService -{ - Task SaAndInstanceSetup(string bpnl, string companyName, CancellationToken cancellationToken); -} \ No newline at end of file diff --git a/src/Dim.Clients/Services/SetupDimService.cs b/src/Dim.Clients/Services/SetupDimService.cs deleted file mode 100644 index e4aa234..0000000 --- a/src/Dim.Clients/Services/SetupDimService.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Dim.Clients.Api.Cf; -using Dim.Clients.Api.Dim; -using Dim.Clients.Api.Entitlements; -using Dim.Clients.Api.Provisioning; -using Dim.Clients.Api.Services; -using Dim.Clients.Api.SubAccounts; -using Dim.Clients.Api.Subscriptions; -using Dim.Clients.Token; -using Microsoft.Extensions.Options; -using System.Text.Json; - -namespace Dim.Clients.Services; - -public class SetupDimService : ISetupDimService -{ - private readonly ISubAccountClient _subAccountClient; - private readonly IServiceClient _serviceClient; - private readonly IEntitlementClient _entitlementClient; - private readonly ISubscriptionClient _subscriptionClient; - private readonly IProvisioningClient _provisioningClient; - private readonly ICfClient _cfClient; - private readonly IDimClient _dimClient; - private readonly SetupDimSettings _settings; - - public SetupDimService( - ISubAccountClient subAccountClient, - IServiceClient serviceClient, - ISubscriptionClient subscriptionClient, - IEntitlementClient entitlementClient, - IProvisioningClient provisioningClient, - ICfClient cfClient, - IDimClient dimClient, - IOptions options) - { - _subAccountClient = subAccountClient; - _serviceClient = serviceClient; - _entitlementClient = entitlementClient; - _subscriptionClient = subscriptionClient; - _provisioningClient = provisioningClient; - _cfClient = cfClient; - _dimClient = dimClient; - _settings = options.Value; - } - - public async Task SaAndInstanceSetup(string bpnl, string companyName, CancellationToken cancellationToken) - { - var parentDirectoryId = _settings.RootDirectoryId; - var adminMail = _settings.AdminMail; - var subAccountAuth = new AuthSettings - { - TokenAddress = $"{_settings.AuthUrl}/oauth/token", - ClientId = _settings.ClientidCisCentral, - ClientSecret = _settings.ClientsecretCisCentral - }; - var subAccountId = await _subAccountClient.CreateSubaccount(subAccountAuth, adminMail, bpnl, companyName, parentDirectoryId, cancellationToken).ConfigureAwait(false); - Thread.Sleep(30000); - var saBinding = await _subAccountClient.CreateServiceManagerBindings(subAccountAuth, subAccountId, cancellationToken).ConfigureAwait(false); - await _entitlementClient.AssignEntitlements(subAccountAuth, subAccountId, cancellationToken).ConfigureAwait(false); - var serviceInstance = await _serviceClient.CreateServiceInstance(saBinding, cancellationToken).ConfigureAwait(false); - var serviceBinding = await _serviceClient.CreateServiceBinding(saBinding, serviceInstance.Id, cancellationToken).ConfigureAwait(false); - var bindingResponse = await _serviceClient.GetServiceBinding(saBinding, serviceBinding.Name, cancellationToken).ConfigureAwait(false); - - await _subscriptionClient.SubscribeApplication(saBinding.Url, bindingResponse, "decentralized-identity-management-app", "standard", cancellationToken).ConfigureAwait(false); - - var cfEnvironmentId = await _provisioningClient.CreateCloudFoundryEnvironment(saBinding.Url, bindingResponse, bpnl, adminMail, cancellationToken) - .ConfigureAwait(false); - var spaceId = await _cfClient.CreateCloudFoundrySpace(bpnl, cfEnvironmentId, cancellationToken).ConfigureAwait(false); - await _cfClient.AddSpaceRoleToUser("space_manager", adminMail, spaceId, cancellationToken).ConfigureAwait(false); - await _cfClient.AddSpaceRoleToUser("space_developer", adminMail, spaceId, cancellationToken).ConfigureAwait(false); - - var servicePlanId = await _cfClient.GetServicePlan("decentralized-identity-management", "standard", cancellationToken).ConfigureAwait(false); - await _cfClient.CreateDimServiceInstance(spaceId, servicePlanId, cancellationToken).ConfigureAwait(false); - await _cfClient.CreateServiceInstanceBindings(cancellationToken).ConfigureAwait(false); - - // TODO: get the data of the dim service instance - var dimAuth = new AuthSettings - { - TokenAddress = "", - ClientId = "", - ClientSecret = "" - }; - var dimBaseUrl = ""; - var result = await _dimClient.CreateCompanyIdentity(dimAuth, dimBaseUrl, bpnl, cancellationToken).ConfigureAwait(false); - var didDocument = await _dimClient.GetDidDocument(result.DownloadUrl, cancellationToken).ConfigureAwait(false); - return didDocument; - } -} diff --git a/src/Dim.Clients/Services/SetupDimServiceExtensions.cs b/src/Dim.Clients/Services/SetupDimServiceExtensions.cs deleted file mode 100644 index a141c14..0000000 --- a/src/Dim.Clients/Services/SetupDimServiceExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Dim.Clients.Api.Cf.DependencyInjection; -using Dim.Clients.Api.Dim.DependencyInjection; -using Dim.Clients.Api.Entitlements.DependencyInjection; -using Dim.Clients.Api.Provisioning.DependencyInjection; -using Dim.Clients.Api.Services.DependencyInjection; -using Dim.Clients.Api.SubAccounts.DependencyInjection; -using Dim.Clients.Api.Subscriptions.DependencyInjection; -using Dim.Clients.Token; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Dim.Clients.Services; - -public static class SetupDimServiceExtensions -{ - public static IServiceCollection AddSetupDim(this IServiceCollection services, IConfiguration config) - { - services.AddOptions() - .Bind(config.GetSection("Dim")) - .ValidateOnStart(); - - services - .AddTransient() - .AddTransient() - .AddSubAccountClient(config.GetSection("SubAccount")) - .AddEntitlementClient(config.GetSection("Entitlement")) - .AddServiceClient() - .AddSubscriptionClient() - .AddProvisioningClient() - .AddCfClient(config.GetSection("Cf")) - .AddDimClient(); - - return services; - } -} diff --git a/src/Dim.Clients/Services/SetupDimSettings.cs b/src/Dim.Clients/Services/SetupDimSettings.cs deleted file mode 100644 index 47cbe0f..0000000 --- a/src/Dim.Clients/Services/SetupDimSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Services; - -public class SetupDimSettings -{ - [Required(AllowEmptyStrings = false)] - public string AdminMail { get; set; } = null!; - - [Required] - public Guid RootDirectoryId { get; set; } - - [Required(AllowEmptyStrings = false)] - public string AuthUrl { get; set; } = null!; - - [Required(AllowEmptyStrings = false)] - public string ClientidCisCentral { get; set; } = null!; - - [Required(AllowEmptyStrings = false)] - public string ClientsecretCisCentral { get; set; } = null!; -} \ No newline at end of file diff --git a/src/Dim.Clients/Token/AuthResponse.cs b/src/Dim.Clients/Token/AuthResponse.cs deleted file mode 100644 index ad1159e..0000000 --- a/src/Dim.Clients/Token/AuthResponse.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Dim.Clients.Token; - -public record AuthResponse( - [property: JsonPropertyName("access_token")] string? AccessToken, - [property: JsonPropertyName("expires_in")] int ExpiresIn, - [property: JsonPropertyName("token_type")] string? TokenType, - [property: JsonPropertyName("jti")] string? Jti, - [property: JsonPropertyName("scope")] string? Scope -); - -public record LegacyAuthResponse( - [property: JsonPropertyName("access_token")] string? AccessToken, - [property: JsonPropertyName("token_type")] string? TokenType, - [property: JsonPropertyName("id_token")] string? IdToken, - [property: JsonPropertyName("refresh_token")] string? RefreshToken, - [property: JsonPropertyName("expires_in")] int ExpiresIn, - [property: JsonPropertyName("scope")] string? Scope, - [property: JsonPropertyName("jti")] string? Jti -); diff --git a/src/Dim.Clients/Token/AuthSettings.cs b/src/Dim.Clients/Token/AuthSettings.cs deleted file mode 100644 index 03ab44a..0000000 --- a/src/Dim.Clients/Token/AuthSettings.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Dim.Clients.Token; - -public class AuthSettings -{ - [Required(AllowEmptyStrings = false)] - public string ClientId { get; set; } = null!; - - [Required(AllowEmptyStrings = false)] - public string ClientSecret { get; set; } = null!; - - [Required(AllowEmptyStrings = false)] - public string TokenAddress { get; set; } = null!; -} diff --git a/src/Dim.Clients/Token/GetTokenSettings.cs b/src/Dim.Clients/Token/GetTokenSettings.cs deleted file mode 100644 index 7c6a405..0000000 --- a/src/Dim.Clients/Token/GetTokenSettings.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Dim.Clients.Token; - -public record GetTokenSettings(string HttpClientName, string ClientId, string ClientSecret, string TokenAddress); diff --git a/src/Dim.Clients/Token/ITokenService.cs b/src/Dim.Clients/Token/ITokenService.cs deleted file mode 100644 index 3d72076..0000000 --- a/src/Dim.Clients/Token/ITokenService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Dim.Clients.Token; - -public interface ITokenService -{ - Task GetAuthorizedClient(AuthSettings settings, CancellationToken cancellationToken); - Task GetAuthorizedLegacyClient(AuthSettings settings, CancellationToken cancellationToken); -} diff --git a/src/Dim.Web/Controllers/DimController.cs b/src/Dim.Web/Controllers/DimController.cs deleted file mode 100644 index ef02209..0000000 --- a/src/Dim.Web/Controllers/DimController.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Dim.Clients.Services; -using Dim.Web.Extensions; -using Dim.Web.Models; -using System.Text.Json; - -namespace Dim.Web.Controllers; - -/// -/// Creates a new instance of -/// -public static class DimController -{ - public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) - { - var policyHub = group.MapGroup("/dim"); - - policyHub.MapPost("setup-dim", (string bpnl, string companyName, CancellationToken cancellationToken, ISetupDimService setupDimService) => setupDimService.SaAndInstanceSetup(bpnl, companyName, cancellationToken)) - .WithSwaggerDescription("Gets the keys for the attributes", - "Example: Post: api/dim/setup-dim", - "The busines partner number of the company", - "The company name") - .Produces(StatusCodes.Status200OK, typeof(JsonDocument), Constants.JsonContentType); - - return group; - } -} diff --git a/src/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs b/src/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs deleted file mode 100644 index c1d7d64..0000000 --- a/src/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Dim.Web.Extensions; - -public static class RouteHandlerBuilderExtensions -{ - public static RouteHandlerBuilder WithSwaggerDescription(this RouteHandlerBuilder builder, string summary, string description, params string[] parameterDescriptions) => - builder.WithOpenApi(op => - { - op.Summary = summary; - op.Description = description; - for (var i = 0; i < parameterDescriptions.Length; i++) - { - if (i < op.Parameters.Count) - { - op.Parameters[i].Description = parameterDescriptions[i]; - } - } - - return op; - }); -} diff --git a/src/Dim.Web/Models/Constants.cs b/src/Dim.Web/Models/Constants.cs deleted file mode 100644 index 415d647..0000000 --- a/src/Dim.Web/Models/Constants.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Dim.Web.Models; - -public static class Constants -{ - public const string JsonContentType = "application/json"; -} diff --git a/src/Dim.Web/Program.cs b/src/Dim.Web/Program.cs deleted file mode 100644 index c754a4d..0000000 --- a/src/Dim.Web/Program.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Dim.Clients.Services; -using Dim.Web.Controllers; - -var builder = WebApplication.CreateBuilder(args); - -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddSetupDim(builder.Configuration); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - -app.UseHttpsRedirection(); - -app.MapGroup("/api") - .WithOpenApi() - .MapDimApi(); - -app.Run(); diff --git a/src/Dim.Web/Properties/launchSettings.json b/src/Dim.Web/Properties/launchSettings.json deleted file mode 100644 index 668bff3..0000000 --- a/src/Dim.Web/Properties/launchSettings.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:39711", - "sslPort": 44301 - } - }, - "profiles": { - "dim.web": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/src/Dim.Web/WebApplication1.http b/src/Dim.Web/WebApplication1.http deleted file mode 100644 index 9abb4d8..0000000 --- a/src/Dim.Web/WebApplication1.http +++ /dev/null @@ -1,6 +0,0 @@ -@WebApplication1_HostAddress = http://localhost:5297 - -GET {{WebApplication1_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/src/Dim.Web/appsettings.json b/src/Dim.Web/appsettings.json deleted file mode 100644 index 969cd62..0000000 --- a/src/Dim.Web/appsettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "Dim": { - "AdminMail": "", - "RootDirectoryId": "", - "ClientidCisCentral": "", - "ClientsecretCisCentral": "", - "AuthUrl": "" - }, - "SubAccount": { - "BaseUrl": "" - }, - "Entitlement": { - "BaseUrl": "" - }, - "Cf": { - "ClientId": "", - "ClientSecret": "", - "TokenAddress": "", - "BaseUrl": "" - } -} \ No newline at end of file diff --git a/src/Dim.sln b/src/Dim.sln index 6ef1db8..90ca4c2 100644 --- a/src/Dim.sln +++ b/src/Dim.sln @@ -1,8 +1,44 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Clients", "Dim.Clients\Dim.Clients.csproj", "{8E2AE6EC-C6FE-4419-B75F-2C9E8E8E5436}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "processes", "processes", "{30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Web", "Dim.Web\Dim.Web.csproj", "{71557885-C517-4C5A-A844-A0846E152AF3}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "clients", "clients", "{B84A3CAB-AC86-4B2D-A490-79E1002350FF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Clients", "clients\Dim.Clients\Dim.Clients.csproj", "{8356C7AF-6F88-4A62-B3E9-5656634A6FEA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library", "processes\Processes.Library\Processes.Library.csproj", "{BFDDFCF5-8F35-4348-A568-57D8E34DF289}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker", "processes\Processes.Worker\Processes.Worker.csproj", "{E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library", "processes\Processes.Worker.Library\Processes.Worker.Library.csproj", "{4F40B8F2-12D4-403B-B666-8F0540354455}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "database", "database", "{4A305F87-23B4-4929-B138-4B7D2710526F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.DbAccess", "database\Dim.DbAccess\Dim.DbAccess.csproj", "{6984539B-7B5A-481E-A955-3AE537D43166}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Entities", "database\Dim.Entities\Dim.Entities.csproj", "{0A85DB84-41A0-4EE0-815B-D922329A8BCA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Executor", "processes\DimProcess.Executor\DimProcess.Executor.csproj", "{41AA384E-E433-440E-A02E-83F03AC98A63}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Library", "processes\DimProcess.Library\DimProcess.Library.csproj", "{F14C5E02-DC54-4A27-B2DE-513A644E6473}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Migrations", "database\Dim.Migrations\Dim.Migrations.csproj", "{1621A06B-9AF2-421C-BB48-08028560E108}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "web", "web", "{D20B95EF-D7D9-45BA-B5A4-C8D3B64AC745}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dim.Web", "web\Dim.Web\Dim.Web.csproj", "{1B50DC13-DB8A-41CF-9D40-A8740B068AC2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{CB1B7D43-9AFC-47EF-8915-A547F7F553AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library.Tests", "..\tests\processes\Processes.Library.Tests\Processes.Library.Tests.csproj", "{AA29EECC-CD77-4452-B48E-CC038E4FBB69}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library.Tests", "..\tests\processes\Processes.Worker.Library.Tests\Processes.Worker.Library.Tests.csproj", "{9888C703-CC13-47EB-89F0-B8A34D64BD07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Shared", "..\tests\shared\Tests.Shared\Tests.Shared.csproj", "{0D288AF0-1CE5-4B2B-9F80-532040F24BCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Executor.Tests", "..\tests\processes\DimProcess.Executor.Tests\DimProcess.Executor.Tests.csproj", "{A44447B0-794D-451A-A571-E3B761174B48}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DimProcess.Library.Tests", "..\tests\processes\DimProcess.Library.Tests\DimProcess.Library.Tests.csproj", "{85D316A0-17BE-4983-AB06-5C72365ABD9B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -10,15 +46,82 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8E2AE6EC-C6FE-4419-B75F-2C9E8E8E5436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8E2AE6EC-C6FE-4419-B75F-2C9E8E8E5436}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8E2AE6EC-C6FE-4419-B75F-2C9E8E8E5436}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8E2AE6EC-C6FE-4419-B75F-2C9E8E8E5436}.Release|Any CPU.Build.0 = Release|Any CPU - {71557885-C517-4C5A-A844-A0846E152AF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71557885-C517-4C5A-A844-A0846E152AF3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71557885-C517-4C5A-A844-A0846E152AF3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71557885-C517-4C5A-A844-A0846E152AF3}.Release|Any CPU.Build.0 = Release|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA}.Release|Any CPU.Build.0 = Release|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BFDDFCF5-8F35-4348-A568-57D8E34DF289}.Release|Any CPU.Build.0 = Release|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2}.Release|Any CPU.Build.0 = Release|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F40B8F2-12D4-403B-B666-8F0540354455}.Release|Any CPU.Build.0 = Release|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6984539B-7B5A-481E-A955-3AE537D43166}.Release|Any CPU.Build.0 = Release|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A85DB84-41A0-4EE0-815B-D922329A8BCA}.Release|Any CPU.Build.0 = Release|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41AA384E-E433-440E-A02E-83F03AC98A63}.Release|Any CPU.Build.0 = Release|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F14C5E02-DC54-4A27-B2DE-513A644E6473}.Release|Any CPU.Build.0 = Release|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1621A06B-9AF2-421C-BB48-08028560E108}.Release|Any CPU.Build.0 = Release|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2}.Release|Any CPU.Build.0 = Release|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA29EECC-CD77-4452-B48E-CC038E4FBB69}.Release|Any CPU.Build.0 = Release|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9888C703-CC13-47EB-89F0-B8A34D64BD07}.Release|Any CPU.Build.0 = Release|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF}.Release|Any CPU.Build.0 = Release|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A44447B0-794D-451A-A571-E3B761174B48}.Release|Any CPU.Build.0 = Release|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85D316A0-17BE-4983-AB06-5C72365ABD9B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution + {8356C7AF-6F88-4A62-B3E9-5656634A6FEA} = {B84A3CAB-AC86-4B2D-A490-79E1002350FF} + {BFDDFCF5-8F35-4348-A568-57D8E34DF289} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {E5F65B6F-BD8F-4FAF-80A2-69C9DA20DCC2} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {4F40B8F2-12D4-403B-B666-8F0540354455} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {6984539B-7B5A-481E-A955-3AE537D43166} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {0A85DB84-41A0-4EE0-815B-D922329A8BCA} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {41AA384E-E433-440E-A02E-83F03AC98A63} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {F14C5E02-DC54-4A27-B2DE-513A644E6473} = {30AA679F-7CF6-4FBE-9A13-BEF98BCFE3D2} + {1621A06B-9AF2-421C-BB48-08028560E108} = {4A305F87-23B4-4929-B138-4B7D2710526F} + {1B50DC13-DB8A-41CF-9D40-A8740B068AC2} = {D20B95EF-D7D9-45BA-B5A4-C8D3B64AC745} + {AA29EECC-CD77-4452-B48E-CC038E4FBB69} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {9888C703-CC13-47EB-89F0-B8A34D64BD07} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {0D288AF0-1CE5-4B2B-9F80-532040F24BCF} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {A44447B0-794D-451A-A571-E3B761174B48} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} + {85D316A0-17BE-4983-AB06-5C72365ABD9B} = {CB1B7D43-9AFC-47EF-8915-A547F7F553AB} EndGlobalSection EndGlobal diff --git a/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs b/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs new file mode 100644 index 0000000..168f9fd --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/AddSpaceRoleToUserRequest.cs @@ -0,0 +1,49 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record AddSpaceRoleToUserRequest( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("relationships")] SpaceRoleRelationship Relationship +); + +public record SpaceRoleRelationship( + [property: JsonPropertyName("user")] RelationshipUser User, + [property: JsonPropertyName("space")] SpaceRoleSpace Space +); + +public record RelationshipUser( + [property: JsonPropertyName("data")] UserData Data +); + +public record UserData( + [property: JsonPropertyName("username")] string Username, + [property: JsonPropertyName("origin")] string Origin +); + +public record SpaceRoleSpace( + [property: JsonPropertyName("data")] SpaceRoleData Data +); + +public record SpaceRoleData( + [property: JsonPropertyName("guid")] Guid Id +); diff --git a/src/clients/Dim.Clients/Api/Cf/CfClient.cs b/src/clients/Dim.Clients/Api/Cf/CfClient.cs new file mode 100644 index 0000000..140f84a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/CfClient.cs @@ -0,0 +1,247 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf.DependencyInjection; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Cf; + +public class CfClient : ICfClient +{ + private readonly CfSettings _settings; + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public CfClient(IBasicAuthTokenService basicAuthTokenService, IOptions settings) + { + _basicAuthTokenService = basicAuthTokenService; + _settings = settings.Value; + } + + public async Task CreateCloudFoundrySpace(string tenantName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var cfEnvironmentId = await GetEnvironmentId(tenantName, cancellationToken, client).ConfigureAwait(false); + var data = new CreateSpaceRequest( + $"{tenantName}-space", + new SpaceRelationship(new SpaceOrganization(new SpaceRelationshipData(cfEnvironmentId))) + ); + + var result = await client.PostAsJsonAsync("/v3/spaces", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-cfe", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + private static async Task GetEnvironmentId(string tenantName, CancellationToken cancellationToken, HttpClient client) + { + var environmentsResponse = await client.GetAsync("/v3/organizations", cancellationToken) + .CatchingIntoServiceExceptionFor("get-organizations", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE); + var environments = await environmentsResponse.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + var tenantEnvironment = environments.Resources.Where(x => x.Name == tenantName); + if (tenantEnvironment.Count() > 1) + { + throw new ConflictException($"There should only be one cf environment for tenant {tenantName}"); + } + + return tenantEnvironment.Single().EnvironmentId; + } + + public async Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new AddSpaceRoleToUserRequest( + type, + new SpaceRoleRelationship( + new RelationshipUser(new UserData(user, "sap.ids")), + new SpaceRoleSpace(new SpaceRoleData(spaceId)) + ) + ); + + await client.PostAsJsonAsync("/v3/roles", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("add-space-roles", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } + + public async Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync("/v3/service_plans", cancellationToken) + .CatchingIntoServiceExceptionFor("get-service-plan", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("response should never be null here"); + } + + var servicePlans = response.Resources.Where(x => x.Name == servicePlanType && + x.BrokerCatalog?.BrokerCatalogMetadata?.AutoSubscription?.AppName == servicePlanName); + if (response == null || servicePlans.Count() != 1) + { + throw new ServiceException($"There must be exactly one service plan with name {servicePlanName} and type {servicePlanType}"); + } + + return servicePlans.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateDimServiceInstance(string tenantName, Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new CreateDimServiceInstance( + "managed", + $"{tenantName}-dim-instance", + new DimRelationships( + new DimSpace(new DimData(spaceId)), + new DimServicePlan(new DimData(servicePlanId))) + ); + + await client.PostAsJsonAsync("/v3/service_instances", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-dim-si", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + } + + private async Task GetServiceInstances(string tenantName, Guid? spaceId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync("/v3/service_instances", cancellationToken) + .CatchingIntoServiceExceptionFor("get-si", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + var name = $"{tenantName}-dim-instance"; + var resources = response.Resources.Where(x => x.Name == name && x.Type == "managed" && (spaceId == null || x.Relationships.Space.Data.Id == spaceId.Value)); + if (resources.Count() != 1) + { + throw new ServiceException($"There must be exactly one service instance"); + } + + return resources.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateServiceInstanceBindings(string tenantName, Guid spaceId, CancellationToken cancellationToken) + { + var serviceInstanceId = await GetServiceInstances(tenantName, spaceId, cancellationToken).ConfigureAwait(false); + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new CreateServiceCredentialBindingRequest( + "key", + $"{tenantName}-dim-key01", + new ServiceCredentialRelationships( + new DimServiceInstance(new DimData(serviceInstanceId))) + ); + await client.PostAsJsonAsync("/v3/service_credential_bindings", data, JsonSerializerOptions.Default, cancellationToken) + .CatchingIntoServiceExceptionFor("create-si-bindings", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS); + } + + public async Task GetServiceBinding(string tenantName, Guid spaceId, string bindingName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var serviceInstanceId = await GetServiceInstances(tenantName, spaceId, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"/v3/service_credential_bindings?names={bindingName}", cancellationToken) + .CatchingIntoServiceExceptionFor("get-credential-binding", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + var resources = response.Resources.Where(x => x.Relationships.ServiceInstance.Data.Id == serviceInstanceId); + if (resources.Count() != 1) + { + throw new ServiceException($"There must be exactly one service credential binding"); + } + + return resources.Single().Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetServiceBindingDetails(Guid id, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedLegacyClient(_settings, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"/v3/service_credential_bindings/{id}/details", cancellationToken) + .CatchingIntoServiceExceptionFor("get-credential-binding-name", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException($"There must be exactly one service instance"); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs b/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs new file mode 100644 index 0000000..46c5f26 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/CreateCfeRequest.cs @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record CreateSpaceRequest( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] SpaceRelationship Relationship +); + +public record SpaceRelationship( + [property: JsonPropertyName("organization")] SpaceOrganization Organization +); + +public record SpaceOrganization( + [property: JsonPropertyName("data")] SpaceRelationshipData Data +); + +public record SpaceRelationshipData( + [property: JsonPropertyName("guid")] Guid Id +); + +public record CreateSpaceResponse( + [property: JsonPropertyName("guid")] Guid Id +); + +public record GetEnvironmentsResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record EnvironmentResource( + [property: JsonPropertyName("guid")] Guid EnvironmentId, + [property: JsonPropertyName("name")] string Name +); diff --git a/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs new file mode 100644 index 0000000..544c9f0 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfClientServiceExtensions.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Cf.DependencyInjection; + +public static class CfClientServiceExtensions +{ + public static IServiceCollection AddCfClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs new file mode 100644 index 0000000..df0560e --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/DependencyInjection/CfSettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Cf.DependencyInjection; + +public class CfSettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Cf/ICfClient.cs b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs new file mode 100644 index 0000000..603392b --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/ICfClient.cs @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Clients.Api.Cf; + +public interface ICfClient +{ + Task CreateCloudFoundrySpace(string tenantName, CancellationToken cancellationToken); + Task AddSpaceRoleToUser(string type, string user, Guid spaceId, CancellationToken cancellationToken); + Task GetServicePlan(string servicePlanName, string servicePlanType, CancellationToken cancellationToken); + Task CreateDimServiceInstance(string tenantName, Guid spaceId, Guid servicePlanId, CancellationToken cancellationToken); + Task CreateServiceInstanceBindings(string tenantName, Guid spaceId, CancellationToken cancellationToken); + Task GetServiceBinding(string tenantName, Guid spaceId, string bindingName, CancellationToken cancellationToken); + Task GetServiceBindingDetails(Guid id, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs b/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs new file mode 100644 index 0000000..248a339 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Cf/ServicePlanResponse.cs @@ -0,0 +1,134 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Cf; + +public record ServicePlanResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServicePlanResources( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("broker_catalog")] BrokerCatalog? BrokerCatalog +); + +public record BrokerCatalog( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("metadata")] BrokerCatalogMetadata? BrokerCatalogMetadata +); + +public record BrokerCatalogMetadata( + [property: JsonPropertyName("auto_subscription")] AutoSupscription? AutoSubscription +); + +public record AutoSupscription( + [property: JsonPropertyName("app_name")] string? AppName +); + +public record ServiceInstanceResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServiceInstanceResource( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("relationships")] ServiceInstanceRelationship Relationships +); + +public record ServiceInstanceRelationship( + [property: JsonPropertyName("space")] ServiceInstanceRelationshipSpace Space +); + +public record ServiceInstanceRelationshipSpace( + [property: JsonPropertyName("data")] DimData Data +); + +public record CreateServiceCredentialBindingRequest( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] ServiceCredentialRelationships Relationships +); + +public record ServiceCredentialRelationships( + [property: JsonPropertyName("service_instance")] DimServiceInstance ServiceInstance +); + +public record DimServiceInstance( + [property: JsonPropertyName("data")] DimData Data +); + +public record CreateDimServiceInstance( + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("relationships")] DimRelationships Relationships +); + +public record DimRelationships( + [property: JsonPropertyName("space")] DimSpace Space, + [property: JsonPropertyName("service_plan")] DimServicePlan ServicePlan +); + +public record DimServicePlan( + [property: JsonPropertyName("data")] DimData Data +); + +public record DimSpace( + [property: JsonPropertyName("data")] DimData Data +); + +public record DimData( + [property: JsonPropertyName("guid")] Guid Id +); + +public record ServiceCredentialBindingResponse( + [property: JsonPropertyName("resources")] IEnumerable Resources +); + +public record ServiceCredentialBindingRelationships( + [property: JsonPropertyName("service_instance")] ScbServiceInstnace ServiceInstance +); + +public record ScbServiceInstnace( + [property: JsonPropertyName("data")] DimData Data +); + +public record ServiceCredentialBindingResource( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("relationships")] ServiceCredentialBindingRelationships Relationships +); + +public record ServiceCredentialBindingDetailResponse( + [property: JsonPropertyName("credentials")] Credentials Credentials +); + +public record Credentials( + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("uaa")] Uaa Uaa +); + +public record Uaa( + [property: JsonPropertyName("clientid")] string ClientId, + [property: JsonPropertyName("clientsecret")] string ClientSecret, + [property: JsonPropertyName("url")] string Url, + [property: JsonPropertyName("apiurl")] string ApiUrl +); diff --git a/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs b/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs new file mode 100644 index 0000000..0525faf --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/ApplicationResponse.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record ApplicationResponse( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("applicationKey")] string ApplicaitonKey, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("colorAccent")] int ColorAccent +); diff --git a/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs b/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs new file mode 100644 index 0000000..91fb437 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CompanyIdentityPatch.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CompanyIdentityPatch( + [property: JsonPropertyName("applicationUpdates")] ApplicationUpdates ApplicationUpdates +); + +public record ApplicationUpdates( + [property: JsonPropertyName("assignApplications")] IEnumerable AssignApplications +); diff --git a/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs b/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs new file mode 100644 index 0000000..e2bf29b --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CreateApplicationRequest.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CreateApplicationRequest( + [property: JsonPropertyName("payload")] ApplicationPayload Payload +); + +public record ApplicationPayload( + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("colorAccent")] int ColorAccent +); + +public record CreateApplicationResponse( + [property: JsonPropertyName("id")] string Id +); diff --git a/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs b/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs new file mode 100644 index 0000000..d27a7dc --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/CreateCompanyIdentityRequest.cs @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Dim; + +public record CreateCompanyIdentityRequest( + [property: JsonPropertyName("payload")] Payload Payload +); + +public record Payload( + [property: JsonPropertyName("hostingUrl")] string HostingUrl, + [property: JsonPropertyName("bootstrap")] Bootstrap Bootstrap, + [property: JsonPropertyName("keys")] IEnumerable Keys +); + +public record Service( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("type")] string Type +); + +public record Bootstrap( + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("protocols")] IEnumerable Protocols +); + +public record Key( + [property: JsonPropertyName("type")] string Type +); + +public record Network( + [property: JsonPropertyName("didMethod")] string DidMethod, + [property: JsonPropertyName("type")] string Type +); + +public record CreateCompanyIdentityResponse( + [property: JsonPropertyName("did")] string Did, + [property: JsonPropertyName("companyId")] Guid CompanyId, + [property: JsonPropertyName("downloadURL")] string DownloadUrl +); diff --git a/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs new file mode 100644 index 0000000..a938f15 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/DependencyInjection/DimClientServiceExtensions.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Dim.DependencyInjection; + +public static class DimClientServiceExtensions +{ + public static IServiceCollection AddDimClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Dim/DimClient.cs b/src/clients/Dim.Clients/Api/Dim/DimClient.cs new file mode 100644 index 0000000..810a169 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/DimClient.cs @@ -0,0 +1,158 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Dim; + +public class DimClient : IDimClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + private readonly IHttpClientFactory _clientFactory; + + public DimClient(IBasicAuthTokenService basicAuthTokenService, IHttpClientFactory clientFactory) + { + _basicAuthTokenService = basicAuthTokenService; + _clientFactory = clientFactory; + } + + public async Task CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimBasicAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateCompanyIdentityRequest(new Payload( + hostingUrl, + new Bootstrap("Holder with IATP", "Holder IATP", Enumerable.Repeat("IATP", 1)), + isIssuer ? + Enumerable.Empty() : + new Key[] + { + new("SIGNING"), + new("SIGNING_VC"), + })); + var result = await client.PostAsJsonAsync($"{baseUrl}/api/v2.0.0/companyIdentities", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-company-identity", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetDidDocument(string url, CancellationToken cancellationToken) + { + var client = _clientFactory.CreateClient("didDocumentDownload"); + using var result = await client.GetStreamAsync(url, cancellationToken).ConfigureAwait(false); + var document = await JsonDocument.ParseAsync(result, cancellationToken: cancellationToken).ConfigureAwait(false); + return document; + } + + public async Task CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var data = new CreateApplicationRequest(new ApplicationPayload( + "catena-x-portal", + $"Catena-X Portal MIW for {tenantName}", + 6)); + var result = await client.PostAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/applications", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response was empty", true); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var result = await client.GetAsync($"{dimBaseUrl}/api/v2.0.0/applications/{applicationId}", cancellationToken) + .CatchingIntoServiceExceptionFor("get-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.ApplicaitonKey; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(dimAuth, cancellationToken).ConfigureAwait(false); + var data = new CompanyIdentityPatch(new ApplicationUpdates(Enumerable.Repeat(applicationKey, 1))); + await client.PatchAsJsonAsync($"{dimBaseUrl}/api/v2.0.0/companyIdentities/{companyId}", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("assign-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async m => + { + var message = await m.Content.ReadAsStringAsync().ConfigureAwait(false); + return (false, message); + }).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Api/Dim/IDimClient.cs b/src/clients/Dim.Clients/Api/Dim/IDimClient.cs new file mode 100644 index 0000000..aef6ba2 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Dim/IDimClient.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; +using System.Text.Json; + +namespace Dim.Clients.Api.Dim; + +public interface IDimClient +{ + Task CreateCompanyIdentity(BasicAuthSettings dimBasicAuth, string hostingUrl, string baseUrl, string tenantName, bool isIssuer, CancellationToken cancellationToken); + Task GetDidDocument(string url, CancellationToken cancellationToken); + Task CreateApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string tenantName, CancellationToken cancellationToken); + + Task GetApplication(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationId, CancellationToken cancellationToken); + Task AssignApplicationToCompany(BasicAuthSettings dimAuth, string dimBaseUrl, string applicationKey, Guid companyId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs new file mode 100644 index 0000000..8046c45 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectoryClientServiceExtensions.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Directories.DependencyInjection; + +public static class DirectoryClientServiceExtensions +{ + public static IServiceCollection AddDirectoryClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs new file mode 100644 index 0000000..f009a26 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DependencyInjection/DirectorySettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Directories.DependencyInjection; + +public class DirectorySettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs b/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs new file mode 100644 index 0000000..ccb8823 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DirectoryClient.cs @@ -0,0 +1,75 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Directories.DependencyInjection; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Directories; + +public class DirectoryClient : IDirectoryClient +{ + private readonly DirectorySettings _settings; + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public DirectoryClient(IBasicAuthTokenService basicAuthTokenService, IOptions settings) + { + _basicAuthTokenService = basicAuthTokenService; + _settings = settings.Value; + } + + public async Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken).ConfigureAwait(false); + var directory = new DirectoryRequest( + description, + Enumerable.Repeat("phil.schneider@digitalnativesolutions.de", 1), + bpnl, + new Dictionary>() + { + { "cloud_management_service", new[] { "Created by API - Don't change it" } } + } + ); + + var result = await client.PostAsJsonAsync($"/accounts/v1/directories?parentId={parentId}", directory, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-directory", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Directory response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs b/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs new file mode 100644 index 0000000..7d1b860 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/DirectoryRequest.cs @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Directories; + +public record DirectoryRequest( + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("directoryAdmins")] IEnumerable DirectoryAdmins, + [property: JsonPropertyName("displayName")] string DisplayName, + [property: JsonPropertyName("labels")] Dictionary> Labels +); + +public record DirectoryResponse( + [property: JsonPropertyName("guid")] Guid Id, + [property: JsonPropertyName("subdomain")] string Subdomain +); diff --git a/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs b/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs new file mode 100644 index 0000000..9c25577 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Directories/IDirectoryClient.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Clients.Api.Directories; + +public interface IDirectoryClient +{ + Task CreateDirectory(string description, string bpnl, Guid parentId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs b/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs new file mode 100644 index 0000000..aa5622a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/CreateSubAccountRequest.cs @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Entitlements; + +public record CreateSubAccountRequest( + [property: JsonPropertyName("subaccountServicePlans")] IEnumerable SubaccountServicePlans +); + +public record SubaccountServicePlan( + [property: JsonPropertyName("assignmentInfo")] IEnumerable AssignmentInfo, + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("servicePlanName")] string ServicePlanName +); + +public record AssignmentInfo( + [property: JsonPropertyName("enable")] bool? Enabled, + [property: JsonPropertyName("amount")] int? Amount, + [property: JsonPropertyName("subaccountGUID")] Guid SubaccountId +); diff --git a/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs new file mode 100644 index 0000000..768e672 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementClientServiceExtensions.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.Entitlements.DependencyInjection; + +public static class EntitlementClientServiceExtensions +{ + public static IServiceCollection AddEntitlementClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs new file mode 100644 index 0000000..394967a --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/DependencyInjection/EntitlementSettings.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Entitlements.DependencyInjection; + +public class EntitlementSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs b/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs new file mode 100644 index 0000000..4f80830 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/EntitlementClient.cs @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; + +namespace Dim.Clients.Api.Entitlements; + +public class EntitlementClient : IEntitlementClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public EntitlementClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task AssignEntitlements(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var data = new CreateSubAccountRequest( + new List + { + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "cis", "local"), + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "decentralized-identity-management-app", "standard"), + new(Enumerable.Repeat(new AssignmentInfo(null, 1, subAccountId), 1), "decentralized-identity-management", "standard"), + new(Enumerable.Repeat(new AssignmentInfo(true, null, subAccountId), 1), "auditlog-viewer", "free") + } + ); + + await client.PutAsJsonAsync("/entitlements/v1/subaccountServicePlans", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("assign-entitlements", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs b/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs new file mode 100644 index 0000000..8c37af8 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Entitlements/IEntitlementClient.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; + +namespace Dim.Clients.Api.Entitlements; + +public interface IEntitlementClient +{ + Task AssignEntitlements(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs b/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs new file mode 100644 index 0000000..cafca13 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/CreateCfeRequest.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Api.Provisioning; + +public record CreateCfeRequest( + [property: JsonPropertyName("environmentType")] string EnvironmentType, + [property: JsonPropertyName("parameters")] Dictionary Parameters, + [property: JsonPropertyName("landscapeLabel")] string LandscapeLabel, + [property: JsonPropertyName("planName")] string PlanName, + [property: JsonPropertyName("serviceName")] string ServiceName, + [property: JsonPropertyName("user")] string User +); + +public record CreateCfeResponse( + [property: JsonPropertyName("id")] Guid Id +); diff --git a/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs new file mode 100644 index 0000000..1682055 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningClientServiceExtensions.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Provisioning.DependencyInjection; + +public static class ProvisioningClientServiceExtensions +{ + public static IServiceCollection AddProvisioningClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs new file mode 100644 index 0000000..d3d3070 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/DependencyInjection/ProvisioningSettings.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.Provisioning.DependencyInjection; + +public class ProvisioningSettings : BasicAuthSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs b/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs new file mode 100644 index 0000000..0c3a111 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/IProvisioningClient.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Services; + +namespace Dim.Clients.Api.Provisioning; + +public interface IProvisioningClient +{ + Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string tenantName, string user, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs b/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs new file mode 100644 index 0000000..1c090e8 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Provisioning/ProvisioningClient.cs @@ -0,0 +1,63 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Services; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Dim.Clients.Api.Provisioning; + +public class ProvisioningClient : IProvisioningClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public ProvisioningClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateCloudFoundryEnvironment(string authUrl, BindingItem bindingData, string tenantName, string user, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + TokenAddress = $"{authUrl}/oauth/token", + ClientId = bindingData.Credentials.Uaa.ClientId, + ClientSecret = bindingData.Credentials.Uaa.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); + var data = new CreateCfeRequest( + "cloudfoundry", + new Dictionary + { + { "instance_name", tenantName } + }, + "cf-eu10", + "standard", + "cloudfoundry", + user + ); + + await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.ProvisioningServiceUrl}/provisioning/v1/environments", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-cf-env", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs b/src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs similarity index 67% rename from src/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs rename to src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs index 8079d4c..86c7ee7 100644 --- a/src/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs +++ b/src/clients/Dim.Clients/Api/Services/CreateServiceInstanceRequest.cs @@ -1,3 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + using System.Text.Json.Serialization; namespace Dim.Clients.Api.Services; diff --git a/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs new file mode 100644 index 0000000..3b0164e --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/DependencyInjection/ServiceClientServiceExtensions.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Services.DependencyInjection; + +public static class ServiceClientServiceExtensions +{ + public static IServiceCollection AddServiceClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Services/IServiceClient.cs b/src/clients/Dim.Clients/Api/Services/IServiceClient.cs new file mode 100644 index 0000000..124aa63 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Services/IServiceClient.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.SubAccounts; + +namespace Dim.Clients.Api.Services; + +public interface IServiceClient +{ + Task CreateServiceInstance(ServiceManagementBindingItem saBinding, CancellationToken cancellationToken); + Task CreateServiceBinding(ServiceManagementBindingItem saBinding, string serviceInstanceId, CancellationToken cancellationToken); + Task GetServiceBinding(ServiceManagementBindingItem saBinding, string serviceBindingName, CancellationToken cancellationToken); +} diff --git a/src/Dim.Clients/Api/Services/ServiceClient.cs b/src/clients/Dim.Clients/Api/Services/ServiceClient.cs similarity index 62% rename from src/Dim.Clients/Api/Services/ServiceClient.cs rename to src/clients/Dim.Clients/Api/Services/ServiceClient.cs index e2b258f..98d8060 100644 --- a/src/Dim.Clients/Api/Services/ServiceClient.cs +++ b/src/clients/Dim.Clients/Api/Services/ServiceClient.cs @@ -1,22 +1,50 @@ -using Dim.Clients.Api.SubAccounts; +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.SubAccounts; using Dim.Clients.Extensions; using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; using System.Net.Http.Json; using System.Text.Json; namespace Dim.Clients.Api.Services; -public class ServiceClient(ITokenService tokenService) : IServiceClient +public class ServiceClient : IServiceClient { - public async Task CreateServiceInstance(ServiceManagementBindingResponse saBinding, CancellationToken cancellationToken) + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public ServiceClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateServiceInstance(ServiceManagementBindingItem saBinding, CancellationToken cancellationToken) { - var serviceAuth = new AuthSettings + var serviceAuth = new BasicAuthSettings { TokenAddress = $"{saBinding.Url}/oauth/token", ClientId = saBinding.ClientId, ClientSecret = saBinding.ClientSecret }; - var client = await tokenService.GetAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); var directory = new CreateServiceInstanceRequest( "cis-local-instance", "cis", @@ -28,7 +56,7 @@ public async Task CreateServiceInstance(ServiceMa ); var result = await client.PostAsJsonAsync($"{saBinding.SmUrl}/v1/service_instances?async=false", directory, JsonSerializerExtensions.Options, cancellationToken) - .CatchingIntoServiceExceptionFor("create-service-instance", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + .CatchingIntoServiceExceptionFor("create-service-instance", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); try { var response = await result.Content @@ -47,15 +75,15 @@ public async Task CreateServiceInstance(ServiceMa } } - public async Task CreateServiceBinding(ServiceManagementBindingResponse saBinding, string serviceInstanceId, CancellationToken cancellationToken) + public async Task CreateServiceBinding(ServiceManagementBindingItem saBinding, string serviceInstanceId, CancellationToken cancellationToken) { - var serviceAuth = new AuthSettings + var serviceAuth = new BasicAuthSettings { TokenAddress = $"{saBinding.Url}/oauth/token", ClientId = saBinding.ClientId, ClientSecret = saBinding.ClientSecret }; - var client = await tokenService.GetAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); var data = new CreateServiceBindingRequest( "cis-local-binding", serviceInstanceId @@ -81,15 +109,15 @@ public async Task CreateServiceBinding(ServiceMana } } - public async Task GetServiceBinding(ServiceManagementBindingResponse saBinding, string serviceBindingName, CancellationToken cancellationToken) + public async Task GetServiceBinding(ServiceManagementBindingItem saBinding, string serviceBindingName, CancellationToken cancellationToken) { - var serviceAuth = new AuthSettings + var serviceAuth = new BasicAuthSettings { TokenAddress = $"{saBinding.Url}/oauth/token", ClientId = saBinding.ClientId, ClientSecret = saBinding.ClientSecret }; - var client = await tokenService.GetAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(serviceAuth, cancellationToken).ConfigureAwait(false); var result = await client.GetAsync($"{saBinding.SmUrl}/v1/service_bindings?fieldQuery=name eq '{serviceBindingName}'", cancellationToken) .CatchingIntoServiceExceptionFor("get-service-binding", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); try diff --git a/src/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs b/src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs similarity index 53% rename from src/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs rename to src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs index 00019a7..42e799e 100644 --- a/src/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs +++ b/src/clients/Dim.Clients/Api/SubAccounts/CreateSubAccountRequest.cs @@ -1,3 +1,22 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + using System.Text.Json.Serialization; namespace Dim.Clients.Api.SubAccounts; @@ -20,6 +39,10 @@ public record CreateSubaccountResponse( ); public record ServiceManagementBindingResponse( + [property: JsonPropertyName("items")] IEnumerable Items +); + +public record ServiceManagementBindingItem( [property: JsonPropertyName("clientid")] string ClientId, [property: JsonPropertyName("clientsecret")] string ClientSecret, [property: JsonPropertyName("sm_url")] string SmUrl, diff --git a/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs new file mode 100644 index 0000000..321ddbe --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountClientServiceExtensions.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Dim.Clients.Api.SubAccounts.DependencyInjection; + +public static class SubAccountClientServiceExtensions +{ + public static IServiceCollection AddSubAccountClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseUrl, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs new file mode 100644 index 0000000..4712e81 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/DependencyInjection/SubAccountSettings.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Api.SubAccounts.DependencyInjection; + +public class SubAccountSettings +{ + [Required] + public string BaseUrl { get; set; } = null!; +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs b/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs new file mode 100644 index 0000000..e325ab2 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/ISubAccountClient.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Token; + +namespace Dim.Clients.Api.SubAccounts; + +public interface ISubAccountClient +{ + Task CreateSubaccount(BasicAuthSettings basicAuthSettings, string adminMail, string tenantName, Guid directoryId, CancellationToken cancellationToken); + Task CreateServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); + Task GetServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs b/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs new file mode 100644 index 0000000..fdcfbf5 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/SubAccountClient.cs @@ -0,0 +1,122 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Dim.Clients.Api.SubAccounts; + +public class SubAccountClient : ISubAccountClient +{ + private static readonly Regex TenantName = new(@"(?<=[^\w-])|(?<=[^-])[\W_]+|(?<=[^-])$", RegexOptions.Compiled, new TimeSpan(0, 0, 0, 1)); + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public SubAccountClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task CreateSubaccount(BasicAuthSettings basicAuthSettings, string adminMail, string tenantName, Guid directoryId, CancellationToken cancellationToken) + { + var subdomain = TenantName.Replace(tenantName, "-").ToLower().TrimStart('-').TrimEnd('-'); + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var directory = new CreateSubAccountRequest( + false, + $"CX customer sub-account {tenantName}", + tenantName, + new Dictionary> + { + { "cloud_management_service", new[] { "Created by API - Don't change it" } }, + { "tenantName", new[] { tenantName } } + }, + "API", + directoryId, + "eu10", + Enumerable.Repeat(adminMail, 1), + subdomain, + UsedForProduction.USED_FOR_PRODUCTION + ); + + var result = await client.PostAsJsonAsync("/accounts/v1/subaccounts", directory, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, async message => + { + var errorMessage = await message.Content.ReadAsStringAsync().ConfigureAwait(false); + return new(false, errorMessage); + }).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null) + { + throw new ServiceException("Response must not be null"); + } + + return response.Id; + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } + + public async Task CreateServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + var data = new + { + name = "accessServiceManager" + }; + + await client.PostAsJsonAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + } + + public async Task GetServiceManagerBindings(BasicAuthSettings basicAuthSettings, Guid subAccountId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(basicAuthSettings, cancellationToken).ConfigureAwait(false); + + var result = await client.GetAsync($"/accounts/v2/subaccounts/{subAccountId}/serviceManagerBindings", cancellationToken) + .CatchingIntoServiceExceptionFor("create-subaccount", HttpAsyncResponseMessageExtension.RecoverOptions.ALLWAYS).ConfigureAwait(false); + try + { + var response = await result.Content + .ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken) + .ConfigureAwait(false); + + if (response == null || response.Items.Count() != 1) + { + throw new ServiceException("Response must not be null and contain exactly 1 item"); + } + + return response.Items.Single(); + } + catch (JsonException je) + { + throw new ServiceException(je.Message); + } + } +} diff --git a/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs b/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs new file mode 100644 index 0000000..3bcdbd5 --- /dev/null +++ b/src/clients/Dim.Clients/Api/SubAccounts/UsedForProduction.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Clients.Api.SubAccounts; + +public enum UsedForProduction +{ + USED_FOR_PRODUCTION = 1, + NOT_USED_FOR_PRODUCTION = 2, + UNSET = 3 +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs b/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs new file mode 100644 index 0000000..6d91fd5 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/DependencyInjection/SubscriptionClientServiceExtensions.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Api.Subscriptions.DependencyInjection; + +public static class SubscriptionClientServiceExtensions +{ + public static IServiceCollection AddSubscriptionClient(this IServiceCollection services) + { + services + .AddCustomHttpClientWithAuthentication(null, null) + .AddTransient(); + + return services; + } +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs b/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs new file mode 100644 index 0000000..ff59545 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/ISubscriptionClient.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Services; + +namespace Dim.Clients.Api.Subscriptions; + +public interface ISubscriptionClient +{ + Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken); +} diff --git a/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs b/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs new file mode 100644 index 0000000..e153f35 --- /dev/null +++ b/src/clients/Dim.Clients/Api/Subscriptions/SubscriptionClient.cs @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Services; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using System.Net.Http.Json; + +namespace Dim.Clients.Api.Subscriptions; + +public class SubscriptionClient : ISubscriptionClient +{ + private readonly IBasicAuthTokenService _basicAuthTokenService; + + public SubscriptionClient(IBasicAuthTokenService basicAuthTokenService) + { + _basicAuthTokenService = basicAuthTokenService; + } + + public async Task SubscribeApplication(string authUrl, BindingItem bindingData, string applicationName, string planName, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + TokenAddress = $"{authUrl}/oauth/token", + ClientId = bindingData.Credentials.Uaa.ClientId, + ClientSecret = bindingData.Credentials.Uaa.ClientSecret + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken).ConfigureAwait(false); + var data = new + { + planName = planName + }; + + await client.PostAsJsonAsync($"{bindingData.Credentials.Endpoints.SaasRegistryServiceUrl}/saas-manager/v1/applications/{applicationName}/subscription", data, JsonSerializerExtensions.Options, cancellationToken) + .CatchingIntoServiceExceptionFor("subscribe-application", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + } +} diff --git a/src/clients/Dim.Clients/Dim.Clients.csproj b/src/clients/Dim.Clients/Dim.Clients.csproj new file mode 100644 index 0000000..35d6649 --- /dev/null +++ b/src/clients/Dim.Clients/Dim.Clients.csproj @@ -0,0 +1,44 @@ + + + + + + net7.0 + enable + enable + Dim.Clients + Dim.Clients + b2a7407a-26de-4833-b207-bb823d67ca22 + + + + + + + + + + + + + + + + diff --git a/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs b/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..cd300dc --- /dev/null +++ b/src/clients/Dim.Clients/Extensions/HttpClientExtensions.cs @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.Clients.Extensions; + +public static class HttpClientExtensions +{ + public static IServiceCollection AddCustomHttpClientWithAuthentication(this IServiceCollection services, string? baseAddress, string? authAddress) where T : class + { + services.AddHttpClient(typeof(T).Name, c => + { + if (baseAddress != null) + { + c.BaseAddress = new Uri(baseAddress); + } + }); + + services.AddHttpClient($"{typeof(T).Name}Auth", c => + { + if (authAddress != null) + { + c.BaseAddress = new Uri(authAddress); + } + }); + return services; + } +} diff --git a/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs b/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs new file mode 100644 index 0000000..3831956 --- /dev/null +++ b/src/clients/Dim.Clients/Extensions/JsonSerializerExtensions.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Dim.Clients.Extensions; + +public static class JsonSerializerExtensions +{ + public static readonly JsonSerializerOptions Options = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = + { + new JsonStringEnumConverter(allowIntegerValues: false) + }, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; +} diff --git a/src/clients/Dim.Clients/Token/AuthResponse.cs b/src/clients/Dim.Clients/Token/AuthResponse.cs new file mode 100644 index 0000000..a9c4b61 --- /dev/null +++ b/src/clients/Dim.Clients/Token/AuthResponse.cs @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Dim.Clients.Token; + +public record AuthResponse( + [property: JsonPropertyName("access_token")] string? AccessToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn, + [property: JsonPropertyName("token_type")] string? TokenType, + [property: JsonPropertyName("jti")] string? Jti, + [property: JsonPropertyName("scope")] string? Scope +); + +public record LegacyAuthResponse( + [property: JsonPropertyName("access_token")] string? AccessToken, + [property: JsonPropertyName("token_type")] string? TokenType, + [property: JsonPropertyName("id_token")] string? IdToken, + [property: JsonPropertyName("refresh_token")] string? RefreshToken, + [property: JsonPropertyName("expires_in")] int ExpiresIn, + [property: JsonPropertyName("scope")] string? Scope, + [property: JsonPropertyName("jti")] string? Jti +); diff --git a/src/clients/Dim.Clients/Token/BasicAuthSettings.cs b/src/clients/Dim.Clients/Token/BasicAuthSettings.cs new file mode 100644 index 0000000..ecaadd6 --- /dev/null +++ b/src/clients/Dim.Clients/Token/BasicAuthSettings.cs @@ -0,0 +1,34 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.ComponentModel.DataAnnotations; + +namespace Dim.Clients.Token; + +public class BasicAuthSettings +{ + [Required(AllowEmptyStrings = false)] + public string ClientId { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientSecret { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string TokenAddress { get; set; } = null!; +} diff --git a/src/Dim.Clients/Token/TokenService.cs b/src/clients/Dim.Clients/Token/BasicAuthTokenService.cs similarity index 78% rename from src/Dim.Clients/Token/TokenService.cs rename to src/clients/Dim.Clients/Token/BasicAuthTokenService.cs index f1f8fc5..23a6a34 100644 --- a/src/Dim.Clients/Token/TokenService.cs +++ b/src/clients/Dim.Clients/Token/BasicAuthTokenService.cs @@ -1,6 +1,5 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG - * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * Copyright (c) 2024 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -19,51 +18,52 @@ ********************************************************************************/ using Dim.Clients.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; using System.Net.Http.Headers; using System.Net.Http.Json; namespace Dim.Clients.Token; -public class TokenService : ITokenService +public class BasicAuthTokenService : IBasicAuthTokenService { private readonly IHttpClientFactory _httpClientFactory; - public TokenService(IHttpClientFactory httpClientFactory) + public BasicAuthTokenService(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } - public async Task GetAuthorizedClient(AuthSettings settings, CancellationToken cancellationToken) + public async Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken) { - var tokenParameters = new GetTokenSettings( + var tokenParameters = new GetBasicTokenSettings( $"{typeof(T).Name}Auth", settings.ClientId, settings.ClientSecret, settings.TokenAddress); - var token = await this.GetTokenAsync(tokenParameters, cancellationToken).ConfigureAwait(false); + var token = await this.GetBasicTokenAsync(tokenParameters, cancellationToken).ConfigureAwait(false); var httpClient = _httpClientFactory.CreateClient(typeof(T).Name); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return httpClient; } - public async Task GetAuthorizedLegacyClient(AuthSettings settings, CancellationToken cancellationToken) + public async Task GetBasicAuthorizedLegacyClient(BasicAuthSettings settings, CancellationToken cancellationToken) { - var tokenParameters = new GetTokenSettings( + var tokenParameters = new GetBasicTokenSettings( $"{typeof(T).Name}Auth", settings.ClientId, settings.ClientSecret, settings.TokenAddress); - var token = await this.GetLegacyToken(tokenParameters, cancellationToken).ConfigureAwait(false); + var token = await this.GetBasicLegacyToken(tokenParameters, cancellationToken).ConfigureAwait(false); var httpClient = _httpClientFactory.CreateClient(typeof(T).Name); httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); return httpClient; } - private async Task GetTokenAsync(GetTokenSettings settings, CancellationToken cancellationToken) + private async Task GetBasicTokenAsync(GetBasicTokenSettings settings, CancellationToken cancellationToken) { var formParameters = new Dictionary { @@ -82,8 +82,8 @@ public async Task GetAuthorizedLegacyClient(AuthSettings settings var responseObject = await response.Content.ReadFromJsonAsync(JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); return responseObject?.AccessToken; } - - private async Task GetLegacyToken(GetTokenSettings settings, CancellationToken cancellationToken) + + private async Task GetBasicLegacyToken(GetBasicTokenSettings settings, CancellationToken cancellationToken) { var formParameters = new Dictionary { diff --git a/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs b/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs new file mode 100644 index 0000000..4761c48 --- /dev/null +++ b/src/clients/Dim.Clients/Token/GetBasicTokenSettings.cs @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Clients.Token; + +public record GetBasicTokenSettings(string HttpClientName, string ClientId, string ClientSecret, string TokenAddress); + +public record GetTokenSettings( + string HttpClientName, + string Username, + string Password, + string ClientId, + string GrantType, + string ClientSecret, + string Scope, + string TokenUrl); diff --git a/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs b/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs new file mode 100644 index 0000000..a3299b3 --- /dev/null +++ b/src/clients/Dim.Clients/Token/IBasicAuthTokenService.cs @@ -0,0 +1,26 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Clients.Token; + +public interface IBasicAuthTokenService +{ + Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken); + Task GetBasicAuthorizedLegacyClient(BasicAuthSettings settings, CancellationToken cancellationToken); +} diff --git a/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs b/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs new file mode 100644 index 0000000..a11c8a2 --- /dev/null +++ b/src/database/Dim.DbAccess/DependencyInjection/DimRepositoriesServiceExtensions.cs @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Dim.DbAccess.DependencyInjection; + +public static class DimRepositoriesServiceExtensions +{ + public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration) + { + services + .AddScoped() + .AddDbContext(o => o + .UseNpgsql(configuration.GetConnectionString("DimDb"))) + .AddHealthChecks() + .AddDbContextCheck("DimDb", tags: new[] { "dimdb" }); + return services; + } +} diff --git a/src/database/Dim.DbAccess/Dim.DbAccess.csproj b/src/database/Dim.DbAccess/Dim.DbAccess.csproj new file mode 100644 index 0000000..45d007e --- /dev/null +++ b/src/database/Dim.DbAccess/Dim.DbAccess.csproj @@ -0,0 +1,40 @@ + + + + + + net7.0 + enable + enable + Dim.DbAccess + Dim.DbAccess + + + + + + + + + + + + + diff --git a/src/database/Dim.DbAccess/DimRepositories.cs b/src/database/Dim.DbAccess/DimRepositories.cs new file mode 100644 index 0000000..df06f5b --- /dev/null +++ b/src/database/Dim.DbAccess/DimRepositories.cs @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess.Repositories; +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; + +namespace Dim.DbAccess; + +public class DimRepositories : IDimRepositories +{ + private readonly DimDbContext _dbContext; + + private static readonly IReadOnlyDictionary> _types = new Dictionary> { + { typeof(IProcessStepRepository), context => new ProcessStepRepository(context) }, + { typeof(ITenantRepository), context => new TenantRepository(context) } + }.ToImmutableDictionary(); + + public DimRepositories(DimDbContext dimDbContext) + { + _dbContext = dimDbContext; + } + + public RepositoryType GetInstance() + { + Object? repository = default; + + if (_types.TryGetValue(typeof(RepositoryType), out var createFunc)) + { + repository = createFunc(_dbContext); + } + + return (RepositoryType)(repository ?? throw new ArgumentException($"unexpected type {typeof(RepositoryType).Name}", nameof(RepositoryType))); + } + + /// + public TEntity Attach(TEntity entity, Action? setOptionalParameters = null) where TEntity : class + { + var attachedEntity = _dbContext.Attach(entity).Entity; + setOptionalParameters?.Invoke(attachedEntity); + + return attachedEntity; + } + + public void AttachRange(IEnumerable entities, Action setOptionalParameters) where TEntity : class + { + foreach (var entity in entities) + { + var attachedEntity = _dbContext.Attach(entity).Entity; + setOptionalParameters.Invoke(attachedEntity); + } + } + + public IEnumerable AttachRange(IEnumerable entities) where TEntity : class + { + foreach (var entity in entities) + { + yield return _dbContext.Attach(entity).Entity; + } + } + + /// + public TEntity Remove(TEntity entity) where TEntity : class + => _dbContext.Remove(entity).Entity; + + public void RemoveRange(IEnumerable entities) where TEntity : class + => _dbContext.RemoveRange(entities); + + public Task SaveAsync() + { + try + { + return _dbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException e) + { + throw new ConflictException("while processing a concurrent update was saved to the database (reason could also be data to be deleted is no longer existing)", e); + } + } + + public void Clear() => _dbContext.ChangeTracker.Clear(); +} diff --git a/src/database/Dim.DbAccess/IDimRepositories.cs b/src/database/Dim.DbAccess/IDimRepositories.cs new file mode 100644 index 0000000..8061ee6 --- /dev/null +++ b/src/database/Dim.DbAccess/IDimRepositories.cs @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.DbAccess; + +public interface IDimRepositories +{ + RepositoryType GetInstance(); + + /// + TEntity Attach(TEntity entity, Action? setOptionalParameters = null) where TEntity : class; + + void AttachRange(IEnumerable entities, Action setOptionalParameters) where TEntity : class; + IEnumerable AttachRange(IEnumerable entities) where TEntity : class; + + /// + TEntity Remove(TEntity entity) where TEntity : class; + + void RemoveRange(IEnumerable entities) where TEntity : class; + Task SaveAsync(); + void Clear(); +} diff --git a/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs b/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs new file mode 100644 index 0000000..3709608 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/IProcessStepRepository.cs @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Entities; +using Dim.Entities.Enums; + +namespace Dim.DbAccess.Repositories; + +/// +/// Repository for accessing and creating processSteps on persistence layer. +/// +public interface IProcessStepRepository +{ + Process CreateProcess(ProcessTypeId processTypeId); + ProcessStep CreateProcessStep(ProcessStepTypeId processStepTypeId, ProcessStepStatusId processStepStatusId, Guid processId); + IEnumerable CreateProcessStepRange(IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus); + void AttachAndModifyProcessStep(Guid processStepId, Action? initialize, Action modify); + void AttachAndModifyProcessSteps(IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdsInitializeModifyData); + IAsyncEnumerable GetActiveProcesses(IEnumerable processTypeIds, IEnumerable processStepTypeIds, DateTimeOffset lockExpiryDate); + IAsyncEnumerable<(Guid ProcessStepId, ProcessStepTypeId ProcessStepTypeId)> GetProcessStepData(Guid processId); +} diff --git a/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs new file mode 100644 index 0000000..8eabcff --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/ITenantRepository.cs @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Entities; + +namespace Dim.DbAccess.Repositories; + +public interface ITenantRepository +{ + Tenant CreateTenant(string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId); + Task<(bool Exists, Guid TenantId, string CompanyName, string Bpn)> GetTenantDataForProcessId(Guid processId); + void AttachAndModifyTenant(Guid tenantId, Action? initialize, Action modify); + Task GetSubAccountIdByTenantId(Guid tenantId); + Task<(Guid? SubAccountId, string? ServiceInstanceId)> GetSubAccountAndServiceInstanceIdsByTenantId(Guid tenantId); + Task<(Guid? SubAccountId, string? ServiceBindingName)> GetSubAccountIdAndServiceBindingNameByTenantId(Guid tenantId); + Task GetSpaceId(Guid tenantId); + Task GetDimInstanceId(Guid tenantId); + Task<(string bpn, string? DownloadUrl, string? Did, Guid? DimInstanceId)> GetCallbackData(Guid tenantId); + Task<(Guid? DimInstanceId, string HostingUrl, bool IsIssuer)> GetDimInstanceIdAndHostingUrl(Guid tenantId); + Task<(string? ApplicationId, Guid? CompanyId, Guid? DimInstanceId)> GetApplicationAndCompanyId(Guid tenantId); +} diff --git a/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs b/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs new file mode 100644 index 0000000..298eea0 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/ProcessStepRepository.cs @@ -0,0 +1,100 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.EntityFrameworkCore; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Dim.DbAccess.Repositories; + +public class ProcessStepRepository : IProcessStepRepository +{ + private readonly DimDbContext _context; + + /// + /// Constructor + /// + /// DimDb context. + public ProcessStepRepository(DimDbContext dimDbContext) + { + _context = dimDbContext; + } + + public Process CreateProcess(ProcessTypeId processTypeId) => + _context.Add(new Process(Guid.NewGuid(), processTypeId, Guid.NewGuid())).Entity; + + public ProcessStep CreateProcessStep(Dim.Entities.Enums.ProcessStepTypeId processStepTypeId, Dim.Entities.Enums.ProcessStepStatusId processStepStatusId, Guid processId) => + _context.Add(new ProcessStep(Guid.NewGuid(), processStepTypeId, processStepStatusId, processId, DateTimeOffset.UtcNow)).Entity; + + public IEnumerable CreateProcessStepRange(IEnumerable<(Dim.Entities.Enums.ProcessStepTypeId ProcessStepTypeId, Dim.Entities.Enums.ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) + { + var processSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToList(); + _context.AddRange(processSteps); + return processSteps; + } + + public void AttachAndModifyProcessStep(Guid processStepId, Action? initialize, Action modify) + { + var step = new ProcessStep(processStepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + _context.Attach(step); + step.DateLastChanged = DateTimeOffset.UtcNow; + modify(step); + } + + public void AttachAndModifyProcessSteps(IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdsInitializeModifyData) + { + var stepModifyData = processStepIdsInitializeModifyData.Select(data => + { + var step = new ProcessStep(data.ProcessStepId, default, default, Guid.Empty, default); + data.Initialize?.Invoke(step); + return (Step: step, data.Modify); + }).ToList(); + _context.AttachRange(stepModifyData.Select(data => data.Step)); + stepModifyData.ForEach(data => + { + data.Step.DateLastChanged = DateTimeOffset.UtcNow; + data.Modify(data.Step); + }); + } + + public IAsyncEnumerable GetActiveProcesses(IEnumerable processTypeIds, IEnumerable processStepTypeIds, DateTimeOffset lockExpiryDate) => + _context.Processes + .AsNoTracking() + .Where(process => + processTypeIds.Contains(process.ProcessTypeId) && + process.ProcessSteps.Any(step => processStepTypeIds.Contains(step.ProcessStepTypeId) && step.ProcessStepStatusId == ProcessStepStatusId.TODO) && + (process.LockExpiryDate == null || process.LockExpiryDate < lockExpiryDate)) + .AsAsyncEnumerable(); + + public IAsyncEnumerable<(Guid ProcessStepId, ProcessStepTypeId ProcessStepTypeId)> GetProcessStepData(Guid processId) => + _context.ProcessSteps + .AsNoTracking() + .Where(step => + step.ProcessId == processId && + step.ProcessStepStatusId == ProcessStepStatusId.TODO) + .OrderBy(step => step.ProcessStepTypeId) + .Select(step => + new ValueTuple( + step.Id, + step.ProcessStepTypeId)) + .AsAsyncEnumerable(); +} diff --git a/src/database/Dim.DbAccess/Repositories/TenantRepository.cs b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs new file mode 100644 index 0000000..5c45412 --- /dev/null +++ b/src/database/Dim.DbAccess/Repositories/TenantRepository.cs @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities; +using Dim.Entities.Entities; +using Microsoft.EntityFrameworkCore; + +namespace Dim.DbAccess.Repositories; + +public class TenantRepository : ITenantRepository +{ + private readonly DimDbContext _context; + + public TenantRepository(DimDbContext context) + { + _context = context; + } + + public Tenant CreateTenant(string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId) => + _context.Tenants.Add(new Tenant(Guid.NewGuid(), companyName, bpn, didDocumentLocation, isIssuer, processId, operatorId)).Entity; + + public Task<(bool Exists, Guid TenantId, string CompanyName, string Bpn)> GetTenantDataForProcessId(Guid processId) => + _context.Tenants + .Where(x => x.ProcessId == processId) + .Select(x => new ValueTuple(true, x.Id, x.CompanyName, x.Bpn)) + .SingleOrDefaultAsync(); + + public void AttachAndModifyTenant(Guid tenantId, Action? initialize, Action modify) + { + var tenant = new Tenant(tenantId, null!, null!, null!, default, Guid.Empty, Guid.Empty); + initialize?.Invoke(tenant); + _context.Tenants.Attach(tenant); + modify(tenant); + } + + public Task GetSubAccountIdByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.SubAccountId) + .SingleOrDefaultAsync(); + + public Task<(Guid? SubAccountId, string? ServiceInstanceId)> GetSubAccountAndServiceInstanceIdsByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.SubAccountId, x.ServiceInstanceId)) + .SingleOrDefaultAsync(); + + public Task<(Guid? SubAccountId, string? ServiceBindingName)> GetSubAccountIdAndServiceBindingNameByTenantId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.SubAccountId, x.ServiceBindingName)) + .SingleOrDefaultAsync(); + + public Task GetSpaceId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.SpaceId) + .SingleOrDefaultAsync(); + + public Task GetDimInstanceId(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => x.DimInstanceId) + .SingleOrDefaultAsync(); + + public Task<(string bpn, string? DownloadUrl, string? Did, Guid? DimInstanceId)> GetCallbackData(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.Bpn, x.DidDownloadUrl, x.Did, x.DimInstanceId)) + .SingleOrDefaultAsync(); + + public Task<(Guid? DimInstanceId, string HostingUrl, bool IsIssuer)> GetDimInstanceIdAndHostingUrl(Guid tenantId) + => _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple(x.DimInstanceId, x.DidDocumentLocation, x.IsIssuer)) + .SingleOrDefaultAsync(); + + public Task<(string? ApplicationId, Guid? CompanyId, Guid? DimInstanceId)> GetApplicationAndCompanyId(Guid tenantId) => + _context.Tenants + .Where(x => x.Id == tenantId) + .Select(x => new ValueTuple( + x.ApplicationId, + x.CompanyId, + x.DimInstanceId)) + .SingleOrDefaultAsync(); +} diff --git a/src/database/Dim.Entities/Dim.Entities.csproj b/src/database/Dim.Entities/Dim.Entities.csproj new file mode 100644 index 0000000..a9dad8a --- /dev/null +++ b/src/database/Dim.Entities/Dim.Entities.csproj @@ -0,0 +1,38 @@ + + + + + + net7.0 + enable + enable + Dim.Entities + Dim.Entities + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/src/database/Dim.Entities/DimDbContext.cs b/src/database/Dim.Entities/DimDbContext.cs new file mode 100644 index 0000000..bfa9bb9 --- /dev/null +++ b/src/database/Dim.Entities/DimDbContext.cs @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.EntityFrameworkCore; + +namespace Dim.Entities; + +public class DimDbContext : DbContext +{ + protected DimDbContext() + { + } + + public DimDbContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet Processes { get; set; } = default!; + public virtual DbSet ProcessSteps { get; set; } = default!; + public virtual DbSet ProcessStepStatuses { get; set; } = default!; + public virtual DbSet ProcessStepTypes { get; set; } = default!; + public virtual DbSet ProcessTypes { get; set; } = default!; + public virtual DbSet Tenants { get; set; } = default!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSnakeCaseNamingConvention(); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasAnnotation("Relational:Collation", "en_US.utf8"); + modelBuilder.HasDefaultSchema("dim"); + + modelBuilder.Entity() + .HasOne(d => d.ProcessType) + .WithMany(p => p.Processes) + .HasForeignKey(d => d.ProcessTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + modelBuilder.Entity() + .HasOne(d => d.Process) + .WithMany(p => p.ProcessSteps) + .HasForeignKey(d => d.ProcessId) + .OnDelete(DeleteBehavior.ClientSetNull); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessTypeId)) + .Cast() + .Select(e => new ProcessType(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessStepStatusId)) + .Cast() + .Select(e => new ProcessStepStatus(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ProcessStepTypeId)) + .Cast() + .Select(e => new ProcessStepType(e)) + ); + + modelBuilder.Entity() + .HasOne(d => d.Process) + .WithMany(p => p.Tenants) + .HasForeignKey(d => d.ProcessId) + .OnDelete(DeleteBehavior.ClientSetNull); + } +} diff --git a/src/database/Dim.Entities/Entities/Process.cs b/src/database/Dim.Entities/Entities/Process.cs new file mode 100644 index 0000000..d336ccb --- /dev/null +++ b/src/database/Dim.Entities/Entities/Process.cs @@ -0,0 +1,54 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class Process : ILockableEntity +{ + private Process() + { + ProcessSteps = new HashSet(); + Tenants = new HashSet(); + } + + public Process(Guid id, ProcessTypeId processTypeId, Guid version) : this() + { + Id = id; + ProcessTypeId = processTypeId; + Version = version; + } + + public Guid Id { get; private set; } + + public ProcessTypeId ProcessTypeId { get; set; } + + public DateTimeOffset? LockExpiryDate { get; set; } + + [ConcurrencyCheck] + public Guid Version { get; set; } + + // Navigation properties + public virtual ProcessType? ProcessType { get; set; } + public virtual ICollection ProcessSteps { get; private set; } + public virtual ICollection Tenants { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStep.cs b/src/database/Dim.Entities/Entities/ProcessStep.cs new file mode 100644 index 0000000..0d281bd --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStep.cs @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; + +namespace Dim.Entities.Entities; + +public class ProcessStep +{ + public ProcessStep(Guid id, ProcessStepTypeId processStepTypeId, ProcessStepStatusId processStepStatusId, Guid processId, DateTimeOffset dateCreated) + { + Id = id; + ProcessStepTypeId = processStepTypeId; + ProcessStepStatusId = processStepStatusId; + ProcessId = processId; + DateCreated = dateCreated; + } + + public Guid Id { get; private set; } + + public ProcessStepTypeId ProcessStepTypeId { get; private set; } + + public ProcessStepStatusId ProcessStepStatusId { get; set; } + + public Guid ProcessId { get; private set; } + + public DateTimeOffset DateCreated { get; private set; } + + public DateTimeOffset? DateLastChanged { get; set; } + + public string? Message { get; set; } + + // Navigation properties + public virtual ProcessStepType? ProcessStepType { get; private set; } + public virtual ProcessStepStatus? ProcessStepStatus { get; set; } + public virtual Process? Process { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStepStatus.cs b/src/database/Dim.Entities/Entities/ProcessStepStatus.cs new file mode 100644 index 0000000..231ddcb --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStepStatus.cs @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessStepStatus +{ + private ProcessStepStatus() + { + this.Label = null!; + this.ProcessSteps = new HashSet(); + } + + public ProcessStepStatus(ProcessStepStatusId processStepStatusId) : this() + { + Id = processStepStatusId; + Label = processStepStatusId.ToString(); + } + + public ProcessStepStatusId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection ProcessSteps { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessStepType.cs b/src/database/Dim.Entities/Entities/ProcessStepType.cs new file mode 100644 index 0000000..6988b2d --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessStepType.cs @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessStepType +{ + private ProcessStepType() + { + this.Label = null!; + this.ProcessSteps = new HashSet(); + } + + public ProcessStepType(ProcessStepTypeId processStepTypeId) : this() + { + Id = processStepTypeId; + Label = processStepTypeId.ToString(); + } + + public ProcessStepTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection ProcessSteps { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/ProcessType.cs b/src/database/Dim.Entities/Entities/ProcessType.cs new file mode 100644 index 0000000..2b6fff2 --- /dev/null +++ b/src/database/Dim.Entities/Entities/ProcessType.cs @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Dim.Entities.Entities; + +public class ProcessType +{ + private ProcessType() + { + this.Label = null!; + this.Processes = new HashSet(); + } + + public ProcessType(ProcessTypeId processTypeId) : this() + { + Id = processTypeId; + Label = processTypeId.ToString(); + } + + public ProcessTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection Processes { get; private set; } +} diff --git a/src/database/Dim.Entities/Entities/Tenant.cs b/src/database/Dim.Entities/Entities/Tenant.cs new file mode 100644 index 0000000..cb70cdf --- /dev/null +++ b/src/database/Dim.Entities/Entities/Tenant.cs @@ -0,0 +1,61 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Entities.Entities; + +public class Tenant +{ + public Tenant(Guid id, string companyName, string bpn, string didDocumentLocation, bool isIssuer, Guid processId, Guid operatorId) + { + Id = id; + CompanyName = companyName; + Bpn = bpn; + DidDocumentLocation = didDocumentLocation; + IsIssuer = isIssuer; + ProcessId = processId; + OperatorId = operatorId; + } + + public Guid Id { get; set; } + public string CompanyName { get; set; } + public string Bpn { get; set; } + + public string DidDocumentLocation { get; set; } + + public bool IsIssuer { get; set; } + + public Guid ProcessId { get; set; } + + public Guid? SubAccountId { get; set; } + + public string? ServiceInstanceId { get; set; } + + public string? ServiceBindingName { get; set; } + + public Guid? SpaceId { get; set; } + + public Guid? DimInstanceId { get; set; } + public string? DidDownloadUrl { get; set; } + public string? Did { get; set; } + public string? ApplicationId { get; set; } + public Guid? CompanyId { get; set; } + public string? ApplicationKey { get; set; } + public Guid OperatorId { get; set; } + public virtual Process? Process { get; set; } +} diff --git a/src/database/Dim.Entities/Entities/VerifyProcessData.cs b/src/database/Dim.Entities/Entities/VerifyProcessData.cs new file mode 100644 index 0000000..5579881 --- /dev/null +++ b/src/database/Dim.Entities/Entities/VerifyProcessData.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Entities.Entities; + +public record VerifyProcessData( + Process? Process, + IEnumerable? ProcessSteps +); diff --git a/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs b/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs new file mode 100644 index 0000000..59f4c8a --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessStepStatusId.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessStepStatusId +{ + TODO = 1, + DONE = 2, + SKIPPED = 3, + FAILED = 4, + DUPLICATE = 5 +} diff --git a/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs new file mode 100644 index 0000000..e2c86f5 --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessStepTypeId.cs @@ -0,0 +1,42 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessStepTypeId +{ + // Setup Dim Process + CREATE_SUBACCOUNT = 1, + CREATE_SERVICEMANAGER_BINDINGS = 2, + ASSIGN_ENTITLEMENTS = 3, + CREATE_SERVICE_INSTANCE = 4, + CREATE_SERVICE_BINDING = 5, + SUBSCRIBE_APPLICATION = 6, + CREATE_CLOUD_FOUNDRY_ENVIRONMENT = 7, + CREATE_CLOUD_FOUNDRY_SPACE = 8, + ADD_SPACE_MANAGER_ROLE = 9, + ADD_SPACE_DEVELOPER_ROLE = 10, + CREATE_DIM_SERVICE_INSTANCE = 11, + CREATE_SERVICE_INSTANCE_BINDING = 12, + GET_DIM_DETAILS = 13, + CREATE_APPLICATION = 14, + CREATE_COMPANY_IDENTITY = 15, + ASSIGN_COMPANY_APPLICATION = 16, + SEND_CALLBACK = 17 +} diff --git a/src/database/Dim.Entities/Enums/ProcessTypeId.cs b/src/database/Dim.Entities/Enums/ProcessTypeId.cs new file mode 100644 index 0000000..5abaddd --- /dev/null +++ b/src/database/Dim.Entities/Enums/ProcessTypeId.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Entities.Enums; + +public enum ProcessTypeId +{ + SETUP_DIM = 1 +} diff --git a/src/database/Dim.Migrations/Dim.Migrations.csproj b/src/database/Dim.Migrations/Dim.Migrations.csproj new file mode 100644 index 0000000..dfb42e4 --- /dev/null +++ b/src/database/Dim.Migrations/Dim.Migrations.csproj @@ -0,0 +1,63 @@ + + + + + + Dim.Migrations + Dim.Migrations + net7.0 + enable + enable + Linux + ..\..\.. + True + Exe + + true + 2f0a8ca8-0f29-4d24-a9cc-792453cf4114 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + Always + + + + + + + + diff --git a/src/database/Dim.Migrations/Migrations/20240307101150_initial.Designer.cs b/src/database/Dim.Migrations/Migrations/20240307101150_initial.Designer.cs new file mode 100644 index 0000000..d753cd7 --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240307101150_initial.Designer.cs @@ -0,0 +1,457 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// +using System; +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dim.Migrations.Migrations +{ + [DbContext(typeof(DimDbContext))] + [Migration("20240307101150_initial")] + partial class initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dim") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LockExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("lock_expiry_date"); + + b.Property("ProcessTypeId") + .HasColumnType("integer") + .HasColumnName("process_type_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_processes"); + + b.HasIndex("ProcessTypeId") + .HasDatabaseName("ix_processes_process_type_id"); + + b.ToTable("processes", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_last_changed"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ProcessStepStatusId") + .HasColumnType("integer") + .HasColumnName("process_step_status_id"); + + b.Property("ProcessStepTypeId") + .HasColumnType("integer") + .HasColumnName("process_step_type_id"); + + b.HasKey("Id") + .HasName("pk_process_steps"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_process_steps_process_id"); + + b.HasIndex("ProcessStepStatusId") + .HasDatabaseName("ix_process_steps_process_step_status_id"); + + b.HasIndex("ProcessStepTypeId") + .HasDatabaseName("ix_process_steps_process_step_type_id"); + + b.ToTable("process_steps", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_statuses"); + + b.ToTable("process_step_statuses", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "TODO" + }, + new + { + Id = 2, + Label = "DONE" + }, + new + { + Id = 3, + Label = "SKIPPED" + }, + new + { + Id = 4, + Label = "FAILED" + }, + new + { + Id = 5, + Label = "DUPLICATE" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_types"); + + b.ToTable("process_step_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_SUBACCOUNT" + }, + new + { + Id = 2, + Label = "CREATE_SERVICEMANAGER_BINDINGS" + }, + new + { + Id = 3, + Label = "ASSIGN_ENTITLEMENTS" + }, + new + { + Id = 4, + Label = "CREATE_SERVICE_INSTANCE" + }, + new + { + Id = 5, + Label = "CREATE_SERVICE_BINDING" + }, + new + { + Id = 6, + Label = "SUBSCRIBE_APPLICATION" + }, + new + { + Id = 7, + Label = "CREATE_CLOUD_FOUNDRY_ENVIRONMENT" + }, + new + { + Id = 8, + Label = "CREATE_CLOUD_FOUNDRY_SPACE" + }, + new + { + Id = 9, + Label = "ADD_SPACE_MANAGER_ROLE" + }, + new + { + Id = 10, + Label = "ADD_SPACE_DEVELOPER_ROLE" + }, + new + { + Id = 11, + Label = "CREATE_DIM_SERVICE_INSTANCE" + }, + new + { + Id = 12, + Label = "CREATE_SERVICE_INSTANCE_BINDING" + }, + new + { + Id = 13, + Label = "GET_DIM_DETAILS" + }, + new + { + Id = 14, + Label = "CREATE_APPLICATION" + }, + new + { + Id = 15, + Label = "CREATE_COMPANY_IDENTITY" + }, + new + { + Id = 16, + Label = "ASSIGN_COMPANY_APPLICATION" + }, + new + { + Id = 17, + Label = "SEND_CALLBACK" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_types"); + + b.ToTable("process_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "SETUP_DIM" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("text") + .HasColumnName("application_id"); + + b.Property("ApplicationKey") + .HasColumnType("text") + .HasColumnName("application_key"); + + b.Property("Bpn") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpn"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("company_name"); + + b.Property("Did") + .HasColumnType("text") + .HasColumnName("did"); + + b.Property("DidDocumentLocation") + .IsRequired() + .HasColumnType("text") + .HasColumnName("did_document_location"); + + b.Property("DidDownloadUrl") + .HasColumnType("text") + .HasColumnName("did_download_url"); + + b.Property("DimInstanceId") + .HasColumnType("uuid") + .HasColumnName("dim_instance_id"); + + b.Property("IsIssuer") + .HasColumnType("boolean") + .HasColumnName("is_issuer"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasColumnName("operator_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ServiceBindingName") + .HasColumnType("text") + .HasColumnName("service_binding_name"); + + b.Property("ServiceInstanceId") + .HasColumnType("text") + .HasColumnName("service_instance_id"); + + b.Property("SpaceId") + .HasColumnType("uuid") + .HasColumnName("space_id"); + + b.Property("SubAccountId") + .HasColumnType("uuid") + .HasColumnName("sub_account_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_tenants_process_id"); + + b.ToTable("tenants", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.HasOne("Dim.Entities.Entities.ProcessType", "ProcessType") + .WithMany("Processes") + .HasForeignKey("ProcessTypeId") + .IsRequired() + .HasConstraintName("fk_processes_process_types_process_type_id"); + + b.Navigation("ProcessType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_process_steps_processes_process_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepStatus", "ProcessStepStatus") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_statuses_process_step_status_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepType", "ProcessStepType") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_types_process_step_type_id"); + + b.Navigation("Process"); + + b.Navigation("ProcessStepStatus"); + + b.Navigation("ProcessStepType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("Tenants") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_tenants_processes_process_id"); + + b.Navigation("Process"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Navigation("ProcessSteps"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Navigation("Processes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/20240307101150_initial.cs b/src/database/Dim.Migrations/Migrations/20240307101150_initial.cs new file mode 100644 index 0000000..0bad05c --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/20240307101150_initial.cs @@ -0,0 +1,273 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Dim.Migrations.Migrations +{ + /// + public partial class initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "dim"); + + migrationBuilder.CreateTable( + name: "process_step_statuses", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_step_statuses", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "process_step_types", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_step_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "process_types", + schema: "dim", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_process_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "processes", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + process_type_id = table.Column(type: "integer", nullable: false), + lock_expiry_date = table.Column(type: "timestamp with time zone", nullable: true), + version = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_processes", x => x.id); + table.ForeignKey( + name: "fk_processes_process_types_process_type_id", + column: x => x.process_type_id, + principalSchema: "dim", + principalTable: "process_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "process_steps", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + process_step_type_id = table.Column(type: "integer", nullable: false), + process_step_status_id = table.Column(type: "integer", nullable: false), + process_id = table.Column(type: "uuid", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: false), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + message = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_process_steps", x => x.id); + table.ForeignKey( + name: "fk_process_steps_process_step_statuses_process_step_status_id", + column: x => x.process_step_status_id, + principalSchema: "dim", + principalTable: "process_step_statuses", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_process_steps_process_step_types_process_step_type_id", + column: x => x.process_step_type_id, + principalSchema: "dim", + principalTable: "process_step_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_process_steps_processes_process_id", + column: x => x.process_id, + principalSchema: "dim", + principalTable: "processes", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "tenants", + schema: "dim", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + company_name = table.Column(type: "text", nullable: false), + bpn = table.Column(type: "text", nullable: false), + did_document_location = table.Column(type: "text", nullable: false), + is_issuer = table.Column(type: "boolean", nullable: false), + process_id = table.Column(type: "uuid", nullable: false), + sub_account_id = table.Column(type: "uuid", nullable: true), + service_instance_id = table.Column(type: "text", nullable: true), + service_binding_name = table.Column(type: "text", nullable: true), + space_id = table.Column(type: "uuid", nullable: true), + dim_instance_id = table.Column(type: "uuid", nullable: true), + did_download_url = table.Column(type: "text", nullable: true), + did = table.Column(type: "text", nullable: true), + application_id = table.Column(type: "text", nullable: true), + company_id = table.Column(type: "uuid", nullable: true), + application_key = table.Column(type: "text", nullable: true), + operator_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_tenants", x => x.id); + table.ForeignKey( + name: "fk_tenants_processes_process_id", + column: x => x.process_id, + principalSchema: "dim", + principalTable: "processes", + principalColumn: "id"); + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_step_statuses", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "TODO" }, + { 2, "DONE" }, + { 3, "SKIPPED" }, + { 4, "FAILED" }, + { 5, "DUPLICATE" } + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_step_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "CREATE_SUBACCOUNT" }, + { 2, "CREATE_SERVICEMANAGER_BINDINGS" }, + { 3, "ASSIGN_ENTITLEMENTS" }, + { 4, "CREATE_SERVICE_INSTANCE" }, + { 5, "CREATE_SERVICE_BINDING" }, + { 6, "SUBSCRIBE_APPLICATION" }, + { 7, "CREATE_CLOUD_FOUNDRY_ENVIRONMENT" }, + { 8, "CREATE_CLOUD_FOUNDRY_SPACE" }, + { 9, "ADD_SPACE_MANAGER_ROLE" }, + { 10, "ADD_SPACE_DEVELOPER_ROLE" }, + { 11, "CREATE_DIM_SERVICE_INSTANCE" }, + { 12, "CREATE_SERVICE_INSTANCE_BINDING" }, + { 13, "GET_DIM_DETAILS" }, + { 14, "CREATE_APPLICATION" }, + { 15, "CREATE_COMPANY_IDENTITY" }, + { 16, "ASSIGN_COMPANY_APPLICATION" }, + { 17, "SEND_CALLBACK" } + }); + + migrationBuilder.InsertData( + schema: "dim", + table: "process_types", + columns: new[] { "id", "label" }, + values: new object[] { 1, "SETUP_DIM" }); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_id", + schema: "dim", + table: "process_steps", + column: "process_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_status_id", + schema: "dim", + table: "process_steps", + column: "process_step_status_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_type_id", + schema: "dim", + table: "process_steps", + column: "process_step_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_processes_process_type_id", + schema: "dim", + table: "processes", + column: "process_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_tenants_process_id", + schema: "dim", + table: "tenants", + column: "process_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "process_steps", + schema: "dim"); + + migrationBuilder.DropTable( + name: "tenants", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_step_statuses", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_step_types", + schema: "dim"); + + migrationBuilder.DropTable( + name: "processes", + schema: "dim"); + + migrationBuilder.DropTable( + name: "process_types", + schema: "dim"); + } + } +} diff --git a/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs new file mode 100644 index 0000000..09107ab --- /dev/null +++ b/src/database/Dim.Migrations/Migrations/DimDbContextModelSnapshot.cs @@ -0,0 +1,454 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +// +using System; +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Dim.Migrations.Migrations +{ + [DbContext(typeof(DimDbContext))] + partial class DimDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("dim") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LockExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("lock_expiry_date"); + + b.Property("ProcessTypeId") + .HasColumnType("integer") + .HasColumnName("process_type_id"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("uuid") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_processes"); + + b.HasIndex("ProcessTypeId") + .HasDatabaseName("ix_processes_process_type_id"); + + b.ToTable("processes", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("DateCreated") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_created"); + + b.Property("DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_last_changed"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ProcessStepStatusId") + .HasColumnType("integer") + .HasColumnName("process_step_status_id"); + + b.Property("ProcessStepTypeId") + .HasColumnType("integer") + .HasColumnName("process_step_type_id"); + + b.HasKey("Id") + .HasName("pk_process_steps"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_process_steps_process_id"); + + b.HasIndex("ProcessStepStatusId") + .HasDatabaseName("ix_process_steps_process_step_status_id"); + + b.HasIndex("ProcessStepTypeId") + .HasDatabaseName("ix_process_steps_process_step_type_id"); + + b.ToTable("process_steps", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_statuses"); + + b.ToTable("process_step_statuses", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "TODO" + }, + new + { + Id = 2, + Label = "DONE" + }, + new + { + Id = 3, + Label = "SKIPPED" + }, + new + { + Id = 4, + Label = "FAILED" + }, + new + { + Id = 5, + Label = "DUPLICATE" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_step_types"); + + b.ToTable("process_step_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_SUBACCOUNT" + }, + new + { + Id = 2, + Label = "CREATE_SERVICEMANAGER_BINDINGS" + }, + new + { + Id = 3, + Label = "ASSIGN_ENTITLEMENTS" + }, + new + { + Id = 4, + Label = "CREATE_SERVICE_INSTANCE" + }, + new + { + Id = 5, + Label = "CREATE_SERVICE_BINDING" + }, + new + { + Id = 6, + Label = "SUBSCRIBE_APPLICATION" + }, + new + { + Id = 7, + Label = "CREATE_CLOUD_FOUNDRY_ENVIRONMENT" + }, + new + { + Id = 8, + Label = "CREATE_CLOUD_FOUNDRY_SPACE" + }, + new + { + Id = 9, + Label = "ADD_SPACE_MANAGER_ROLE" + }, + new + { + Id = 10, + Label = "ADD_SPACE_DEVELOPER_ROLE" + }, + new + { + Id = 11, + Label = "CREATE_DIM_SERVICE_INSTANCE" + }, + new + { + Id = 12, + Label = "CREATE_SERVICE_INSTANCE_BINDING" + }, + new + { + Id = 13, + Label = "GET_DIM_DETAILS" + }, + new + { + Id = 14, + Label = "CREATE_APPLICATION" + }, + new + { + Id = 15, + Label = "CREATE_COMPANY_IDENTITY" + }, + new + { + Id = 16, + Label = "ASSIGN_COMPANY_APPLICATION" + }, + new + { + Id = 17, + Label = "SEND_CALLBACK" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_process_types"); + + b.ToTable("process_types", "dim"); + + b.HasData( + new + { + Id = 1, + Label = "SETUP_DIM" + }); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("text") + .HasColumnName("application_id"); + + b.Property("ApplicationKey") + .HasColumnType("text") + .HasColumnName("application_key"); + + b.Property("Bpn") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpn"); + + b.Property("CompanyId") + .HasColumnType("uuid") + .HasColumnName("company_id"); + + b.Property("CompanyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("company_name"); + + b.Property("Did") + .HasColumnType("text") + .HasColumnName("did"); + + b.Property("DidDocumentLocation") + .IsRequired() + .HasColumnType("text") + .HasColumnName("did_document_location"); + + b.Property("DidDownloadUrl") + .HasColumnType("text") + .HasColumnName("did_download_url"); + + b.Property("DimInstanceId") + .HasColumnType("uuid") + .HasColumnName("dim_instance_id"); + + b.Property("IsIssuer") + .HasColumnType("boolean") + .HasColumnName("is_issuer"); + + b.Property("OperatorId") + .HasColumnType("uuid") + .HasColumnName("operator_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("ServiceBindingName") + .HasColumnType("text") + .HasColumnName("service_binding_name"); + + b.Property("ServiceInstanceId") + .HasColumnType("text") + .HasColumnName("service_instance_id"); + + b.Property("SpaceId") + .HasColumnType("uuid") + .HasColumnName("space_id"); + + b.Property("SubAccountId") + .HasColumnType("uuid") + .HasColumnName("sub_account_id"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_tenants_process_id"); + + b.ToTable("tenants", "dim"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.HasOne("Dim.Entities.Entities.ProcessType", "ProcessType") + .WithMany("Processes") + .HasForeignKey("ProcessTypeId") + .IsRequired() + .HasConstraintName("fk_processes_process_types_process_type_id"); + + b.Navigation("ProcessType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStep", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_process_steps_processes_process_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepStatus", "ProcessStepStatus") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_statuses_process_step_status_id"); + + b.HasOne("Dim.Entities.Entities.ProcessStepType", "ProcessStepType") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessStepTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_process_steps_process_step_types_process_step_type_id"); + + b.Navigation("Process"); + + b.Navigation("ProcessStepStatus"); + + b.Navigation("ProcessStepType"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Tenant", b => + { + b.HasOne("Dim.Entities.Entities.Process", "Process") + .WithMany("Tenants") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_tenants_processes_process_id"); + + b.Navigation("Process"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.Process", b => + { + b.Navigation("ProcessSteps"); + + b.Navigation("Tenants"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepStatus", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessStepType", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Dim.Entities.Entities.ProcessType", b => + { + b.Navigation("Processes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/Dim.Migrations/Program.cs b/src/database/Dim.Migrations/Program.cs new file mode 100644 index 0000000..94e80a9 --- /dev/null +++ b/src/database/Dim.Migrations/Program.cs @@ -0,0 +1,58 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding.DependencyInjection; +using Serilog; +using System.Reflection; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Starting process"); +try +{ + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddDatabaseInitializer(hostContext.Configuration.GetSection("Seeding")) + .AddDbContext(o => + o.UseNpgsql(hostContext.Configuration.GetConnectionString("DimDb"), + x => x.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_dim"))); + }) + .AddLogging() + .Build(); + + await host.Services.InitializeDatabasesAsync(); // We don't actually run anything here. The magic happens in InitializeDatabasesAsync +} +catch (Exception ex) when (!ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) +{ + Log.Fatal("Unhandled exception {Exception}", ex); + throw; +} +finally +{ + Log.Information("Process Shutting down..."); + Log.CloseAndFlush(); +} diff --git a/src/database/Dim.Migrations/Properties/launchSettings.json b/src/database/Dim.Migrations/Properties/launchSettings.json new file mode 100644 index 0000000..f334184 --- /dev/null +++ b/src/database/Dim.Migrations/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Dim.Migrations": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/database/Dim.Migrations/appsettings.json b/src/database/Dim.Migrations/appsettings.json new file mode 100644 index 0000000..39a2b97 --- /dev/null +++ b/src/database/Dim.Migrations/appsettings.json @@ -0,0 +1,34 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Dim.Migrations": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithProcessId", + "WithThreadId" + ], + "Properties": { + "Application": "Dim.Migrations" + } + }, + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Seeding": { + "DataPaths": [ + ], + "TestDataEnvironments": [] + } +} diff --git a/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs b/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs new file mode 100644 index 0000000..4d23319 --- /dev/null +++ b/src/processes/DimProcess.Executor/DependencyInjection/DimProcessCollectionExtensions.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +namespace DimProcess.Executor.DependencyInjection; + +public static class DimProcessCollectionExtensions +{ + public static IServiceCollection AddDimProcessExecutor(this IServiceCollection services, IConfiguration config) => + services + .AddTransient() + .AddDimProcessHandler(config); +} diff --git a/src/processes/DimProcess.Executor/DimProcess.Executor.csproj b/src/processes/DimProcess.Executor/DimProcess.Executor.csproj new file mode 100644 index 0000000..f61044a --- /dev/null +++ b/src/processes/DimProcess.Executor/DimProcess.Executor.csproj @@ -0,0 +1,40 @@ + + + + + + DimProcess.Executor + DimProcess.Executor + net7.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs b/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs new file mode 100644 index 0000000..b1fe5ae --- /dev/null +++ b/src/processes/DimProcess.Executor/DimProcessTypeExecutor.cs @@ -0,0 +1,153 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; + +namespace DimProcess.Executor; + +public class DimProcessTypeExecutor : IProcessTypeExecutor +{ + private readonly IDimRepositories _dimRepositories; + private readonly IDimProcessHandler _dimProcessHandler; + + private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( + ProcessStepTypeId.CREATE_SUBACCOUNT, + ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, + ProcessStepTypeId.ASSIGN_ENTITLEMENTS, + ProcessStepTypeId.CREATE_SERVICE_INSTANCE, + ProcessStepTypeId.CREATE_SERVICE_BINDING, + ProcessStepTypeId.SUBSCRIBE_APPLICATION, + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, + ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, + ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, + ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, + ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, + ProcessStepTypeId.GET_DIM_DETAILS, + ProcessStepTypeId.CREATE_APPLICATION, + ProcessStepTypeId.CREATE_COMPANY_IDENTITY, + ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, + ProcessStepTypeId.SEND_CALLBACK); + + private Guid _tenantId; + private string _tenantName; + + public DimProcessTypeExecutor( + IDimRepositories dimRepositories, + IDimProcessHandler dimProcessHandler) + { + _dimRepositories = dimRepositories; + _dimProcessHandler = dimProcessHandler; + } + + public ProcessTypeId GetProcessTypeId() => ProcessTypeId.SETUP_DIM; + public bool IsExecutableStepTypeId(ProcessStepTypeId processStepTypeId) => _executableProcessSteps.Contains(processStepTypeId); + public IEnumerable GetExecutableStepTypeIds() => _executableProcessSteps; + public ValueTask IsLockRequested(ProcessStepTypeId processStepTypeId) => new(false); + + public async ValueTask InitializeProcess(Guid processId, IEnumerable processStepTypeIds) + { + var (exists, tenantId, companyName, bpn) = await _dimRepositories.GetInstance().GetTenantDataForProcessId(processId).ConfigureAwait(false); + if (!exists) + { + throw new NotFoundException($"process {processId} does not exist or is not associated with an tenant"); + } + + _tenantId = tenantId; + _tenantName = $"{bpn}_{companyName}"; + return new IProcessTypeExecutor.InitializationResult(false, null); + } + + public async ValueTask ExecuteProcessStep(ProcessStepTypeId processStepTypeId, IEnumerable processStepTypeIds, CancellationToken cancellationToken) + { + if (_tenantId == Guid.Empty || _tenantName == default) + { + throw new UnexpectedConditionException("tenantId and tenantName should never be empty here"); + } + + IEnumerable? nextStepTypeIds; + ProcessStepStatusId stepStatusId; + bool modified; + string? processMessage; + + try + { + (nextStepTypeIds, stepStatusId, modified, processMessage) = processStepTypeId switch + { + ProcessStepTypeId.CREATE_SUBACCOUNT => await _dimProcessHandler.CreateSubaccount(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS => await _dimProcessHandler.CreateServiceManagerBindings(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ASSIGN_ENTITLEMENTS => await _dimProcessHandler.AssignEntitlements(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_INSTANCE => await _dimProcessHandler.CreateServiceInstance(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_BINDING => await _dimProcessHandler.CreateServiceBindings(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SUBSCRIBE_APPLICATION => await _dimProcessHandler.SubscribeApplication(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT => await _dimProcessHandler.CreateCloudFoundryEnvironment(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE => await _dimProcessHandler.CreateCloudFoundrySpace(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE => await _dimProcessHandler.AddSpaceManagerRole(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE => await _dimProcessHandler.AddSpaceDeveloperRole(_tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE => await _dimProcessHandler.CreateDimServiceInstance(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING => await _dimProcessHandler.CreateServiceInstanceBindings(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.GET_DIM_DETAILS => await _dimProcessHandler.GetDimDetails(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_APPLICATION => await _dimProcessHandler.CreateApplication(_tenantName, _tenantId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_COMPANY_IDENTITY => await _dimProcessHandler.CreateCompanyIdentity(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION => await _dimProcessHandler.AssignCompanyApplication(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SEND_CALLBACK => await _dimProcessHandler.SendCallback(_tenantId, _tenantName, cancellationToken) + .ConfigureAwait(false), + _ => (null, ProcessStepStatusId.TODO, false, null) + }; + } + catch (Exception ex) when (ex is not SystemException) + { + (stepStatusId, processMessage, nextStepTypeIds) = ProcessError(ex, processStepTypeId); + modified = true; + } + + return new IProcessTypeExecutor.StepExecutionResult(modified, stepStatusId, nextStepTypeIds, null, processMessage); + } + + private static (ProcessStepStatusId StatusId, string? ProcessMessage, IEnumerable? nextSteps) ProcessError(Exception ex, ProcessStepTypeId processStepTypeId) + { + return ex switch + { + ServiceException { IsRecoverable: true } => (ProcessStepStatusId.TODO, ex.Message, null), + _ => (ProcessStepStatusId.FAILED, ex.Message, null) + }; + } +} diff --git a/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs b/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs new file mode 100644 index 0000000..736262a --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/CallbackDataModel.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DimProcess.Library.Callback; + +public record CallbackDataModel( + [property: JsonPropertyName("did")] string Did, + [property: JsonPropertyName("didDocument")] JsonDocument DidDocument, + [property: JsonPropertyName("authenticationDetails")] AuthenticationDetail AuthenticationDetails +); + +public record AuthenticationDetail( + [property: JsonPropertyName("authenticationServiceUrl")] string AuthenticationServiceUrl, + [property: JsonPropertyName("clientID")] string ClientId, + [property: JsonPropertyName("clientSecret")] string ClientSecret +); diff --git a/src/processes/DimProcess.Library/Callback/CallbackService.cs b/src/processes/DimProcess.Library/Callback/CallbackService.cs new file mode 100644 index 0000000..b3fab78 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/CallbackService.cs @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf; +using Dim.Clients.Extensions; +using Dim.Clients.Token; +using DimProcess.Library.Callback.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using System.Net.Http.Json; +using System.Text.Json; + +namespace DimProcess.Library.Callback; + +public class CallbackService : ICallbackService +{ + private readonly ITokenService _tokenService; + private readonly CallbackSettings _settings; + + public CallbackService(ITokenService tokenService, IOptions options) + { + _tokenService = tokenService; + _settings = options.Value; + } + + public async Task SendCallback(string bpn, ServiceCredentialBindingDetailResponse dimDetails, JsonDocument didDocument, string did, CancellationToken cancellationToken) + { + var httpClient = await _tokenService.GetAuthorizedClient(_settings, cancellationToken) + .ConfigureAwait(false); + var data = new CallbackDataModel( + did, + didDocument, + new AuthenticationDetail( + dimDetails.Credentials.Uaa.Url, + dimDetails.Credentials.Uaa.ClientId, + dimDetails.Credentials.Uaa.ClientSecret) + ); + await httpClient.PostAsJsonAsync($"{bpn}", data, JsonSerializerExtensions.Options, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs new file mode 100644 index 0000000..dbe3611 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackServiceExtensions.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace DimProcess.Library.Callback.DependencyInjection; + +public static class CallbackServiceExtensions +{ + public static IServiceCollection AddCallbackClient(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + services + .AddCustomHttpClientWithAuthentication(settings.Value.BaseAddress, settings.Value.TokenAddress) + .AddTransient(); + + return services; + } +} diff --git a/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs new file mode 100644 index 0000000..d3e6631 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/DependencyInjection/CallbackSettings.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; + +namespace DimProcess.Library.Callback.DependencyInjection; + +public class CallbackSettings : KeyVaultAuthSettings +{ + public string BaseAddress { get; set; } +} diff --git a/src/processes/DimProcess.Library/Callback/ICallbackService.cs b/src/processes/DimProcess.Library/Callback/ICallbackService.cs new file mode 100644 index 0000000..97e0ac6 --- /dev/null +++ b/src/processes/DimProcess.Library/Callback/ICallbackService.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf; +using System.Text.Json; + +namespace DimProcess.Library.Callback; + +public interface ICallbackService +{ + Task SendCallback(string bpn, ServiceCredentialBindingDetailResponse dimDetails, JsonDocument didDocument, string did, CancellationToken cancellationToken); +} diff --git a/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs new file mode 100644 index 0000000..ed2cace --- /dev/null +++ b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerExtensions.cs @@ -0,0 +1,56 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf.DependencyInjection; +using Dim.Clients.Api.Dim.DependencyInjection; +using Dim.Clients.Api.Entitlements.DependencyInjection; +using Dim.Clients.Api.Provisioning.DependencyInjection; +using Dim.Clients.Api.Services.DependencyInjection; +using Dim.Clients.Api.SubAccounts.DependencyInjection; +using Dim.Clients.Api.Subscriptions.DependencyInjection; +using Dim.Clients.Token; +using DimProcess.Library.Callback.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace DimProcess.Library.DependencyInjection; + +public static class DimHandlerExtensions +{ + public static IServiceCollection AddDimProcessHandler(this IServiceCollection services, IConfiguration config) + { + services.AddOptions() + .Bind(config.GetSection("Dim")) + .ValidateOnStart(); + + services + .AddTransient() + .AddTransient() + .AddSubAccountClient(config.GetSection("SubAccount")) + .AddEntitlementClient(config.GetSection("Entitlement")) + .AddServiceClient() + .AddSubscriptionClient() + .AddProvisioningClient() + .AddCfClient(config.GetSection("Cf")) + .AddDimClient() + .AddCallbackClient(config.GetSection("Callback")); + + return services; + } +} diff --git a/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs new file mode 100644 index 0000000..5b6ea1c --- /dev/null +++ b/src/processes/DimProcess.Library/DependencyInjection/DimHandlerSettings.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.ComponentModel.DataAnnotations; + +namespace DimProcess.Library.DependencyInjection; + +public class DimHandlerSettings +{ + [Required(AllowEmptyStrings = false)] + public string AdminMail { get; set; } = null!; + + [Required] + public Guid RootDirectoryId { get; set; } + + [Required(AllowEmptyStrings = false)] + public string AuthUrl { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientidCisCentral { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string ClientsecretCisCentral { get; set; } = null!; + + [Required(AllowEmptyStrings = false)] + public string EncryptionKey { get; set; } = null!; +} diff --git a/src/processes/DimProcess.Library/DimProcess.Library.csproj b/src/processes/DimProcess.Library/DimProcess.Library.csproj new file mode 100644 index 0000000..cf20a25 --- /dev/null +++ b/src/processes/DimProcess.Library/DimProcess.Library.csproj @@ -0,0 +1,43 @@ + + + + + + DimProcess.Library + DimProcess.Library + net7.0 + enable + enable + 415ef31b-ed90-4c9e-9e5f-5f3e673d4f91 + + + + + + + + + + + + + + + diff --git a/src/processes/DimProcess.Library/DimProcessHandler.cs b/src/processes/DimProcess.Library/DimProcessHandler.cs new file mode 100644 index 0000000..e69912b --- /dev/null +++ b/src/processes/DimProcess.Library/DimProcessHandler.cs @@ -0,0 +1,540 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf; +using Dim.Clients.Api.Dim; +using Dim.Clients.Api.Entitlements; +using Dim.Clients.Api.Provisioning; +using Dim.Clients.Api.Services; +using Dim.Clients.Api.SubAccounts; +using Dim.Clients.Api.Subscriptions; +using Dim.Clients.Token; +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library.Callback; +using DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace DimProcess.Library; + +public class DimProcessHandler : IDimProcessHandler +{ + private readonly IDimRepositories _dimRepositories; + private readonly ISubAccountClient _subAccountClient; + private readonly IServiceClient _serviceClient; + private readonly IEntitlementClient _entitlementClient; + private readonly ISubscriptionClient _subscriptionClient; + private readonly IProvisioningClient _provisioningClient; + private readonly ICfClient _cfClient; + private readonly IDimClient _dimClient; + private readonly ICallbackService _callbackService; + private readonly DimHandlerSettings _settings; + + public DimProcessHandler( + IDimRepositories dimRepositories, + ISubAccountClient subAccountClient, + IServiceClient serviceClient, + ISubscriptionClient subscriptionClient, + IEntitlementClient entitlementClient, + IProvisioningClient provisioningClient, + ICfClient cfClient, + IDimClient dimClient, + ICallbackService callbackService, + IOptions options) + { + _dimRepositories = dimRepositories; + _subAccountClient = subAccountClient; + _serviceClient = serviceClient; + _entitlementClient = entitlementClient; + _subscriptionClient = subscriptionClient; + _provisioningClient = provisioningClient; + _cfClient = cfClient; + _dimClient = dimClient; + _callbackService = callbackService; + _settings = options.Value; + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateSubaccount(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var parentDirectoryId = _settings.RootDirectoryId; + var adminMail = _settings.AdminMail; + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + + var subAccountId = await _subAccountClient.CreateSubaccount(subAccountAuth, adminMail, tenantName, parentDirectoryId, cancellationToken).ConfigureAwait(false); + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.SubAccountId = null; + }, + tenant => + { + tenant.SubAccountId = subAccountId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceManagerBindings(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + + var tenantRepository = _dimRepositories.GetInstance(); + var subAccountId = await tenantRepository.GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + await _subAccountClient.CreateServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ASSIGN_ENTITLEMENTS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignEntitlements(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var subAccountId = await _dimRepositories.GetInstance().GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + await _entitlementClient.AssignEntitlements(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_INSTANCE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstance(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var subAccountId = await _dimRepositories.GetInstance().GetSubAccountIdByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var serviceInstance = await _serviceClient.CreateServiceInstance(saBinding, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ServiceInstanceId = null; + }, + tenant => + { + tenant.ServiceInstanceId = serviceInstance.Id; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_BINDING, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceBindings(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceInstanceId) = await _dimRepositories.GetInstance().GetSubAccountAndServiceInstanceIdsByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceInstanceId)) + { + throw new ConflictException("ServiceInstanceId must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var serviceBinding = await _serviceClient.CreateServiceBinding(saBinding, serviceInstanceId, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ServiceBindingName = null; + }, + tenant => + { + tenant.ServiceBindingName = serviceBinding.Name; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SUBSCRIBE_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SubscribeApplication(Guid tenantId, CancellationToken cancellationToken) + { + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceBindingName) = await _dimRepositories.GetInstance().GetSubAccountIdAndServiceBindingNameByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceBindingName)) + { + throw new ConflictException("ServiceBindingName must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var bindingResponse = await _serviceClient.GetServiceBinding(saBinding, serviceBindingName, cancellationToken).ConfigureAwait(false); + await _subscriptionClient.SubscribeApplication(saBinding.Url, bindingResponse, "decentralized-identity-management-app", "standard", cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundryEnvironment(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var subAccountAuth = new BasicAuthSettings + { + TokenAddress = $"{_settings.AuthUrl}/oauth/token", + ClientId = _settings.ClientidCisCentral, + ClientSecret = _settings.ClientsecretCisCentral + }; + var (subAccountId, serviceBindingName) = await _dimRepositories.GetInstance().GetSubAccountIdAndServiceBindingNameByTenantId(tenantId).ConfigureAwait(false); + if (subAccountId == null) + { + throw new ConflictException("SubAccountId must not be null."); + } + + if (string.IsNullOrEmpty(serviceBindingName)) + { + throw new ConflictException("ServiceBindingName must not be null."); + } + + var saBinding = await _subAccountClient.GetServiceManagerBindings(subAccountAuth, subAccountId.Value, cancellationToken).ConfigureAwait(false); + var bindingResponse = await _serviceClient.GetServiceBinding(saBinding, serviceBindingName, cancellationToken).ConfigureAwait(false); + await _provisioningClient.CreateCloudFoundryEnvironment(saBinding.Url, bindingResponse, tenantName, adminMail, cancellationToken) + .ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundrySpace(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var spaceId = await _cfClient.CreateCloudFoundrySpace(tenantName, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.SpaceId = null; + }, + tenant => + { + tenant.SpaceId = spaceId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceManagerRole(Guid tenantId, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.AddSpaceRoleToUser("space_manager", adminMail, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceDeveloperRole(Guid tenantId, CancellationToken cancellationToken) + { + var adminMail = _settings.AdminMail; + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.AddSpaceRoleToUser("space_developer", adminMail, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateDimServiceInstance(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + var servicePlanId = await _cfClient.GetServicePlan("decentralized-identity-management", "standard", cancellationToken).ConfigureAwait(false); + await _cfClient.CreateDimServiceInstance(tenantName, spaceId.Value, servicePlanId, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstanceBindings(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + await _cfClient.CreateServiceInstanceBindings(tenantName, spaceId.Value, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.GET_DIM_DETAILS, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> GetDimDetails(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var spaceId = await _dimRepositories.GetInstance().GetSpaceId(tenantId).ConfigureAwait(false); + if (spaceId == null) + { + throw new ConflictException("SpaceId must not be null."); + } + + var dimInstanceId = await _cfClient.GetServiceBinding(tenantName, spaceId.Value, $"{tenantName}-dim-key01", cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.DimInstanceId = null; + }, + tenant => + { + tenant.DimInstanceId = dimInstanceId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateApplication(string tenantName, Guid tenantId, CancellationToken cancellationToken) + { + var (dimInstanceId, _, _) = await _dimRepositories.GetInstance().GetDimInstanceIdAndHostingUrl(tenantId).ConfigureAwait(false); + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var applicationId = await _dimClient.CreateApplication(dimAuth, dimBaseUrl, tenantName, cancellationToken).ConfigureAwait(false); + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ApplicationId = null; + }, + tenant => + { + tenant.ApplicationId = applicationId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_COMPANY_IDENTITY, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCompanyIdentity(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var (dimInstanceId, hostingUrl, isIssuer) = await _dimRepositories.GetInstance().GetDimInstanceIdAndHostingUrl(tenantId).ConfigureAwait(false); + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var result = await _dimClient.CreateCompanyIdentity(dimAuth, hostingUrl, dimBaseUrl, tenantName, isIssuer, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.DidDownloadUrl = null; + tenant.Did = null; + tenant.CompanyId = null; + }, + tenant => + { + tenant.DidDownloadUrl = result.DownloadUrl; + tenant.Did = result.Did; + tenant.CompanyId = result.CompanyId; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignCompanyApplication(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var (applicationId, companyId, dimInstanceId) = await _dimRepositories.GetInstance().GetApplicationAndCompanyId(tenantId).ConfigureAwait(false); + if (applicationId == null) + { + throw new ConflictException("ApplicationId must always be set here"); + } + + if (companyId == null) + { + throw new ConflictException("CompanyId must always be set here"); + } + + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + var dimAuth = new BasicAuthSettings + { + TokenAddress = $"{dimDetails.Credentials.Uaa.Url}/oauth/token", + ClientId = dimDetails.Credentials.Uaa.ClientId, + ClientSecret = dimDetails.Credentials.Uaa.ClientSecret + }; + var dimBaseUrl = dimDetails.Credentials.Url; + var applicationKey = await _dimClient.GetApplication(dimAuth, dimBaseUrl, applicationId, cancellationToken); + await _dimClient.AssignApplicationToCompany(dimAuth, dimBaseUrl, applicationKey, companyId.Value, cancellationToken).ConfigureAwait(false); + + _dimRepositories.GetInstance().AttachAndModifyTenant(tenantId, tenant => + { + tenant.ApplicationKey = null; + }, + tenant => + { + tenant.ApplicationKey = applicationKey; + }); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SEND_CALLBACK, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid tenantId, string tenantName, CancellationToken cancellationToken) + { + var (bpn, downloadUrl, did, dimInstanceId) = await _dimRepositories.GetInstance().GetCallbackData(tenantId).ConfigureAwait(false); + if (downloadUrl == null) + { + throw new ConflictException("DownloadUrl must not be null."); + } + + if (did == null) + { + throw new ConflictException("Did must not be null."); + } + + if (dimInstanceId == null) + { + throw new ConflictException("DimInstanceId must not be null."); + } + + var dimDetails = await _cfClient.GetServiceBindingDetails(dimInstanceId.Value, cancellationToken).ConfigureAwait(false); + var didDocument = await _dimClient.GetDidDocument(downloadUrl, cancellationToken).ConfigureAwait(false); + + await _callbackService.SendCallback(bpn, dimDetails, didDocument, did, cancellationToken).ConfigureAwait(false); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.DONE, + false, + null); + } +} diff --git a/src/processes/DimProcess.Library/IDimProcessHandler.cs b/src/processes/DimProcess.Library/IDimProcessHandler.cs new file mode 100644 index 0000000..f051891 --- /dev/null +++ b/src/processes/DimProcess.Library/IDimProcessHandler.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; + +namespace DimProcess.Library; + +public interface IDimProcessHandler +{ + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateSubaccount(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceManagerBindings(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignEntitlements(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstance(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceBindings(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SubscribeApplication(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundryEnvironment(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCloudFoundrySpace(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceManagerRole(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AddSpaceDeveloperRole(Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateDimServiceInstance(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateServiceInstanceBindings(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateApplication(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> GetDimDetails(string tenantName, Guid tenantId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCompanyIdentity(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> AssignCompanyApplication(Guid tenantId, string tenantName, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SendCallback(Guid tenantId, string tenantName, CancellationToken cancellationToken); +} diff --git a/src/processes/Processes.Library/ManualProcessStepData.cs b/src/processes/Processes.Library/ManualProcessStepData.cs new file mode 100644 index 0000000..7629614 --- /dev/null +++ b/src/processes/Processes.Library/ManualProcessStepData.cs @@ -0,0 +1,31 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.Entities.Entities; +using Dim.Entities.Enums; + +namespace Dim.Processes.Library; + +public record ManualProcessStepData( + ProcessStepTypeId ProcessStepTypeId, + Process Process, + IEnumerable ProcessSteps, + IDimRepositories PortalRepositories +); diff --git a/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs new file mode 100644 index 0000000..c4a8cd1 --- /dev/null +++ b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs @@ -0,0 +1,138 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Dim.Processes.Library; + +public static class VerifyProcessDataExtensions +{ + public static ManualProcessStepData CreateManualProcessData( + this VerifyProcessData? processData, + ProcessStepTypeId processStepTypeId, + IDimRepositories portalRepositories, + Func getProcessEntityName) + { + if (processData is null) + { + throw new NotFoundException($"{getProcessEntityName()} does not exist"); + } + + if (processData.Process == null) + { + throw new ConflictException($"{getProcessEntityName()} is not associated with any process"); + } + + if (processData.Process.IsLocked()) + { + throw new ConflictException($"process {processData.Process.Id} associated with {getProcessEntityName()} is locked, lock expiry is set to {processData.Process.LockExpiryDate}"); + } + + if (processData.ProcessSteps == null) + { + throw new UnexpectedConditionException("processSteps should never be null here"); + } + + if (processData.ProcessSteps.Any(step => step.ProcessStepStatusId != ProcessStepStatusId.TODO)) + { + throw new UnexpectedConditionException($"processSteps should never have any other status than TODO here"); + } + + if (processData.ProcessSteps.All(step => step.ProcessStepTypeId != processStepTypeId)) + { + throw new ConflictException($"{getProcessEntityName()}, process step {processStepTypeId} is not eligible to run"); + } + + return new(processStepTypeId, processData.Process, processData.ProcessSteps, portalRepositories); + } +} + +public static class ManualProcessStepDataExtensions +{ + public static void RequestLock(this ManualProcessStepData context, DateTimeOffset lockExpiryDate) + { + context.PortalRepositories.Attach(context.Process); + + var isLocked = context.Process.TryLock(lockExpiryDate); + if (!isLocked) + { + throw new UnexpectedConditionException("process TryLock should never fail here"); + } + } + + public static void SkipProcessSteps(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .AttachAndModifyProcessSteps( + context.ProcessSteps + .Where(step => step.ProcessStepTypeId != context.ProcessStepTypeId) + .GroupBy(step => step.ProcessStepTypeId) + .IntersectBy(processStepTypeIds, group => group.Key) + .SelectMany(group => ModifyStepStatusRange(group, ProcessStepStatusId.SKIPPED))); + + public static void SkipProcessStepsExcept(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .AttachAndModifyProcessSteps( + context.ProcessSteps + .Where(step => step.ProcessStepTypeId != context.ProcessStepTypeId) + .GroupBy(step => step.ProcessStepTypeId) + .ExceptBy(processStepTypeIds, group => group.Key) + .SelectMany(group => ModifyStepStatusRange(group, ProcessStepStatusId.SKIPPED))); + + public static void ScheduleProcessSteps(this ManualProcessStepData context, IEnumerable processStepTypeIds) => + context.PortalRepositories.GetInstance() + .CreateProcessStepRange( + processStepTypeIds + .Except(context.ProcessSteps.Select(step => step.ProcessStepTypeId)) + .Select(stepTypeId => (stepTypeId, ProcessStepStatusId.TODO, context.Process.Id))); + + public static void FinalizeProcessStep(this ManualProcessStepData context) + { + context.PortalRepositories.GetInstance().AttachAndModifyProcessSteps( + ModifyStepStatusRange(context.ProcessSteps.Where(step => step.ProcessStepTypeId == context.ProcessStepTypeId), ProcessStepStatusId.DONE)); + + context.PortalRepositories.Attach(context.Process); + if (!context.Process.ReleaseLock()) + { + context.Process.UpdateVersion(); + } + } + + private static IEnumerable<(Guid, Action?, Action)> ModifyStepStatusRange(IEnumerable steps, ProcessStepStatusId processStepStatusId) + { + var firstStep = steps.FirstOrDefault(); + + if (firstStep == null) + yield break; + + foreach (var step in steps) + { + yield return ( + step.Id, + null, + ps => ps.ProcessStepStatusId = ps.Id == firstStep.Id + ? processStepStatusId + : ProcessStepStatusId.DUPLICATE); + } + } +} diff --git a/src/processes/Processes.Library/Processes.Library.csproj b/src/processes/Processes.Library/Processes.Library.csproj new file mode 100644 index 0000000..784bcbe --- /dev/null +++ b/src/processes/Processes.Library/Processes.Library.csproj @@ -0,0 +1,40 @@ + + + + + + Dim.Processes.Library + Dim.Processes.Library + net7.0 + enable + enable + 76a1cf69-39e1-43a7-b6a7-fef83be5359f + + + + + + + + + + + + diff --git a/src/processes/Processes.Worker.Library/IProcessExecutor.cs b/src/processes/Processes.Worker.Library/IProcessExecutor.cs new file mode 100644 index 0000000..a6643d0 --- /dev/null +++ b/src/processes/Processes.Worker.Library/IProcessExecutor.cs @@ -0,0 +1,35 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public interface IProcessExecutor +{ + enum ProcessExecutionResult + { + SaveRequested = 1, + LockRequested = 2, + Unmodified = 3 + } + IAsyncEnumerable ExecuteProcess(Guid processId, ProcessTypeId processTypeId, CancellationToken cancellationToken); + IEnumerable GetRegisteredProcessTypeIds(); + IEnumerable GetExecutableStepTypeIds(); +} diff --git a/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs b/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs new file mode 100644 index 0000000..c7c8b78 --- /dev/null +++ b/src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs @@ -0,0 +1,46 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public interface IProcessTypeExecutor +{ + record InitializationResult(bool Modified, IEnumerable? ScheduleStepTypeIds); + record StepExecutionResult(bool Modified, ProcessStepStatusId ProcessStepStatusId, IEnumerable? ScheduleStepTypeIds, IEnumerable? SkipStepTypeIds, string? ProcessMessage); + + ValueTask InitializeProcess(Guid processId, IEnumerable processStepTypeIds); + ValueTask IsLockRequested(ProcessStepTypeId processStepTypeId); + + /// + /// tbd + /// + /// + /// + /// + /// Is thrown if entity is not found + /// Is thrown if ... + /// + ValueTask ExecuteProcessStep(ProcessStepTypeId processStepTypeId, IEnumerable processStepTypeIds, CancellationToken cancellationToken); + bool IsExecutableStepTypeId(ProcessStepTypeId processStepTypeId); + ProcessTypeId GetProcessTypeId(); + IEnumerable GetExecutableStepTypeIds(); +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionService.cs b/src/processes/Processes.Worker.Library/ProcessExecutionService.cs new file mode 100644 index 0000000..dc14420 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionService.cs @@ -0,0 +1,145 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +/// +/// Service that reads all open/pending processSteps of a checklist and triggers their execution. +/// +public class ProcessExecutionService +{ + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly ProcessExecutionServiceSettings _settings; + private readonly ILogger _logger; + + /// + /// Creates a new instance of + /// + /// access to the services + /// date time provider + /// access to the options + /// the logger + public ProcessExecutionService( + IServiceScopeFactory serviceScopeFactory, + IDateTimeProvider dateTimeProvider, + IOptions options, + ILogger logger) + { + _serviceScopeFactory = serviceScopeFactory; + _dateTimeProvider = dateTimeProvider; + _settings = options.Value; + _logger = logger; + } + + /// + /// Handles the checklist processing + /// + /// Cancellation Token + public async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + using var processServiceScope = _serviceScopeFactory.CreateScope(); + var executorRepositories = processServiceScope.ServiceProvider.GetRequiredService(); + var processExecutor = processServiceScope.ServiceProvider.GetRequiredService(); + + using var outerLoopScope = _serviceScopeFactory.CreateScope(); + var outerLoopRepositories = outerLoopScope.ServiceProvider.GetRequiredService(); + + var lockExpiryTime = new TimeSpan(_settings.LockExpirySeconds * 10000000L); + var activeProcesses = outerLoopRepositories.GetInstance().GetActiveProcesses(processExecutor.GetRegisteredProcessTypeIds(), processExecutor.GetExecutableStepTypeIds(), _dateTimeProvider.OffsetNow); + await foreach (var process in activeProcesses.WithCancellation(stoppingToken).ConfigureAwait(false)) + { + try + { + if (process.IsLocked()) + { + _logger.LogInformation("skipping locked process {processId} type {processType}, lock expires at {lockExpireDate}", process.Id, process.ProcessTypeId, process.LockExpiryDate); + continue; + } + _logger.LogInformation("start processing process {processId} type {processType}", process.Id, process.ProcessTypeId); + + bool EnsureLock() + { + if (process.IsLocked()) + { + return false; + } + var isLocked = process.TryLock(_dateTimeProvider.OffsetNow.Add(lockExpiryTime)); + if (!isLocked) + { + throw new UnexpectedConditionException("process TryLock should never fail here"); + } + return true; + } + + bool UpdateVersion() + { + if (!process.IsLocked()) + { + process.UpdateVersion(); + } + return true; + } + + await foreach (var executionResult in processExecutor.ExecuteProcess(process.Id, process.ProcessTypeId, stoppingToken).WithCancellation(stoppingToken).ConfigureAwait(false)) + { + if (executionResult switch + { + IProcessExecutor.ProcessExecutionResult.LockRequested => EnsureLock(), + IProcessExecutor.ProcessExecutionResult.SaveRequested => UpdateVersion(), + _ => false + }) + { + await executorRepositories.SaveAsync().ConfigureAwait(false); + } + executorRepositories.Clear(); + } + + if (process.ReleaseLock()) + { + await executorRepositories.SaveAsync().ConfigureAwait(false); + executorRepositories.Clear(); + } + _logger.LogInformation("finished processing process {processId}", process.Id); + } + catch (Exception ex) when (ex is not SystemException) + { + _logger.LogInformation(ex, "error processing process {processId} type {processType}: {message}", process.Id, process.ProcessTypeId, ex.Message); + executorRepositories.Clear(); + } + } + } + catch (Exception ex) + { + Environment.ExitCode = 1; + _logger.LogError(ex, "processing failed with following Exception {ExceptionMessage}", ex.Message); + } + } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs new file mode 100644 index 0000000..eaf39f0 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs @@ -0,0 +1,37 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public static class ProcessExecutionServiceExtensions +{ + public static IServiceCollection AddProcessExecutionService(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions().Bind(section); + services + .AddTransient() + .AddTransient() + .AddTransient(); + return services; + } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs b/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs new file mode 100644 index 0000000..4dba7df --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs @@ -0,0 +1,28 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public class ProcessExecutionServiceSettings +{ + [Required] + public int LockExpirySeconds { get; set; } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutor.cs b/src/processes/Processes.Worker.Library/ProcessExecutor.cs new file mode 100644 index 0000000..f5aa1db --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutor.cs @@ -0,0 +1,213 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; + +public class ProcessExecutor : IProcessExecutor +{ + private readonly ImmutableDictionary _executors; + private readonly IProcessStepRepository _processStepRepository; + private readonly ILogger _logger; + + public ProcessExecutor(IEnumerable executors, IDimRepositories portalRepositories, ILogger logger) + { + _processStepRepository = portalRepositories.GetInstance(); + _executors = executors.ToImmutableDictionary(executor => executor.GetProcessTypeId()); + _logger = logger; + } + + public IEnumerable GetRegisteredProcessTypeIds() => _executors.Keys; + public IEnumerable GetExecutableStepTypeIds() => _executors.Values.SelectMany(executor => executor.GetExecutableStepTypeIds()); + + public async IAsyncEnumerable ExecuteProcess(Guid processId, ProcessTypeId processTypeId, [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (!_executors.TryGetValue(processTypeId, out var executor)) + { + throw new UnexpectedConditionException($"processType {processTypeId} is not a registered executable processType."); + } + + var allSteps = await _processStepRepository + .GetProcessStepData(processId) + .PreSortedGroupBy(x => x.ProcessStepTypeId, x => x.ProcessStepId) + .ToDictionaryAsync(g => g.Key, g => g.AsEnumerable(), cancellationToken) + .ConfigureAwait(false); + + var context = new ProcessContext( + processId, + allSteps, + new ProcessStepTypeSet(allSteps.Keys.Where(x => executor.IsExecutableStepTypeId(x))), + executor); + + var (modified, initialStepTypeIds) = await executor.InitializeProcess(processId, context.AllSteps.Keys).ConfigureAwait(false); + + modified |= ScheduleProcessStepTypeIds(initialStepTypeIds, context); + + yield return modified + ? IProcessExecutor.ProcessExecutionResult.SaveRequested + : IProcessExecutor.ProcessExecutionResult.Unmodified; + + while (context.ExecutableStepTypeIds.TryGetNext(out var stepTypeId)) + { + if (await executor.IsLockRequested(stepTypeId).ConfigureAwait(false)) + { + yield return IProcessExecutor.ProcessExecutionResult.LockRequested; + } + ProcessStepStatusId resultStepStatusId; + IEnumerable? scheduleStepTypeIds; + IEnumerable? skipStepTypeIds; + string? processMessage; + bool success; + try + { + (modified, resultStepStatusId, scheduleStepTypeIds, skipStepTypeIds, processMessage) = await executor.ExecuteProcessStep(stepTypeId, context.AllSteps.Keys, cancellationToken).ConfigureAwait(false); + success = true; + } + catch (Exception e) when (e is not SystemException) + { + resultStepStatusId = ProcessStepStatusId.FAILED; + processMessage = $"{e.GetType()}: {e.Message}"; + scheduleStepTypeIds = null; + skipStepTypeIds = null; + modified = false; + success = false; + } + if (!success) + { + yield return IProcessExecutor.ProcessExecutionResult.Unmodified; + } + modified |= SetProcessStepStatus(stepTypeId, resultStepStatusId, context, processMessage); + modified |= SkipProcessStepTypeIds(skipStepTypeIds, context); + modified |= ScheduleProcessStepTypeIds(scheduleStepTypeIds, context); + + yield return modified + ? IProcessExecutor.ProcessExecutionResult.SaveRequested + : IProcessExecutor.ProcessExecutionResult.Unmodified; + } + } + + private bool ScheduleProcessStepTypeIds(IEnumerable? scheduleStepTypeIds, ProcessContext context) + { + if (scheduleStepTypeIds == null || !scheduleStepTypeIds.Any()) + { + return false; + } + + var newStepTypeIds = scheduleStepTypeIds.Except(context.AllSteps.Keys).ToList(); + if (!newStepTypeIds.Any()) + { + return false; + } + foreach (var newStep in _processStepRepository.CreateProcessStepRange(newStepTypeIds.Select(stepTypeId => (stepTypeId, ProcessStepStatusId.TODO, context.ProcessId)))) + { + context.AllSteps.Add(newStep.ProcessStepTypeId, new[] { newStep.Id }); + if (context.Executor.IsExecutableStepTypeId(newStep.ProcessStepTypeId)) + { + context.ExecutableStepTypeIds.Add(newStep.ProcessStepTypeId); + } + } + return true; + } + + private bool SkipProcessStepTypeIds(IEnumerable? skipStepTypeIds, ProcessContext context) + { + if (skipStepTypeIds == null || !skipStepTypeIds.Any()) + { + return false; + } + var modified = false; + foreach (var skipStepTypeId in skipStepTypeIds) + { + var skippedStep = SetProcessStepStatus(skipStepTypeId, ProcessStepStatusId.SKIPPED, context, null); + if (skippedStep) + { + _logger.LogInformation("Skipped step {SkipStepTypeId} for process {ProcessId}", skipStepTypeId, context.ProcessId); + } + + modified |= skippedStep; + } + return modified; + } + + private bool SetProcessStepStatus(ProcessStepTypeId stepTypeId, ProcessStepStatusId stepStatusId, ProcessContext context, string? processMessage) + { + if ((stepStatusId == ProcessStepStatusId.TODO && processMessage == null) || !context.AllSteps.Remove(stepTypeId, out var stepIds)) + { + return false; + } + + var isFirst = true; + foreach (var stepId in stepIds) + { + _processStepRepository.AttachAndModifyProcessStep(stepId, null, step => + { + step.ProcessStepStatusId = isFirst ? stepStatusId : ProcessStepStatusId.DUPLICATE; + step.Message = processMessage; + }); + isFirst = false; + } + if (context.Executor.IsExecutableStepTypeId(stepTypeId)) + { + context.ExecutableStepTypeIds.Remove(stepTypeId); + } + return true; + } + + private sealed record ProcessContext( + Guid ProcessId, + IDictionary> AllSteps, + ProcessStepTypeSet ExecutableStepTypeIds, + IProcessTypeExecutor Executor + ); + + private sealed class ProcessStepTypeSet + { + private readonly HashSet _items; + + public ProcessStepTypeSet(IEnumerable items) + { + _items = new HashSet(items); + } + + public bool TryGetNext(out ProcessStepTypeId item) + { + using var enumerator = _items.GetEnumerator(); + if (!enumerator.MoveNext()) + { + item = default; + return false; + } + item = enumerator.Current; + _items.Remove(item); + return true; + } + + public void Add(ProcessStepTypeId item) => _items.Add(item); + + public void Remove(ProcessStepTypeId item) => _items.Remove(item); + } +} diff --git a/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj new file mode 100644 index 0000000..587a7f8 --- /dev/null +++ b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj @@ -0,0 +1,45 @@ + + + + + + Processes.Worker.Library + Processes.Worker.Library + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/src/processes/Processes.Worker/Processes.Worker.csproj b/src/processes/Processes.Worker/Processes.Worker.csproj new file mode 100644 index 0000000..5285bbb --- /dev/null +++ b/src/processes/Processes.Worker/Processes.Worker.csproj @@ -0,0 +1,53 @@ + + + + + + Processes.Worker + Processes.Worker + net7.0 + enable + enable + Exe + Linux + ..\..\.. + True + f3fe97a1-10c5-4549-b468-dd1cb392247e + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs new file mode 100644 index 0000000..db5d6dd --- /dev/null +++ b/src/processes/Processes.Worker/Program.cs @@ -0,0 +1,70 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess.DependencyInjection; +using DimProcess.Executor.DependencyInjection; +using DimProcess.Library.Callback.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using Serilog; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Building worker"); +try +{ + var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddTransient() + .AddDatabase(hostContext.Configuration) + .AddProcessExecutionService(hostContext.Configuration.GetSection("Processes")) + .AddDimProcessExecutor(hostContext.Configuration); + }) + .AddLogging() + .Build(); + Log.Information("Building worker completed"); + + var tokenSource = new CancellationTokenSource(); + Console.CancelKeyPress += (s, e) => + { + Log.Information("Canceling..."); + tokenSource.Cancel(); + e.Cancel = true; + }; + + Log.Information("Start processing"); + var workerInstance = host.Services.GetRequiredService(); + await workerInstance.ExecuteAsync(tokenSource.Token).ConfigureAwait(false); + Log.Information("Execution finished shutting down"); +} +catch (Exception ex) when (!ex.GetType().Name.Equals("StopTheHostException", StringComparison.Ordinal)) +{ + Log.Fatal(ex, "Unhandled exception"); +} +finally +{ + Log.Information("Server Shutting down"); + Log.CloseAndFlush(); +} diff --git a/src/processes/Processes.Worker/Properties/launchSettings.json b/src/processes/Processes.Worker/Properties/launchSettings.json new file mode 100644 index 0000000..5de5fb5 --- /dev/null +++ b/src/processes/Processes.Worker/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Processes.Worker": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/processes/Processes.Worker/appsettings.json b/src/processes/Processes.Worker/appsettings.json new file mode 100644 index 0000000..5850051 --- /dev/null +++ b/src/processes/Processes.Worker/appsettings.json @@ -0,0 +1,56 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Org.Eclipse.TractusX.Portal.Backend": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext" + ], + "Properties": { + "Application": "Dim.Process.Worker" + } + }, + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;", + }, + "Dim": { + "AdminMail": "", + "RootDirectoryId": "", + "ClientidCisCentral": "", + "ClientsecretCisCentral": "", + "AuthUrl": "" + }, + "SubAccount": { + "BaseUrl": "" + }, + "Entitlement": { + "BaseUrl": "" + }, + "Cf": { + "ClientId": "", + "ClientSecret": "", + "TokenAddress": "", + "BaseUrl": "", + "GrantType": "" + }, + "Callback": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "TokenAddress": "", + "BaseAddress": "" + } +} diff --git a/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs new file mode 100644 index 0000000..a49f083 --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/DimBusinessLogic.cs @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Text.RegularExpressions; + +namespace Dim.Web.BusinessLogic; + +public class DimBusinessLogic : IDimBusinessLogic +{ + private readonly IDimRepositories _dimRepositories; + private readonly DimSettings _settings; + + public DimBusinessLogic(IDimRepositories dimRepositories, IOptions options) + { + _dimRepositories = dimRepositories; + _settings = options.Value; + } + + public async Task StartSetupDim(string companyName, string bpn, string didDocumentLocation, bool isIssuer) + { + var processStepRepository = _dimRepositories.GetInstance(); + var processId = processStepRepository.CreateProcess(ProcessTypeId.SETUP_DIM).Id; + processStepRepository.CreateProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, ProcessStepStatusId.TODO, processId); + + _dimRepositories.GetInstance().CreateTenant(companyName, bpn, didDocumentLocation, isIssuer, processId, _settings.OperatorId); + + await _dimRepositories.SaveAsync().ConfigureAwait(false); + } +} diff --git a/src/web/Dim.Web/BusinessLogic/DimSettings.cs b/src/web/Dim.Web/BusinessLogic/DimSettings.cs new file mode 100644 index 0000000..26ca08e --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/DimSettings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Web.BusinessLogic; + +public class DimSettings +{ + public Guid OperatorId { get; set; } +} diff --git a/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs new file mode 100644 index 0000000..045d3a3 --- /dev/null +++ b/src/web/Dim.Web/BusinessLogic/IDimBusinessLogic.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.DependencyInjection; + +namespace Dim.Web.BusinessLogic; + +public interface IDimBusinessLogic : ITransient +{ + Task StartSetupDim(string companyName, string bpn, string didDocumentLocation, bool isIssuer); +} diff --git a/src/web/Dim.Web/Controllers/DimController.cs b/src/web/Dim.Web/Controllers/DimController.cs new file mode 100644 index 0000000..ef23564 --- /dev/null +++ b/src/web/Dim.Web/Controllers/DimController.cs @@ -0,0 +1,53 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Web.BusinessLogic; +using Dim.Web.Extensions; +using Microsoft.AspNetCore.Mvc; + +namespace Dim.Web.Controllers; + +/// +/// Creates a new instance of +/// +public static class DimController +{ + public static RouteGroupBuilder MapDimApi(this RouteGroupBuilder group) + { + var policyHub = group.MapGroup("/dim"); + + policyHub.MapPost("setup-dim", ([FromQuery] string companyName, [FromQuery] string bpn, [FromQuery] string didDocumentLocation, IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.StartSetupDim(companyName, bpn, didDocumentLocation, false)) + .WithSwaggerDescription("Gets the keys for the attributes", + "Example: Post: api/dim/setup-dim", + "the name of the company", + "bpn of the wallets company", + "The did document location") + .Produces(StatusCodes.Status201Created); + + policyHub.MapPost("setup-issuer", ([FromQuery] string companyName, [FromQuery] string bpn, [FromQuery] string didDocumentLocation, IDimBusinessLogic dimBusinessLogic) => dimBusinessLogic.StartSetupDim(companyName, bpn, didDocumentLocation, true)) + .WithSwaggerDescription("Gets the keys for the attributes", + "Example: Post: api/dim/setup-issuer", + "the name of the company", + "bpn of the wallets company", + "The did document location") + .Produces(StatusCodes.Status201Created); + + return group; + } +} diff --git a/src/Dim.Web/Dim.Web.csproj b/src/web/Dim.Web/Dim.Web.csproj similarity index 60% rename from src/Dim.Web/Dim.Web.csproj rename to src/web/Dim.Web/Dim.Web.csproj index df78a45..1d11c87 100644 --- a/src/Dim.Web/Dim.Web.csproj +++ b/src/web/Dim.Web/Dim.Web.csproj @@ -1,22 +1,25 @@ - net8.0 + net7.0 enable enable true Linux Dim.Web - 732e6cca-c7bc-4ed7-a186-77d0c2a0d054 + 732e6cca-c7bc-4ed7-a186-77d0c2a0d054 + True + CS1591 - - + + + - + diff --git a/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs b/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs new file mode 100644 index 0000000..ab472e4 --- /dev/null +++ b/src/web/Dim.Web/Extensions/RouteHandlerBuilderExtensions.cs @@ -0,0 +1,39 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Web.Extensions; + +public static class RouteHandlerBuilderExtensions +{ + public static RouteHandlerBuilder WithSwaggerDescription(this RouteHandlerBuilder builder, string summary, string description, params string[] parameterDescriptions) => + builder.WithOpenApi(op => + { + op.Summary = summary; + op.Description = description; + for (var i = 0; i < parameterDescriptions.Length; i++) + { + if (i < op.Parameters.Count) + { + op.Parameters[i].Description = parameterDescriptions[i]; + } + } + + return op; + }); +} diff --git a/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs b/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..13d6a2b --- /dev/null +++ b/src/web/Dim.Web/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Web.BusinessLogic; + +namespace Dim.Web.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddDim(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + return services; + } +} diff --git a/src/web/Dim.Web/Models/Constants.cs b/src/web/Dim.Web/Models/Constants.cs new file mode 100644 index 0000000..deff25e --- /dev/null +++ b/src/web/Dim.Web/Models/Constants.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Dim.Web.Models; + +public static class Constants +{ + public const string JsonContentType = "application/json"; +} diff --git a/src/web/Dim.Web/Program.cs b/src/web/Dim.Web/Program.cs new file mode 100644 index 0000000..0b06f4c --- /dev/null +++ b/src/web/Dim.Web/Program.cs @@ -0,0 +1,50 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess.DependencyInjection; +using Dim.Web.Controllers; +using Dim.Web.Extensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; +using System.Text.Json.Serialization; + +const string Version = "v1"; + +WebApplicationBuildRunner + .BuildAndRunWebApplication(args, "dim", Version, "dim", + builder => + { + builder.Services + .AddDim(builder.Configuration.GetSection("Dim")) + .AddEndpointsApiExplorer() + .AddDatabase(builder.Configuration) + .ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }) + .Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + }, + (app, env) => + { + app.MapGroup("/api") + .WithOpenApi() + .MapDimApi(); + }); diff --git a/src/web/Dim.Web/Properties/launchSettings.json b/src/web/Dim.Web/Properties/launchSettings.json new file mode 100644 index 0000000..3e6ab2e --- /dev/null +++ b/src/web/Dim.Web/Properties/launchSettings.json @@ -0,0 +1,40 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:30092", + "sslPort": 44344 + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "api/dim/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000" + } + }, + "DimWeb.Service": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/dim/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000", + "Cors__AllowedOrigins__1": "https://portal.example.org" + }, + "applicationUrl": "https://localhost:7001;http://localhost:7000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/src/web/Dim.Web/appsettings.json b/src/web/Dim.Web/appsettings.json new file mode 100644 index 0000000..6a5536a --- /dev/null +++ b/src/web/Dim.Web/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DimDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Dim": { + "RootDirectoryId": "" + }, + "SwaggerEnabled": false +} \ No newline at end of file diff --git a/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj b/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj new file mode 100644 index 0000000..945a56d --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/DimProcess.Executor.Tests.csproj @@ -0,0 +1,31 @@ + + + net8.0 + enable + enable + false + DimProcess.Executor.Tests + DimProcess.Executor.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs b/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs new file mode 100644 index 0000000..c2d99bc --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/DimProcessTypeExecutorTests.cs @@ -0,0 +1,322 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Enums; +using DimProcess.Library; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace DimProcess.Executor.Tests; + +public class CredentialProcessTypeExecutorTests +{ + private readonly DimProcessTypeExecutor _sut; + private readonly IDimProcessHandler _dimProcessHandler; + private readonly ITenantRepository _tenantRepository; + + public CredentialProcessTypeExecutorTests() + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + var repositories = A.Fake(); + _dimProcessHandler = A.Fake(); + + _tenantRepository = A.Fake(); + + A.CallTo(() => repositories.GetInstance()).Returns(_tenantRepository); + + _sut = new DimProcessTypeExecutor(repositories, _dimProcessHandler); + } + + [Fact] + public void GetProcessTypeId_ReturnsExpected() + { + // Assert + _sut.GetProcessTypeId().Should().Be(ProcessTypeId.SETUP_DIM); + } + + [Fact] + public void IsExecutableStepTypeId_WithValid_ReturnsExpected() + { + // Assert + _sut.IsExecutableStepTypeId(ProcessStepTypeId.SEND_CALLBACK).Should().BeTrue(); + } + + [Fact] + public void GetExecutableStepTypeIds_ReturnsExpected() + { + // Assert + _sut.GetExecutableStepTypeIds().Should().HaveCount(17).And.Satisfy( + x => x == ProcessStepTypeId.CREATE_SUBACCOUNT, + x => x == ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS, + x => x == ProcessStepTypeId.ASSIGN_ENTITLEMENTS, + x => x == ProcessStepTypeId.CREATE_SERVICE_INSTANCE, + x => x == ProcessStepTypeId.CREATE_SERVICE_BINDING, + x => x == ProcessStepTypeId.SUBSCRIBE_APPLICATION, + x => x == ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT, + x => x == ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE, + x => x == ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE, + x => x == ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE, + x => x == ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE, + x => x == ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING, + x => x == ProcessStepTypeId.GET_DIM_DETAILS, + x => x == ProcessStepTypeId.CREATE_APPLICATION, + x => x == ProcessStepTypeId.CREATE_COMPANY_IDENTITY, + x => x == ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION, + x => x == ProcessStepTypeId.SEND_CALLBACK); + } + + [Fact] + public async Task IsLockRequested_ReturnsExpected() + { + // Act + var result = await _sut.IsLockRequested(ProcessStepTypeId.SEND_CALLBACK); + + // Assert + result.Should().BeFalse(); + } + + #region InitializeProcess + + [Fact] + public async Task InitializeProcess_WithExistingProcess_ReturnsExpected() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, Guid.NewGuid(), "test", "test1")); + + // Act + var result = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert + result.Modified.Should().BeFalse(); + result.ScheduleStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task InitializeProcess_WithNotExistingProcess_ThrowsNotFoundException() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(false, Guid.Empty, string.Empty, string.Empty)); + + // Act + async Task Act() => await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"process {validProcessId} does not exist or is not associated with an tenant"); + } + + #endregion + + #region ExecuteProcessStep + + [Fact] + public async Task ExecuteProcessStep_WithoutRegistrationId_ThrowsUnexpectedConditionException() + { + // Act + async Task Act() => await _sut.ExecuteProcessStep(ProcessStepTypeId.SEND_CALLBACK, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be("tenantId and tenantName should never be empty here"); + } + + [Theory] + [InlineData(ProcessStepTypeId.CREATE_SUBACCOUNT)] + [InlineData(ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS)] + [InlineData(ProcessStepTypeId.ASSIGN_ENTITLEMENTS)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_INSTANCE)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_BINDING)] + [InlineData(ProcessStepTypeId.SUBSCRIBE_APPLICATION)] + [InlineData(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT)] + [InlineData(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE)] + [InlineData(ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE)] + [InlineData(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE)] + [InlineData(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE)] + [InlineData(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING)] + [InlineData(ProcessStepTypeId.GET_DIM_DETAILS)] + [InlineData(ProcessStepTypeId.CREATE_APPLICATION)] + [InlineData(ProcessStepTypeId.CREATE_COMPANY_IDENTITY)] + [InlineData(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION)] + [InlineData(ProcessStepTypeId.SEND_CALLBACK)] + public async Task ExecuteProcessStep_WithValidData_CallsExpected(ProcessStepTypeId processStepTypeId) + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + SetupMock(tenantId, "test1_test"); + + // Act + var result = await _sut.ExecuteProcessStep(processStepTypeId, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeFalse(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.ProcessMessage.Should().BeNull(); + result.SkipStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task ExecuteProcessStep_WithRecoverableServiceException_ReturnsToDo() + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, "test1_test", A._)) + .Throws(new ServiceException("this is a test", true)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeTrue(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.TODO); + result.ProcessMessage.Should().Be("this is a test"); + result.SkipStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task ExecuteProcessStep_WithServiceException_ReturnsFailedAndRetriggerStep() + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var tenantId = Guid.NewGuid(); + A.CallTo(() => _tenantRepository.GetTenantDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, tenantId, "test", "test1")); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, "test1_test", A._)) + .Throws(new ServiceException("this is a test")); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_SUBACCOUNT, Enumerable.Empty(), CancellationToken.None); + + // Assert + result.Modified.Should().BeTrue(); + result.ScheduleStepTypeIds.Should().BeNull(); + result.ProcessStepStatusId.Should().Be(ProcessStepStatusId.FAILED); + result.ProcessMessage.Should().Be("this is a test"); + result.SkipStepTypeIds.Should().BeNull(); + } + + #endregion + + #region Setup + + private void SetupMock(Guid tenantId, string tenantName) + { + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceManagerBindings(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AssignEntitlements(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceInstance(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceBindings(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.SubscribeApplication(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCloudFoundryEnvironment(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCloudFoundrySpace(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AddSpaceManagerRole(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AddSpaceDeveloperRole(tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateSubaccount(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateDimServiceInstance(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateServiceInstanceBindings(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.GetDimDetails(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateApplication(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.GetDimDetails(tenantName, tenantId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.CreateCompanyIdentity(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.AssignCompanyApplication(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + A.CallTo(() => _dimProcessHandler.SendCallback(tenantId, tenantName, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + } + + #endregion +} diff --git a/tests/processes/DimProcess.Executor.Tests/Usings.cs b/tests/processes/DimProcess.Executor.Tests/Usings.cs new file mode 100644 index 0000000..14669b9 --- /dev/null +++ b/tests/processes/DimProcess.Executor.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj b/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj new file mode 100644 index 0000000..ad17ca6 --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/DimProcess.Library.Tests.csproj @@ -0,0 +1,29 @@ + + + net8.0 + enable + enable + false + DimProcess.Library.Tests + DimProcess.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs b/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs new file mode 100644 index 0000000..007a278 --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/DimProcessHandlerTests.cs @@ -0,0 +1,990 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.Clients.Api.Cf; +using Dim.Clients.Api.Dim; +using Dim.Clients.Api.Entitlements; +using Dim.Clients.Api.Provisioning; +using Dim.Clients.Api.Services; +using Dim.Clients.Api.SubAccounts; +using Dim.Clients.Api.Subscriptions; +using Dim.Clients.Token; +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using DimProcess.Library.Callback; +using DimProcess.Library.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Text.Json; + +namespace DimProcess.Library.Tests; + +public class DimProcessHandlerTests +{ + private readonly Guid _tenantId = Guid.NewGuid(); + private readonly Guid _processId = Guid.NewGuid(); + private readonly Guid _operatorId = Guid.NewGuid(); + private readonly string _tenantName = "testCorp"; + private readonly Guid _rootDirectoryId = Guid.NewGuid(); + + private readonly IDimRepositories _repositories; + private readonly ITenantRepository _tenantRepositories; + private readonly ISubAccountClient _subAccountClient; + private readonly IServiceClient _serviceClient; + private readonly ISubscriptionClient _subscriptionClient; + private readonly IEntitlementClient _entitlementClient; + private readonly IProvisioningClient _provisioningClient; + private readonly ICfClient _cfClient; + private readonly IDimClient _dimClient; + private readonly ICallbackService _callbackService; + private readonly IOptions _options; + + private readonly DimProcessHandler _sut; + private readonly IFixture _fixture; + + public DimProcessHandlerTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _repositories = A.Fake(); + _tenantRepositories = A.Fake(); + + A.CallTo(() => _repositories.GetInstance()).Returns(_tenantRepositories); + + _subAccountClient = A.Fake(); + _serviceClient = A.Fake(); + _subscriptionClient = A.Fake(); + _entitlementClient = A.Fake(); + _provisioningClient = A.Fake(); + _cfClient = A.Fake(); + _dimClient = A.Fake(); + _callbackService = A.Fake(); + _options = Options.Create(new DimHandlerSettings + { + AdminMail = "test@example.org", + AuthUrl = "https://example.org/auth", + ClientidCisCentral = "test123", + ClientsecretCisCentral = "test654", + EncryptionKey = "test123", + RootDirectoryId = _rootDirectoryId + }); + + _sut = new DimProcessHandler(_repositories, _subAccountClient, _serviceClient, _subscriptionClient, + _entitlementClient, _provisioningClient, _cfClient, _dimClient, _callbackService, _options); + } + + #region CreateSubaccount + + [Fact] + public async Task CreateSubaccount_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _subAccountClient.CreateSubaccount(A._, A._, _tenantName, A._, A._)) + .Returns(subAccountId); + + // Act + var result = await _sut.CreateSubaccount(_tenantId, _tenantName, CancellationToken.None); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICEMANAGER_BINDINGS); + tenant.SubAccountId.Should().Be(subAccountId); + } + + #endregion + + #region CreateServiceManagerBindings + + [Fact] + public async Task CreateServiceManagerBindings_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceManagerBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceManagerBindings_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + + // Act + var result = await _sut.CreateServiceManagerBindings(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _subAccountClient.CreateServiceManagerBindings(A._, subAccountId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ASSIGN_ENTITLEMENTS); + } + + #endregion + + #region AssignEntitlements + + [Fact] + public async Task AssignEntitlements_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AssignEntitlements(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task AssignEntitlements_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + + // Act + var result = await _sut.AssignEntitlements(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _entitlementClient.AssignEntitlements(A._, subAccountId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_INSTANCE); + } + + #endregion + + #region CreateServiceInstance + + [Fact] + public async Task CreateServiceInstance_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceInstance(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceInstance_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstance = new CreateServiceInstanceResponse(Guid.NewGuid().ToString(), "test"); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSubAccountIdByTenantId(_tenantId)) + .Returns(subAccountId); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _serviceClient.CreateServiceInstance(A._, A._)) + .Returns(serviceInstance); + + // Act + var result = await _sut.CreateServiceInstance(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _serviceClient.CreateServiceInstance(A._, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_BINDING); + tenant.ServiceInstanceId.Should().Be(serviceInstance.Id); + } + + #endregion + + #region CreateServiceBindings + + [Fact] + public async Task CreateServiceBindings_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.CreateServiceBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateServiceBindings_WithNotExistingServiceInstance_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.CreateServiceBindings(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ServiceInstanceId must not be null."); + } + + [Fact] + public async Task CreateServiceBindings_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid().ToString(); + var binding = new ServiceManagementBindingItem("cl1", "s1", "https://example.org/sm", "https://example.org"); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((subAccountId, serviceInstanceId)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(binding); + A.CallTo(() => _serviceClient.CreateServiceBinding(binding, serviceInstanceId, A._)) + .Returns(new CreateServiceBindingResponse(Guid.NewGuid().ToString(), "expectedName")); + + // Act + var result = await _sut.CreateServiceBindings(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _serviceClient.CreateServiceBinding(binding, serviceInstanceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SUBSCRIBE_APPLICATION); + tenant.ServiceBindingName.Should().Be("expectedName"); + } + + #endregion + + #region SubscribeApplication + + [Fact] + public async Task SubscribeApplication_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.SubscribeApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task SubscribeApplication_WithNotExistingServiceBindingName_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountAndServiceInstanceIdsByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.SubscribeApplication(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task SubscribeApplication_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid(); + var serviceBindingName = Guid.NewGuid().ToString(); + var serviceManagementBinding = new ServiceManagementBindingItem("c1", "cs1", "https://example.org/sm", "https://example.org/"); + var binding = new BindingItem("binding1", serviceInstanceId, _fixture.Create()); + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((subAccountId, serviceBindingName)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(serviceManagementBinding); + A.CallTo(() => _serviceClient.GetServiceBinding(A._, serviceBindingName, A._)) + .Returns(binding); + A.CallTo(() => _serviceClient.CreateServiceBinding(serviceManagementBinding, serviceBindingName, A._)) + .Returns(new CreateServiceBindingResponse(Guid.NewGuid().ToString(), "expectedName")); + + // Act + var result = await _sut.SubscribeApplication(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _subscriptionClient.SubscribeApplication(A._, binding, "decentralized-identity-management-app", "standard", A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_ENVIRONMENT); + } + + #endregion + + #region CreateCloudFoundryEnvironment + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithNotExistingSubAccount_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((null, null)); + async Task Act() => await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SubAccountId must not be null."); + } + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithNotExistingServiceInstance_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((Guid.NewGuid(), null)); + async Task Act() => await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ServiceBindingName must not be null."); + } + + [Fact] + public async Task CreateCloudFoundryEnvironment_WithValidData_ReturnsExpected() + { + // Arrange + var subAccountId = Guid.NewGuid(); + var serviceInstanceId = Guid.NewGuid(); + var serviceManagementBinding = new ServiceManagementBindingItem("c1", "cs1", "https://example.org/sm", "https://example.org/"); + var binding = new BindingItem("binding1", serviceInstanceId, _fixture.Create()); + A.CallTo(() => _tenantRepositories.GetSubAccountIdAndServiceBindingNameByTenantId(_tenantId)) + .Returns((subAccountId, binding.Name)); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(new ServiceManagementBindingItem("test", "test123", "https://example.org/sm", "https://example.org")); + A.CallTo(() => _subAccountClient.GetServiceManagerBindings(A._, subAccountId, A._)) + .Returns(serviceManagementBinding); + A.CallTo(() => _serviceClient.GetServiceBinding(serviceManagementBinding, binding.Name, A._)) + .Returns(binding); + + // Act + var result = await _sut.CreateCloudFoundryEnvironment(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _provisioningClient.CreateCloudFoundryEnvironment(A._, binding, _tenantName, A._, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_CLOUD_FOUNDRY_SPACE); + } + + #endregion + + #region CreateCloudFoundrySpace + + [Fact] + public async Task CreateCloudFoundrySpace_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _cfClient.CreateCloudFoundrySpace(_tenantName, A._)) + .Returns(spaceId); + + // Act + var result = await _sut.CreateCloudFoundrySpace(_tenantId, _tenantName, CancellationToken.None); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ADD_SPACE_MANAGER_ROLE); + tenant.SpaceId.Should().Be(spaceId); + } + + #endregion + + #region AddSpaceManagerRole + + [Fact] + public async Task AddSpaceManagerRole_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AddSpaceManagerRole(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task AddSpaceManagerRole_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + + // Act + var result = await _sut.AddSpaceManagerRole(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.AddSpaceRoleToUser("space_manager", A._, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ADD_SPACE_DEVELOPER_ROLE); + } + + #endregion + + #region AddSpaceManagerRole + + [Fact] + public async Task AddSpaceDeveloperRole_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.AddSpaceDeveloperRole(_tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task AddSpaceDeveloperRole_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + + // Act + var result = await _sut.AddSpaceDeveloperRole(_tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.AddSpaceRoleToUser("space_developer", A._, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_DIM_SERVICE_INSTANCE); + } + + #endregion + + #region CreateDimServiceInstance + + [Fact] + public async Task CreateDimServiceInstance_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateDimServiceInstance(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task CreateDimServiceInstance_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var servicePlanId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServicePlan("decentralized-identity-management", "standard", A._)) + .Returns(servicePlanId); + + // Act + var result = await _sut.CreateDimServiceInstance(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.CreateDimServiceInstance(_tenantName, spaceId, servicePlanId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_SERVICE_INSTANCE_BINDING); + } + + #endregion + + #region CreateServiceInstanceBindings + + [Fact] + public async Task CreateServiceInstanceBindings_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.CreateServiceInstanceBindings(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task CreateServiceInstanceBindings_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var servicePlanId = Guid.NewGuid(); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServicePlan("decentralized-identity-management", "standard", A._)) + .Returns(servicePlanId); + + // Act + var result = await _sut.CreateServiceInstanceBindings(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _cfClient.CreateServiceInstanceBindings(_tenantName, spaceId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.GET_DIM_DETAILS); + } + + #endregion + + #region GetDimDetails + + [Fact] + public async Task GetDimDetails_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns((Guid?)null); + async Task Act() => await _sut.GetDimDetails(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("SpaceId must not be null."); + } + + [Fact] + public async Task GetDimDetails_WithValidData_ReturnsExpected() + { + // Arrange + var spaceId = Guid.NewGuid(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetSpaceId(_tenantId)) + .Returns(spaceId); + A.CallTo(() => _cfClient.GetServiceBinding(_tenantName, spaceId, $"{_tenantName}-dim-key01", A._)) + .Returns(dimInstanceId); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + + // Act + var result = await _sut.GetDimDetails(_tenantName, _tenantId, CancellationToken.None); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_APPLICATION); + tenant.DimInstanceId.Should().Be(dimInstanceId); + } + + #endregion + + #region CreateApplication + + [Fact] + public async Task CreateApplication_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns(((Guid?)null, string.Empty, false)); + async Task Act() => await _sut.CreateApplication(_tenantName, _tenantId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task CreateApplication_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var applicationId = Guid.NewGuid().ToString(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns((dimInstanceId, string.Empty, false)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateApplication(A._, A._, _tenantName, A._)) + .Returns(applicationId); + + // Act + var result = await _sut.CreateApplication(_tenantName, _tenantId, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.CreateApplication(A._, A._, _tenantName, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.CREATE_COMPANY_IDENTITY); + tenant.ApplicationId.Should().Be(applicationId); + } + + #endregion + + #region CreateCompanyIdentity + + [Fact] + public async Task CreateCompanyIdentity_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns(((Guid?)null, string.Empty, false)); + async Task Act() => await _sut.CreateCompanyIdentity(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task CreateCompanyIdentity_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetDimInstanceIdAndHostingUrl(_tenantId)) + .Returns((dimInstanceId, "https://example.org/hosting", false)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, false, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.CreateCompanyIdentity(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, A._, A._, _tenantName, false, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.ASSIGN_COMPANY_APPLICATION); + tenant.Did.Should().Be(identityResponse.Did); + tenant.DidDownloadUrl.Should().Be(identityResponse.DownloadUrl); + tenant.CompanyId.Should().Be(identityResponse.CompanyId); + } + + #endregion + + #region AssignCompanyApplication + + [Fact] + public async Task AssignCompanyApplication_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns(((string?)null, (Guid?)null, (Guid?)null)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ApplicationId must always be set here"); + } + + [Fact] + public async Task AssignCompanyApplication_WithNoCompanyId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), (Guid?)null, (Guid?)null)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("CompanyId must always be set here"); + } + + [Fact] + public async Task AssignCompanyApplication_WithNoDimInstanceId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((Guid.NewGuid().ToString(), Guid.NewGuid(), null)); + async Task Act() => await _sut.AssignCompanyApplication(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task AssignCompanyApplication_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var applicationId = Guid.NewGuid().ToString(); + var applicationKey = Guid.NewGuid().ToString(); + var companyId = Guid.NewGuid(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + A.CallTo(() => _tenantRepositories.GetApplicationAndCompanyId(_tenantId)) + .Returns((applicationId, companyId, dimInstanceId)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _dimClient.GetApplication(A._, A._, applicationId, A._)) + .Returns(applicationKey); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, false, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.AssignCompanyApplication(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _dimClient.AssignApplicationToCompany(A._, A._, applicationKey, companyId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SEND_CALLBACK); + tenant.ApplicationKey.Should().Be(applicationKey); + } + + #endregion + + #region SendCallback + + [Fact] + public async Task SendCallback_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", (string?)null, (string?)null, (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DownloadUrl must not be null."); + } + + [Fact] + public async Task SendCallback_WithNoCompanyId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", (string?)null, (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("Did must not be null."); + } + + [Fact] + public async Task SendCallback_WithNoDimInstanceId_ReturnsExpected() + { + // Arrange + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", Guid.NewGuid().ToString(), (Guid?)null)); + async Task Act() => await _sut.SendCallback(_tenantId, _tenantName, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("DimInstanceId must not be null."); + } + + [Fact] + public async Task SendCallback_WithValidData_ReturnsExpected() + { + // Arrange + var serviceCrenentialBinding = _fixture.Create(); + var identityResponse = _fixture.Create(); + var dimInstanceId = Guid.NewGuid(); + var tenant = new Tenant(_tenantId, "test", "Corp", "https://example.org/did", false, _processId, _operatorId); + var did = Guid.NewGuid().ToString(); + A.CallTo(() => _tenantRepositories.GetCallbackData(_tenantId)) + .Returns(("bpn123", "https://example.org/did", did, dimInstanceId)); + A.CallTo(() => _cfClient.GetServiceBindingDetails(dimInstanceId, A._)) + .Returns(serviceCrenentialBinding); + A.CallTo(() => _dimClient.GetDidDocument(A._, A._)) + .Returns(JsonDocument.Parse("{}")); + A.CallTo(() => _tenantRepositories.AttachAndModifyTenant(_tenantId, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(tenant); + modify(tenant); + }); + A.CallTo(() => _dimClient.CreateCompanyIdentity(A._, "https://example.org/hosting", A._, _tenantName, false, A._)) + .Returns(identityResponse); + + // Act + var result = await _sut.SendCallback(_tenantId, _tenantName, CancellationToken.None); + + // Assert + A.CallTo(() => _callbackService.SendCallback("bpn123", A._, A._, did, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().BeNull(); + } + + #endregion +} diff --git a/tests/processes/DimProcess.Library.Tests/Usings.cs b/tests/processes/DimProcess.Library.Tests/Usings.cs new file mode 100644 index 0000000..14669b9 --- /dev/null +++ b/tests/processes/DimProcess.Library.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs new file mode 100644 index 0000000..822aede --- /dev/null +++ b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs @@ -0,0 +1,515 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Collections.Immutable; +using ProcessStepTypeId = Dim.Entities.Enums.ProcessStepTypeId; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Dim.Processes.Library.Tests; + +public class ManualProcessDataExtensionsTests +{ + private readonly IDimRepositories _respositories; + private readonly IProcessStepRepository _processStepRepository; + private readonly string _entityName; + private readonly Func _getProcessEntityName; + private readonly IFixture _fixture; + + public ManualProcessDataExtensionsTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _respositories = A.Fake(); + _processStepRepository = A.Fake(); + + A.CallTo(() => _respositories.GetInstance()) + .Returns(_processStepRepository); + + _entityName = _fixture.Create(); + _getProcessEntityName = () => _entityName; + } + + #region CreateManualProcessData + + [Fact] + public void CreateManualProcessData_ReturnsExpected() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + var stepTypeId = processSteps[2].ProcessStepTypeId; + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + var result = sut.CreateManualProcessData(stepTypeId, _respositories, _getProcessEntityName); + + // Assert + result.Should().NotBeNull().And.BeOfType().And.Match( + data => + data.ProcessStepTypeId == stepTypeId && + data.Process == sut.Process && + data.ProcessSteps.SequenceEqual(sut.ProcessSteps!) && + data.PortalRepositories == _respositories); + } + + [Fact] + public void CreateManualProcessData_WithNullVerifyProcessData_Throws() + { + // Arrange + var sut = (VerifyProcessData?)null; + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName} does not exist"); + } + + [Fact] + public void CreateManualProcessData_WithNullProcess_Throws() + { + // Arrange + var sut = _fixture.Build() + .With(x => x.Process, (Process?)null) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName} is not associated with any process"); + } + + [Fact] + public void CreateManualProcessData_WithLockedProcess_Throws() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = expiryDate }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"process {process.Id} associated with {_entityName} is locked, lock expiry is set to {expiryDate}"); + } + + [Fact] + public void CreateManualProcessData_WithNullProcessSteps_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, (IEnumerable?)null) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("processSteps should never be null here"); + } + + [Fact] + public void CreateManualProcessData_WithInvalidProcessStepStatus_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.DONE, process.Id, x.Now)).ToImmutableArray(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("processSteps should never have any other status than TODO here"); + } + + [Fact] + public void CreateManualProcessData_WithInvalidProcessStepType_Throws() + { + // Arrange + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var processSteps = _fixture.CreateMany<(Guid ProcessStepId, DateTimeOffset Now)>(5).Select(x => new ProcessStep(x.ProcessStepId, _fixture.Create(), Entities.Enums.ProcessStepStatusId.TODO, process.Id, x.Now)).ToImmutableArray(); + var stepTypeId = Enum.GetValues().Except(processSteps.Select(step => step.ProcessStepTypeId)).First(); + + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + var Act = () => sut.CreateManualProcessData(stepTypeId, _respositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be($"{_entityName}, process step {stepTypeId} is not eligible to run"); + } + + #endregion + + #region RequestLock + + [Fact] + public void RequestLock_WithUnLockedProcess_ReturnsExpected() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = null }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .Create(); + + // Act + sut.RequestLock(expiryDate); + + // Assert + sut.Process.LockExpiryDate.Should().Be(expiryDate); + A.CallTo(() => _respositories.Attach(process, null)).MustHaveHappenedOnceExactly(); + } + + [Fact] + public void RequestLock_WithLockedProcess_Throws() + { + // Arrange + var expiryDate = _fixture.Create(); + var process = new Process(Guid.NewGuid(), _fixture.Create(), Guid.NewGuid()) { LockExpiryDate = expiryDate }; + var sut = _fixture.Build() + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .Create(); + + var Act = () => sut.RequestLock(DateTimeOffset.UtcNow); + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("process TryLock should never fail here"); + sut.Process.LockExpiryDate.Should().Be(expiryDate); + A.CallTo(() => _respositories.Attach(process, null)).MustHaveHappenedOnceExactly(); + } + + #endregion + + #region SkipProcessSteps + + [Fact] + public void SkipProcessSteps_ReturnsExpected() + { + // Arrange + var process = _fixture.Create(); + var stepTypeIds = _fixture.CreateMany(4).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps0 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + + var processSteps = new[] + { + processSteps0[0], + processSteps1[0], + processSteps2[0], + processSteps3[0], + processSteps0[1], + processSteps1[1], + processSteps2[1], + processSteps3[1], + processSteps0[2], + processSteps1[2], + processSteps2[2], + processSteps3[2], + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = _fixture.Build() + .With(x => x.ProcessStepTypeId, stepTypeIds[3]) + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + sut.SkipProcessSteps(new ProcessStepTypeId[] { stepTypeIds[1], stepTypeIds[2], stepTypeIds[3] }); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + + modifiedProcessSteps.Should().HaveCount(6).And.Satisfy( + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before + ); + } + + #endregion + + #region SkipProcessStepsExcept + + [Fact] + public void SkipProcessStepsExcept_ReturnsExpected() + { + // Arrange + var process = _fixture.Create(); + var stepTypeIds = _fixture.CreateMany(4).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps0 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], Entities.Enums.ProcessStepStatusId.TODO, process.Id, before) + }; + + var processSteps = new[] + { + processSteps0[0], + processSteps1[0], + processSteps2[0], + processSteps3[0], + processSteps0[1], + processSteps1[1], + processSteps2[1], + processSteps3[1], + processSteps0[2], + processSteps1[2], + processSteps2[2], + processSteps3[2], + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = _fixture.Build() + .With(x => x.ProcessStepTypeId, stepTypeIds[3]) + .With(x => x.Process, process) + .With(x => x.PortalRepositories, _respositories) + .With(x => x.ProcessSteps, processSteps) + .Create(); + + // Act + sut.SkipProcessStepsExcept(new ProcessStepTypeId[] { stepTypeIds[1], stepTypeIds[2] }); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + + modifiedProcessSteps.Should().HaveCount(3).And.Satisfy( + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == Entities.Enums.ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before + ); + } + + #endregion + + #region ScheduleProcessSteps + + [Fact] + public void ScheduleProcessSteps_ReturnsExpected() + { + // Arrange + var stepTypeIds = _fixture.CreateMany(3).ToImmutableArray(); + var now = DateTimeOffset.UtcNow; + + var createdSteps = new List(); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcesssStepTypeId, Entities.Enums.ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatusProcessIds) => + { + foreach (var data in processStepTypeStatusProcessIds) + { + createdSteps.Add(new ProcessStep(Guid.NewGuid(), data.ProcesssStepTypeId, data.ProcessStepStatusId, data.ProcessId, now)); + } + return createdSteps; + }); + + var sut = _fixture.Build() + .With(x => x.PortalRepositories, _respositories) + .Create(); + + // Act + sut.ScheduleProcessSteps(stepTypeIds); + + // Assert + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + createdSteps.Should().HaveCount(3).And.Satisfy( + x => x.ProcessStepTypeId == stepTypeIds[0] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == stepTypeIds[1] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == stepTypeIds[2] && x.ProcessId == sut.Process.Id && x.ProcessStepStatusId == ProcessStepStatusId.TODO + ); + } + + #endregion + + #region FinalizeProcessStep + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FinalizeProcessStep_ReturnsExpected(bool locked) + { + // Arrange + var version = Guid.NewGuid(); + var process = _fixture.Build() + .With(x => x.Version, version) + .With(x => x.LockExpiryDate, locked ? DateTimeOffset.UtcNow : (DateTimeOffset?)null) + .Create(); + var stepTypeIds = _fixture.CreateMany(3).ToImmutableArray(); + var before = DateTimeOffset.UtcNow.AddDays(-1); + var processSteps = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before) + }; + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .Invokes((IEnumerable<(Guid ProcessStepId, Action? Initialize, Action Modify)> processStepIdInitializeModify) => + { + foreach (var (stepId, initialize, modify) in processStepIdInitializeModify) + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + } + }); + + var sut = new ManualProcessStepData(stepTypeIds[1], process, processSteps, _respositories); + + // Act + sut.FinalizeProcessStep(); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _respositories.Attach(process, null)) + .MustHaveHappenedOnceExactly(); + + process.LockExpiryDate.Should().BeNull(); + process.Version.Should().NotBe(version); + modifiedProcessSteps.Should().ContainSingle().Which.Should().Match( + x => x.Id == processSteps[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE && x.DateLastChanged != before + ); + } + + #endregion +} diff --git a/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj b/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj new file mode 100644 index 0000000..21f726b --- /dev/null +++ b/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj @@ -0,0 +1,47 @@ + + + + + net8.0 + enable + enable + false + Dim.Processes.Library.Tests + Dim.Processes.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/tests/processes/Processes.Library.Tests/Usings.cs b/tests/processes/Processes.Library.Tests/Usings.cs new file mode 100644 index 0000000..14669b9 --- /dev/null +++ b/tests/processes/Processes.Library.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs new file mode 100644 index 0000000..0444b7e --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs @@ -0,0 +1,513 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Dim.Tests.Shared; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; + +namespace Processes.Worker.Library.Tests; + +public class ProcessExecutionServiceTests +{ + private readonly IProcessStepRepository _processStepRepository; + private readonly IDimRepositories _repositories; + private readonly IProcessExecutor _processExecutor; + private readonly IMockLogger _mockLogger; + private readonly ProcessExecutionService _service; + private readonly IFixture _fixture; + + public ProcessExecutionServiceTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + var dateTimeProvider = A.Fake(); + _repositories = A.Fake(); + _processStepRepository = A.Fake(); + _processExecutor = A.Fake(); + + _mockLogger = A.Fake>(); + ILogger logger = new MockLogger(_mockLogger); + + A.CallTo(() => _repositories.GetInstance()) + .Returns(_processStepRepository); + + var settings = _fixture.Create(); + + var options = Options.Create(settings); + var serviceProvider = A.Fake(); + A.CallTo(() => serviceProvider.GetService(typeof(IDimRepositories))).Returns(_repositories); + A.CallTo(() => serviceProvider.GetService(typeof(IProcessExecutor))).Returns(_processExecutor); + var serviceScope = A.Fake(); + A.CallTo(() => serviceScope.ServiceProvider).Returns(serviceProvider); + var serviceScopeFactory = A.Fake(); + A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(serviceScope); + A.CallTo(() => serviceProvider.GetService(typeof(IServiceScopeFactory))).Returns(serviceScopeFactory); + + _service = new ProcessExecutionService(serviceScopeFactory, dateTimeProvider, options, logger); + } + + [Fact] + public async Task ExecuteAsync_WithNoPendingItems_NoServiceCall() + { + // Arrange + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(Array.Empty().ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithPendingItems_CallsProcessExpectedNumberOfTimes() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(Enumerable.Repeat(IProcessExecutor.ProcessExecutionResult.SaveRequested, 2).ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_Unmodified() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(Enumerable.Repeat(IProcessExecutor.ProcessExecutionResult.Unmodified, 2).ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLock() + { + // Arrange + var processId = Guid.NewGuid(); + var processVersion = Guid.NewGuid(); + var process = new Process(processId, ProcessTypeId.SETUP_DIM, processVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(processId, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().HaveCount(6) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != processVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[1].LockExpiryTime && x.Save), + forth => forth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[2].Version && x.LockExpiryTime == changeHistory[2].LockExpiryTime && !x.Save), + fifth => fifth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[3].Version && x.LockExpiryTime == null && x.Save), + sixth => sixth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[4].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLockTwice() + { + // Arrange + var processId = Guid.NewGuid(); + var processVersion = Guid.NewGuid(); + var process = new Process(processId, ProcessTypeId.SETUP_DIM, processVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(processId, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(4, Times.Exactly); + changeHistory.Should().HaveCount(7) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != processVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[1].LockExpiryTime && !x.Save), + forth => forth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[2].Version && x.LockExpiryTime == changeHistory[2].LockExpiryTime && x.Save), + fifth => fifth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[3].Version && x.LockExpiryTime == changeHistory[3].LockExpiryTime && !x.Save), + sixth => sixth.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[4].Version && x.LockExpiryTime == null && x.Save), + seventh => seventh.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[5].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLockThenThrows() + { + // Arrange + var firstId = Guid.NewGuid(); + var secondId = Guid.NewGuid(); + var firstVersion = Guid.NewGuid(); + var secondVersion = Guid.NewGuid(); + var firstProcess = new Process(firstId, ProcessTypeId.SETUP_DIM, firstVersion); + var secondProcess = new Process(secondId, ProcessTypeId.SETUP_DIM, secondVersion); + + var processData = new[] { firstProcess, secondProcess }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + IEnumerable ThrowingEnumerable() + { + yield return IProcessExecutor.ProcessExecutionResult.LockRequested; + throw new Exception("normal error"); + } + + Process? process = null; + + A.CallTo(() => _processExecutor.ExecuteProcess(firstId, A._, A._)) + .ReturnsLazily((Guid Id, ProcessTypeId _, CancellationToken _) => + { + process = firstProcess; + return ThrowingEnumerable().ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(secondId, A._, A._)) + .ReturnsLazily((Guid Id, ProcessTypeId _, CancellationToken _) => + { + process = secondProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + var changeHistory = new List<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add(process == null + ? (Guid.Empty, Guid.Empty, null, true) + : (process.Id, process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add(process == null + ? (Guid.Empty, Guid.Empty, null, false) + : (process.Id, process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(firstId, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _processExecutor.ExecuteProcess(secondId, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().SatisfyRespectively( + first => first.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version != firstVersion && x.LockExpiryTime != null && x.Save), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version == changeHistory[0].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == firstId && x.Version == changeHistory[1].Version && x.LockExpiryTime == changeHistory[0].LockExpiryTime && !x.Save), + third => third.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == secondId && x.Version != secondVersion && x.LockExpiryTime == null && x.Save), + forth => forth.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Id == secondId && x.Version == changeHistory[3].Version && x.LockExpiryTime == null && !x.Save) + ); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == "normal error"), A.That.StartsWith("error processing process"))).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_UnmodifiedSafeRequested() + { + // Arrange + var firstVersion = Guid.NewGuid(); + var process = new Process(Guid.NewGuid(), ProcessTypeId.SETUP_DIM, firstVersion); + var processData = new[] { process }; + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Returns(new[] { IProcessExecutor.ProcessExecutionResult.Unmodified, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable()); + + var changeHistory = new List<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappenedTwiceExactly(); + changeHistory.Should().HaveCount(3) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == firstVersion && x.LockExpiryTime == null && !x.Save), + second => second.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version != changeHistory[0].Version && x.LockExpiryTime == null && x.Save), + third => third.Should().Match<(Guid Version, DateTimeOffset? LockExpiryTime, bool Save)>(x => x.Version == changeHistory[1].Version && x.LockExpiryTime == null && !x.Save) + ); + } + + [Fact] + public async Task ExecuteAsync_ExecuteProcess_Returns_RequestLock_SaveAsyncThrows() + { + // Arrange + var firstProcessId = Guid.NewGuid(); + var firstVersion = Guid.NewGuid(); + var firstProcess = new Process(firstProcessId, ProcessTypeId.SETUP_DIM, firstVersion); + var secondProcessId = Guid.NewGuid(); + var secondVersion = Guid.NewGuid(); + var secondProcess = new Process(secondProcessId, ProcessTypeId.SETUP_DIM, secondVersion); + var thirdProcessId = Guid.NewGuid(); + var thirdVersion = Guid.NewGuid(); + var thirdProcess = new Process(thirdProcessId, ProcessTypeId.SETUP_DIM, thirdVersion); + var processData = new[] { firstProcess, secondProcess, thirdProcess }; + var error = new Exception("save conflict error"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + Process? process = null; + + A.CallTo(() => _processExecutor.ExecuteProcess(firstProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = firstProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.LockRequested, IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(secondProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = secondProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.SaveRequested }.ToAsyncEnumerable(); + }); + + A.CallTo(() => _processExecutor.ExecuteProcess(thirdProcessId, A._, A._)) + .ReturnsLazily((Guid _, ProcessTypeId _, CancellationToken _) => + { + process = thirdProcess; + return new[] { IProcessExecutor.ProcessExecutionResult.Unmodified }.ToAsyncEnumerable(); + }); + + var changeHistory = new List<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(); + + A.CallTo(() => _repositories.SaveAsync()) + .Throws(error); + + A.CallTo(() => _repositories.Clear()) + .Invokes(() => + { + changeHistory.Add( + process == null + ? (Guid.Empty, Guid.Empty, null) + : (process.Id, process.Version, process.LockExpiryDate)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _repositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _repositories.Clear()) + .MustHaveHappened(3, Times.Exactly); + changeHistory.Should().HaveCount(3) + .And.SatisfyRespectively( + first => first.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == firstProcessId && x.Version != firstVersion && x.LockExpiryTime != null), + second => second.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == secondProcessId && x.Version != secondVersion && x.LockExpiryTime == null), + third => third.Should().Match<(Guid Id, Guid Version, DateTimeOffset? LockExpiryTime)>(x => x.Id == thirdProcessId && x.Version == thirdVersion && x.LockExpiryTime == null) + ); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {firstProcessId}"))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {secondProcessId}"))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith($"error processing process {thirdProcessId}"))) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_IgnoresLockedProcesses_LogsInformation() + { + var lockExpiryDate = _fixture.Create(); + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid()) { LockExpiryDate = lockExpiryDate }).ToImmutableArray(); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.StartsWith("skipping locked process"))) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _repositories.Clear()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithException_LogsError() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + var error = new Exception("Only a test"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Throws(error); + + Environment.ExitCode = 0; + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + Environment.ExitCode.Should().Be(0); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("start processing process")))).MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.Matches(e => e != null && e.Message == error.Message), A.That.StartsWith("error processing process"))).MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("finished processing process")))).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Error, A._, A._)).MustNotHaveHappened(); + A.CallTo(() => _repositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithSystemException_Exits() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.SETUP_DIM, Guid.NewGuid())).ToImmutableArray(); + var error = new SystemException("unrecoverable failure"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo(() => _processExecutor.ExecuteProcess(A._, A._, A._)) + .Throws(error); + + Environment.ExitCode = 0; + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + Environment.ExitCode.Should().Be(1); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("start processing process")))).MustHaveHappenedOnceExactly(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNotNull(), A._)).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A.That.IsNull(), A.That.Matches(x => x.StartsWith("finished processing process")))).MustNotHaveHappened(); + A.CallTo(() => _mockLogger.Log(LogLevel.Error, A.That.Matches(e => e != null && e.Message == error.Message), $"processing failed with following Exception {error.Message}")).MustHaveHappenedOnceExactly(); + A.CallTo(() => _repositories.SaveAsync()).MustNotHaveHappened(); + } +} diff --git a/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs new file mode 100644 index 0000000..d757aa3 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs @@ -0,0 +1,880 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Dim.DbAccess; +using Dim.DbAccess.Repositories; +using Dim.Entities.Entities; +using Dim.Entities.Enums; +using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Processes.Worker.Library; +using System.Collections.Immutable; +using ProcessTypeId = Dim.Entities.Enums.ProcessTypeId; + +namespace Processes.Worker.Library.Tests; + +public class ProcessExecutorTests +{ + private readonly IProcessTypeExecutor _processTypeExecutor; + private readonly IProcessStepRepository _processStepRepository; + private readonly IProcessExecutor _sut; + private readonly IFixture _fixture; + + public ProcessExecutorTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _processTypeExecutor = A.Fake(); + _processStepRepository = A.Fake(); + + var dimRepositories = A.Fake(); + var logger = A.Fake>(); + + A.CallTo(() => dimRepositories.GetInstance()) + .Returns(_processStepRepository); + + A.CallTo(() => _processTypeExecutor.GetProcessTypeId()) + .Returns(ProcessTypeId.SETUP_DIM); + + _sut = new ProcessExecutor( + new[] { _processTypeExecutor }, + dimRepositories, + logger); + } + + #region GetRegisteredProcessTypeIds + + [Fact] + public void GetRegisteredProcessTypeIds_ReturnsExpected() + { + // Act + var result = _sut.GetRegisteredProcessTypeIds(); + + // Assert + result.Should().HaveCount(1).And.Contain(ProcessTypeId.SETUP_DIM); + } + + #endregion + + #region ExecuteProcess + + [Fact] + public async Task ExecuteProcess_WithInvalidProcessTypeId_Throws() + { + // Arrange + var Act = async () => await _sut.ExecuteProcess(Guid.NewGuid(), (ProcessTypeId)default, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Act + var result = await Assert.ThrowsAsync(Act); + + // Assert + result.Message.Should().Be($"processType {(ProcessTypeId)default} is not a registered executable processType."); + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithInitialSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepTypeId = _fixture.Create(); + var processStepData = (Id: Guid.NewGuid(), processStepTypeId); + var initialStepTypeIds = Enum.GetValues().Where(x => x != processStepTypeId).Take(3).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, initialStepTypeIds)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveSameCount(initialStepTypeIds) + .And.Satisfy( + x => x.ProcessStepTypeId == initialStepTypeIds[0] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == initialStepTypeIds[1] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == initialStepTypeIds[2] && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(initialStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(initialStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(1).Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == createdProcessSteps!.ElementAt(2).Id && x.ProcessStepStatusId == stepStatusId); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_NoScheduleOrSkippedSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + modifiedProcessSteps + .Should().HaveSameCount(processStepData) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == stepStatusId, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == stepStatusId); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Fact] + public async Task ExecuteProcess_NoExecutableSteps_ReturnsExpected() + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany().Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(false); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveCount(1).And.Contain(IProcessExecutor.ProcessExecutionResult.Unmodified); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_NoScheduleOrSkippedSteps_SingleStepTypeWithDuplicates_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var stepTypeId = _fixture.Create(); + var processStepData = _fixture.CreateMany(3).Select(x => (Id: x, StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + modifiedProcessSteps + .Should().HaveSameCount(processStepData) + .And.Satisfy( + x => x.Id == processStepData[0].Id, + x => x.Id == processStepData[1].Id, + x => x.Id == processStepData[2].Id) + .And.Satisfy( + x => x.ProcessStepStatusId == stepStatusId, + x => x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE, + x => x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithScheduledSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = (Id: Guid.NewGuid(), StepTypeId: _fixture.Create()); + var scheduleStepTypeIds = Enum.GetValues().Where(x => x != processStepData.StepTypeId).Take(3).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(processStepData.StepTypeId, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, scheduleStepTypeIds, null, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A.That.Not.IsEqualTo(processStepData.StepTypeId), A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result. + Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(scheduleStepTypeIds.Length + 1, Times.Exactly); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveSameCount(scheduleStepTypeIds) + .And.Satisfy( + x => x.ProcessStepTypeId == scheduleStepTypeIds[0] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == scheduleStepTypeIds[1] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessStepTypeId == scheduleStepTypeIds[2] && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(scheduleStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(scheduleStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(1).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(2).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified + })] + public async Task ExecuteProcess_WithDuplicateScheduledSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var stepTypeId = _fixture.Create(); + var processStepData = (Id: Guid.NewGuid(), StepTypeId: stepTypeId); + var scheduleStepTypeIds = Enumerable.Repeat(stepTypeId, 3); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(new[] { processStepData }.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(stepTypeId, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, scheduleStepTypeIds, null, null)) + .Once() + .Then + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, null, null)); + + IEnumerable? createdProcessSteps = null; + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .ReturnsLazily((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> processStepTypeStatus) => + { + createdProcessSteps = processStepTypeStatus.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow)).ToImmutableList(); + return createdProcessSteps; + }); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + // Assert + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + + createdProcessSteps + .Should().NotBeNull() + .And.HaveCount(1) + .And.Satisfy( + x => x.ProcessStepTypeId == stepTypeId && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(2, Times.Exactly); + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(2, Times.Exactly); + + modifiedProcessSteps + .Should().HaveCount(2) + .And.Satisfy( + x => x.Id == processStepData.Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == createdProcessSteps!.ElementAt(0).Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + } + } + + [Theory] + [InlineData(ProcessStepStatusId.DONE, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.DONE, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(ProcessStepStatusId.TODO, false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + public async Task ExecuteProcess_WithSkippedSteps_ReturnsExpected(ProcessStepStatusId stepStatusId, bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var skipStepTypeIds = processStepData.Skip(1).Select(x => x.StepTypeId).ToImmutableArray(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Returns(new IProcessTypeExecutor.StepExecutionResult(false, stepStatusId, null, skipStepTypeIds, null)); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + if (stepStatusId == ProcessStepStatusId.DONE) + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(skipStepTypeIds.Length + 1, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(skipStepTypeIds.Length + 1) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == ProcessStepStatusId.DONE, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED); + } + else + { + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(skipStepTypeIds.Length, Times.Exactly); + modifiedProcessSteps + .Should().HaveCount(skipStepTypeIds.Length) + .And.Satisfy( + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED); + } + } + + [Theory] + [InlineData(true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.LockRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + [InlineData(false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested, + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.SaveRequested + })] + public async Task ExecuteProcess_ProcessThrowsTestException_ReturnsExpected(bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany(3).Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var error = _fixture.Create(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Throws(error); + + var modifiedProcessSteps = new List(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .Invokes((Guid stepId, Action? initialize, Action modify) => + { + var step = new ProcessStep(stepId, default, default, Guid.Empty, default); + initialize?.Invoke(step); + modify(step); + modifiedProcessSteps.Add(step); + }); + + // Act + var result = await _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ToListAsync(); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustHaveHappened(processStepData.Length, Times.Exactly); + + modifiedProcessSteps + .Should().HaveCount(processStepData.Length) + .And.Satisfy( + x => x.Id == processStepData[0].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED, + x => x.Id == processStepData[1].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED, + x => x.Id == processStepData[2].Id && x.ProcessStepStatusId == ProcessStepStatusId.FAILED); + } + + [Theory] + [InlineData(true, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + IProcessExecutor.ProcessExecutionResult.LockRequested, + })] + [InlineData(false, new[] { + IProcessExecutor.ProcessExecutionResult.Unmodified, + })] + public async Task ExecuteProcess_ProcessThrowsSystemException_Throws(bool isLockRequested, IEnumerable executionResults) + { + // Arrange + var processId = Guid.NewGuid(); + var processStepData = _fixture.CreateMany().Select(stepTypeId => (Id: Guid.NewGuid(), StepTypeId: stepTypeId)).OrderBy(x => x.StepTypeId).ToImmutableArray(); + var error = _fixture.Create(); + + A.CallTo(() => _processStepRepository.GetProcessStepData(processId)) + .Returns(processStepData.ToAsyncEnumerable()); + + A.CallTo(() => _processTypeExecutor.IsExecutableStepTypeId(A._)) + .Returns(true); + + A.CallTo(() => _processTypeExecutor.IsLockRequested(A._)) + .Returns(isLockRequested); + + A.CallTo(() => _processTypeExecutor.InitializeProcess(processId, A>._)) + .Returns(new IProcessTypeExecutor.InitializationResult(false, null)); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .Throws(error); + + var stepResults = new List(); + + var Act = async () => + { + await foreach (var stepResult in _sut.ExecuteProcess(processId, ProcessTypeId.SETUP_DIM, CancellationToken.None).ConfigureAwait(false)) + { + stepResults.Add(stepResult); + } + }; + + // Act + var result = await Assert.ThrowsAsync(Act); + + // Assert + stepResults.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + result.Message.Should().Be(error.Message); + + A.CallTo(() => _processTypeExecutor.ExecuteProcessStep(A._, A>._, A._)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => _processStepRepository.AttachAndModifyProcessStep(A._, A>._, A>._)) + .MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustNotHaveHappened(); + } + + #endregion + + [Serializable] + public class TestException : Exception + { + public TestException() { } + public TestException(string message) : base(message) { } + public TestException(string message, Exception inner) : base(message, inner) { } + protected TestException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} diff --git a/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj b/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj new file mode 100644 index 0000000..bd508a3 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj @@ -0,0 +1,49 @@ + + + + + net8.0 + enable + enable + false + Processes.Worker.Library.Tests + Processes.Worker.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/processes/Processes.Worker.Library.Tests/Usings.cs b/tests/processes/Processes.Worker.Library.Tests/Usings.cs new file mode 100644 index 0000000..14669b9 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +global using AutoFixture; +global using AutoFixture.AutoFakeItEasy; +global using FakeItEasy; +global using FluentAssertions; +global using Xunit; diff --git a/tests/shared/Tests.Shared/MockLogger.cs b/tests/shared/Tests.Shared/MockLogger.cs new file mode 100644 index 0000000..6e41813 --- /dev/null +++ b/tests/shared/Tests.Shared/MockLogger.cs @@ -0,0 +1,52 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.Extensions.Logging; + +namespace Dim.Tests.Shared; + +public interface IMockLogger +{ + void Log(LogLevel logLevel, Exception? exception, string logMessage); +} + +public class MockLogger : ILogger +{ + private readonly IMockLogger _logger; + + public MockLogger(IMockLogger logger) + { + _logger = logger; + } + + public IDisposable? BeginScope(TState state) where TState : notnull => new TestDisposable(); + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) => + _logger.Log(logLevel, exception, formatter(state, exception)); + + public class TestDisposable : IDisposable + { + public void Dispose() + { + GC.SuppressFinalize(this); + } + } +} diff --git a/tests/shared/Tests.Shared/Tests.Shared.csproj b/tests/shared/Tests.Shared/Tests.Shared.csproj new file mode 100644 index 0000000..966d94a --- /dev/null +++ b/tests/shared/Tests.Shared/Tests.Shared.csproj @@ -0,0 +1,35 @@ + + + + + + Dim.Tests.Shared + Dim.Tests.Shared + net7.0 + enable + enable + + + + + + + +