From d5e30ad2c99ffba44c70c9b51f7a0813621e7362 Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Wed, 6 Mar 2024 18:00:36 +0100 Subject: [PATCH] feat: add issuer component Refs: #2 --- DEPENDENCIES | 57 + FILEHEADER.md | 2 +- README.md | 2 +- src/Directory.Build.props | 25 + src/SsiCredentialIssuer.sln | 177 +++ .../ExpiryCheckServiceExtensions.cs | 47 + .../ExpiryCheckServiceSettings.cs | 40 + .../ExpiryCheckService.cs | 196 +++ .../SsiCredentialIssuer.Expiry.App/Program.cs | 68 + .../Properties/launchSettings.json | 11 + .../SsiCredentialIssuer.Expiry.App.csproj | 79 + .../appsettings.json | 57 + .../Extensions/MediaTypeIdExtensions.cs | 65 + .../IIssuerRepositories.cs | 38 + .../IssuerRepositories.cs | 78 + ...uerRepositoriesStartupServiceExtensions.cs | 42 + .../Models/CompanySsiDetailSorting.cs | 38 + .../Models/CredentialDetailData.cs | 34 + .../Models/CredentialExpiryData.cs | 40 + .../Models/DocumentData.cs | 28 + .../Models/EncryptionTransformationData.cs | 26 + .../Models/ExternalTypeDetailData.cs | 31 + .../Models/SsiApprovalData.cs | 37 + .../Models/SsiCertificateTransferData.cs | 34 + .../Models/TechnicalUserDetails.cs | 33 + .../UseCaseParticipationTransferData.cs | 44 + .../CompanySsiDetailsRepository.cs | 285 ++++ .../Repositories/CredentialRepository.cs | 68 + .../Repositories/DocumentRepository.cs | 63 + .../ICompanySsiDetailsRepository.cs | 99 ++ .../Repositories/ICredentialRepository.cs | 33 + .../Repositories/IDocumentRepository.cs | 43 + .../Repositories/IProcessStepRepository.cs | 37 + .../Repositories/ProcessStepRepository.cs | 100 ++ .../SsiCredentialIssuer.DbAccess.csproj | 42 + .../AuditCompanySsiDetail20240228.cs | 59 + .../AuditEntities/AuditDocument20240305.cs | 63 + .../Attributes/AuditEntityV1Attribute.cs | 43 + .../AuditInsertEditorV1Attribute.cs | 34 + .../Attributes/LastChangedV1Attribute.cs | 32 + .../Attributes/LastEditorV1Attribute.cs | 32 + .../AuditingDependencyInjection.cs | 33 + .../Auditing/Enums/AuditOperationId.cs | 41 + .../Auditing/Enums/AuditPropertyV1Names.cs | 28 + .../Auditing/Extensions/AuditExtensions.cs | 86 + .../EntityTypeBuilderV1Extension.cs | 129 ++ .../Auditing/Handler/AuditHandlerV1.cs | 109 ++ .../Auditing/Handler/IAuditHandler.cs | 28 + .../Auditing/IAuditEntityV1.cs | 52 + .../Auditing/IAuditableV1.cs | 31 + .../Auditing/IBaseEntity.cs | 28 + .../Auditing/Identity/IIdentityIdService.cs | 25 + .../Entities/CompanySsiDetail.cs | 75 + .../CompanySsiDetailAssignedDocument.cs | 16 + .../Entities/CompanySsiDetailStatus.cs | 47 + .../Entities/CompanySsiProcessData.cs | 50 + .../Entities/Document.cs | 83 + .../Entities/DocumentStatus.cs | 46 + .../Entities/DocumentType.cs | 46 + .../Entities/ExpiryCheckType.cs | 49 + .../Entities/MediaType.cs | 46 + .../Entities/Process.cs | 55 + .../Entities/ProcessStep.cs | 56 + .../Entities/ProcessStepStatus.cs | 46 + .../Entities/ProcessStepType.cs | 46 + .../Entities/ProcessType.cs | 46 + .../Entities/UseCase.cs | 50 + .../VerifiedCredentialExternalType.cs | 43 + ...fiedCredentialExternalTypeDetailVersion.cs | 41 + .../Entities/VerifiedCredentialType.cs | 52 + ...ifiedCredentialTypeAssignedExternalType.cs | 46 + .../VerifiedCredentialTypeAssignedKind.cs | 46 + .../VerifiedCredentialTypeAssignedUseCase.cs | 38 + .../Entities/VerifiedCredentialTypeKind.cs | 50 + .../Entities/VerifyProcessData.cs | 25 + .../Enums/CompanySsiDetailStatusId.cs | 27 + .../Enums/DocumentStatusId.cs | 36 + .../Enums/DocumentTypeId.cs | 27 + .../Enums/ExpiryCheckTypeId.cs | 27 + .../Enums/MediaTypeId.cs | 35 + .../Enums/ProcessStepStatusId.cs | 29 + .../Enums/ProcessStepTypeId.cs | 29 + .../Enums/ProcessTypeId.cs | 25 + .../Enums/VerifiedCredentialExternalTypeId.cs | 46 + .../Enums/VerifiedCredentialTypeId.cs | 46 + .../Enums/VerifiedCredentialTypeKindId.cs | 27 + .../IssuerDbContext.cs | 318 ++++ .../SsiCredentialIssuer.Entities.csproj | 46 + .../20240308143713_0.1.0-rc.1.Designer.cs | 1403 +++++++++++++++++ .../Migrations/20240308143713_0.1.0-rc.1.cs | 946 +++++++++++ .../IssuerDbContextModelSnapshot.cs | 1395 ++++++++++++++++ .../SsiCredentialIssuer.Migrations/Program.cs | 60 + .../Properties/launchSettings.json | 10 + .../Seeder/BatchInsertSeeder.cs | 108 ++ .../Seeder/BatchUpdateSeeder.cs | 103 ++ .../Seeder/Data/use_cases.json | 57 + ...ternal_type_detail_versions.consortia.json | 42 + ...dential_external_type_detail_versions.json | 26 + ...edential_type_assigned_external_types.json | 26 + ...rified_credential_type_assigned_kinds.json | 30 + ...ed_credential_type_assigned_use_cases.json | 22 + .../SsiCredentialIssuer.Migrations.csproj | 81 + .../appsettings.json | 36 + .../DependencyInjection/PortalSettings.cs | 31 + .../ServiceCollectionExtensions.cs | 46 + .../Portal.Service/Models/MailData.cs | 28 + .../Models/NotificationRequest.cs | 28 + .../Models/NotificationTypeId.cs | 156 ++ .../Portal.Service/Portal.Service.csproj | 35 + .../Portal.Service/Services/IPortalService.cs | 28 + .../Portal.Service/Services/PortalService.cs | 60 + .../BusinessLogic/IWalletBusinessLogic.cs | 32 + .../BusinessLogic/WalletBusinessLogic.cs | 118 ++ .../ServiceCollectionExtensions.cs | 49 + .../DependencyInjection/WalletSettings.cs | 38 + .../Models/CreateCredentialResponse.cs | 36 + .../Wallet.Service/Models/CredentialData.cs | 31 + .../Models/DeriveCredentialData.cs | 35 + .../Models/EncryptionInformation.cs | 26 + .../Models/GetCredentialResponse.cs | 10 + .../Models/SignCredentialRequest.cs | 39 + .../Schemas/BPNCredential.schema.json | 59 + .../Schemas/FRAMEWORKCredential.schema.json | 65 + .../Schemas/MEMBERSHIPCredential.schema.json | 59 + .../Services/BasicAuthSettings.cs | 36 + .../Services/BasicAuthTokenService.cs | 72 + .../Services/IBasicAuthTokenService.cs | 25 + .../Wallet.Service/Services/IWalletService.cs | 30 + .../Wallet.Service/Services/WalletService.cs | 116 ++ .../Wallet.Service/Wallet.Service.csproj | 67 + .../Authentication/CustomClaimTypes.cs | 28 + .../KeycloakClaimsTransformation.cs | 73 + .../BusinessLogic/CredentialBusinessLogic.cs | 455 ++++++ .../BusinessLogic/CredentialSettings.cs | 62 + .../BusinessLogic/ICredentialBusinessLogic.cs | 43 + .../Controllers/CredentialController.cs | 178 +++ .../CredentialServiceCollectionExtensions.cs | 32 + .../CompanyDataErrorMessageContainer.cs | 88 ++ .../RouteHandlerBuilderExtensions.cs | 48 + .../Identity/ClaimTypes.cs | 26 + .../Identity/ClaimsIdentityDataBuilder.cs | 43 + .../Identity/ClaimsIdentityService.cs | 36 + ...aimsIdentityServiceCollectionExtensions.cs | 35 + .../Identity/IClaimsIdentityDataBuilder.cs | 35 + .../Identity/IIdentityData.cs | 26 + .../Identity/IIdentityService.cs | 25 + .../Identity/IdentityIdService.cs | 47 + .../Identity/MandatoryIdentityClaimHandler.cs | 99 ++ .../Identity/PolicyTypes.cs | 32 + .../Models/Constants.cs | 25 + .../Models/CreateBpnCredentialRequest.cs | 45 + .../Models/CredentialData.cs | 76 + .../Models/DidDocument.cs | 26 + .../UseCaseParticipationCreationData.cs | 36 + .../Models/UseCaseParticipationData.cs | 51 + .../SsiCredentialIssuer.Service/Program.cs | 64 + .../Properties/launchSettings.json | 40 + .../SsiCredentialIssuer.Service.csproj | 73 + .../appsettings.json | 69 + .../CredentialProcess.Library.csproj | 35 + .../CredentialProcessHandler.cs | 116 ++ .../CredentialHandlerExtensions.cs | 38 + .../ICredentialProcessHandler.cs | 30 + .../CredentialProcess.Worker.csproj | 35 + .../CredentialProcessTypeExecutor.cs | 112 ++ .../CredentialProcessCollectionExtensions.cs | 35 + .../ManualProcessStepData.cs | 31 + .../ManualProcessStepDataExtensions.cs | 131 ++ .../Processes.Library.csproj | 40 + .../IProcessExecutor.cs | 35 + .../IProcessTypeExecutor.cs | 46 + .../ProcessExecutionService.cs | 158 ++ .../ProcessExecutionServiceExtensions.cs | 39 + .../ProcessExecutionServiceSettings.cs | 31 + .../ProcessExecutor.cs | 213 +++ .../ProcessIdentityIdService.cs | 16 + .../Processes.Worker.Library.csproj | 45 + .../Processes.Worker/Processes.Worker.csproj | 55 + src/processes/Processes.Worker/Program.cs | 70 + .../Properties/launchSettings.json | 10 + .../Processes.Worker/appsettings.json | 59 + src/settings-coverage.xml | 114 ++ .../Tests.Shared/Extensions/HttpExtensions.cs | 52 + tests/Tests.Shared/FakeIdentity.cs | 28 + tests/Tests.Shared/FakeIdentityIdService.cs | 29 + tests/Tests.Shared/FakeIdentityService.cs | 28 + tests/Tests.Shared/HttpMessageHandlerMock.cs | 61 + tests/Tests.Shared/NoAuditHandler.cs | 33 + tests/Tests.Shared/Tests.Shared.csproj | 33 + .../ExpiryCheckServiceTests.cs | 193 +++ .../GlobalUsings.cs | 23 + ...siCredentialIssuer.Expiry.App.Tests.csproj | 55 + .../CompanySsiDetailsRepositoryTests.cs | 588 +++++++ .../ContentTypeMapperExtensionsTests.cs | 83 + .../CredentialRepositoryTests.cs | 148 ++ .../DocumentRepositoryTests.cs | 116 ++ .../IssuerDbContextTests.cs | 117 ++ .../IssuerRepositoriesTests.cs | 168 ++ .../ProcessStepRepositoryTests.cs | 359 +++++ .../Seeder/Data/company_ssi_details.test.json | 88 ++ .../Data/company_ssi_process_datas.test.json | 12 + .../Seeder/Data/documents.test.json | 79 + .../Seeder/Data/process_steps.test.json | 26 + .../Seeder/Data/processes.test.json | 8 + ...al_external_type_detail_versions.test.json | 34 + ...edential_type_assigned_use_cases.test.json | 10 + .../Setup/TestDbFixture.cs | 114 ++ .../SsiCredentialIssuer.DbAccess.Tests.csproj | 67 + .../Portal.Service.Tests.csproj | 60 + .../PortalServiceTests.cs | 178 +++ .../BusinessLogic/WalletBusinessLogicTests.cs | 236 +++ .../Services/BasicAuthTokenServiceTests.cs | 127 ++ .../Services/WalletServiceTests.cs | 274 ++++ .../Wallet.Service.Tests.csproj | 61 + .../KeycloakClaimsTransformationTests.cs | 97 ++ .../CredentialBusinessLogicTests.cs | 694 ++++++++ .../Controllers/CredentialControllerTests.cs | 77 + .../MandatoryIdentityClaimHandlerTests.cs | 128 ++ .../Setup/AsyncEnumerableStub.cs | 41 + .../Setup/AsyncEnumeratorStub.cs | 43 + .../Setup/AsyncQueryProviderStub.cs | 70 + .../Setup/FakePolicyEvaluator.cs | 36 + .../Setup/IntegrationTestFactory.cs | 139 ++ .../SsiCredentialIssuer.Service.Tests.csproj | 67 + .../Usings.cs | 24 + .../appsettings.IntegrationTests.json | 39 + .../CredentialProcess.Library.Tests.csproj | 48 + .../CredentialProcessHandlerTests.cs | 245 +++ .../CredentialProcess.Worker.Tests.csproj | 47 + .../CredentialProcessTypeExecutorTests.cs | 215 +++ .../ManualProcessDataExtensionsTests.cs | 454 ++++++ .../Processes.Library.Tests.csproj | 47 + .../Processes.Library.Tests/Usings.cs | 24 + .../MockLogger.cs | 52 + .../ProcessExecutionServiceTests.cs | 513 ++++++ .../ProcessExecutorTests.cs | 881 +++++++++++ .../Processes.Worker.Library.Tests.csproj | 47 + .../Processes.Worker.Library.Tests/Usings.cs | 25 + 238 files changed, 21417 insertions(+), 2 deletions(-) create mode 100644 src/Directory.Build.props create mode 100644 src/SsiCredentialIssuer.sln create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceExtensions.cs create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceSettings.cs create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/Program.cs create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/Properties/launchSettings.json create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/SsiCredentialIssuer.Expiry.App.csproj create mode 100644 src/credentials/SsiCredentialIssuer.Expiry.App/appsettings.json create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Extensions/MediaTypeIdExtensions.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/IIssuerRepositories.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/IssuerRepositories.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/IssuerRepositoriesStartupServiceExtensions.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiDetailSorting.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/CredentialDetailData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/CredentialExpiryData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/EncryptionTransformationData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/ExternalTypeDetailData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/SsiCertificateTransferData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/TechnicalUserDetails.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Models/UseCaseParticipationTransferData.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/IProcessStepRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/Repositories/ProcessStepRepository.cs create mode 100644 src/database/SsiCredentialIssuer.DbAccess/SsiCredentialIssuer.DbAccess.csproj create mode 100644 src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditCompanySsiDetail20240228.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditDocument20240305.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditEntityV1Attribute.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditInsertEditorV1Attribute.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastChangedV1Attribute.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastEditorV1Attribute.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/DependencyInjection/AuditingDependencyInjection.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditOperationId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditPropertyV1Names.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/AuditExtensions.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/EntityTypeBuilderV1Extension.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Handler/AuditHandlerV1.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Handler/IAuditHandler.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/IAuditEntityV1.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/IAuditableV1.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/IBaseEntity.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Auditing/Identity/IIdentityIdService.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetail.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailAssignedDocument.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailStatus.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiProcessData.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/Document.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/DocumentStatus.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/DocumentType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/ExpiryCheckType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/MediaType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/Process.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/ProcessStep.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/ProcessStepStatus.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/ProcessStepType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/ProcessType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/UseCase.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalTypeDetailVersion.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedExternalType.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedKind.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedUseCase.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeKind.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Entities/VerifyProcessData.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/CompanySsiDetailStatusId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/DocumentStatusId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/DocumentTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/ExpiryCheckTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/MediaTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepStatusId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialExternalTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeKindId.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/IssuerDbContext.cs create mode 100644 src/database/SsiCredentialIssuer.Entities/SsiCredentialIssuer.Entities.csproj create mode 100644 src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.Designer.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Migrations/IssuerDbContextModelSnapshot.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Program.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Properties/launchSettings.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/BatchUpdateSeeder.cs create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/use_cases.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.consortia.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_external_types.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_kinds.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_use_cases.json create mode 100644 src/database/SsiCredentialIssuer.Migrations/SsiCredentialIssuer.Migrations.csproj create mode 100644 src/database/SsiCredentialIssuer.Migrations/appsettings.json create mode 100644 src/externalservices/Portal.Service/DependencyInjection/PortalSettings.cs create mode 100644 src/externalservices/Portal.Service/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/externalservices/Portal.Service/Models/MailData.cs create mode 100644 src/externalservices/Portal.Service/Models/NotificationRequest.cs create mode 100644 src/externalservices/Portal.Service/Models/NotificationTypeId.cs create mode 100644 src/externalservices/Portal.Service/Portal.Service.csproj create mode 100644 src/externalservices/Portal.Service/Services/IPortalService.cs create mode 100644 src/externalservices/Portal.Service/Services/PortalService.cs create mode 100644 src/externalservices/Wallet.Service/BusinessLogic/IWalletBusinessLogic.cs create mode 100644 src/externalservices/Wallet.Service/BusinessLogic/WalletBusinessLogic.cs create mode 100644 src/externalservices/Wallet.Service/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 src/externalservices/Wallet.Service/DependencyInjection/WalletSettings.cs create mode 100644 src/externalservices/Wallet.Service/Models/CreateCredentialResponse.cs create mode 100644 src/externalservices/Wallet.Service/Models/CredentialData.cs create mode 100644 src/externalservices/Wallet.Service/Models/DeriveCredentialData.cs create mode 100644 src/externalservices/Wallet.Service/Models/EncryptionInformation.cs create mode 100644 src/externalservices/Wallet.Service/Models/GetCredentialResponse.cs create mode 100644 src/externalservices/Wallet.Service/Models/SignCredentialRequest.cs create mode 100644 src/externalservices/Wallet.Service/Schemas/BPNCredential.schema.json create mode 100644 src/externalservices/Wallet.Service/Schemas/FRAMEWORKCredential.schema.json create mode 100644 src/externalservices/Wallet.Service/Schemas/MEMBERSHIPCredential.schema.json create mode 100644 src/externalservices/Wallet.Service/Services/BasicAuthSettings.cs create mode 100644 src/externalservices/Wallet.Service/Services/BasicAuthTokenService.cs create mode 100644 src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs create mode 100644 src/externalservices/Wallet.Service/Services/IWalletService.cs create mode 100644 src/externalservices/Wallet.Service/Services/WalletService.cs create mode 100644 src/externalservices/Wallet.Service/Wallet.Service.csproj create mode 100644 src/issuer/SsiCredentialIssuer.Service/Authentication/CustomClaimTypes.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Authentication/KeycloakClaimsTransformation.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialSettings.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/DependencyInjection/CredentialServiceCollectionExtensions.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CompanyDataErrorMessageContainer.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Extensions/RouteHandlerBuilderExtensions.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/ClaimTypes.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityDataBuilder.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityService.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityServiceCollectionExtensions.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/IClaimsIdentityDataBuilder.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityData.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityService.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/IdentityIdService.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Identity/PolicyTypes.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/Constants.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/CreateBpnCredentialRequest.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/CredentialData.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/DidDocument.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationCreationData.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationData.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Program.cs create mode 100644 src/issuer/SsiCredentialIssuer.Service/Properties/launchSettings.json create mode 100644 src/issuer/SsiCredentialIssuer.Service/SsiCredentialIssuer.Service.csproj create mode 100644 src/issuer/SsiCredentialIssuer.Service/appsettings.json create mode 100644 src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj create mode 100644 src/processes/CredentialProcess.Library/CredentialProcessHandler.cs create mode 100644 src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs create mode 100644 src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs create mode 100644 src/processes/CredentialProcess.Worker/CredentialProcess.Worker.csproj create mode 100644 src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs create mode 100644 src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs create mode 100644 src/processes/Processes.Library/ManualProcessStepData.cs create mode 100644 src/processes/Processes.Library/ManualProcessStepDataExtensions.cs create mode 100644 src/processes/Processes.Library/Processes.Library.csproj create mode 100644 src/processes/Processes.Worker.Library/IProcessExecutor.cs create mode 100644 src/processes/Processes.Worker.Library/IProcessTypeExecutor.cs create mode 100644 src/processes/Processes.Worker.Library/ProcessExecutionService.cs create mode 100644 src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs create mode 100644 src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.cs create mode 100644 src/processes/Processes.Worker.Library/ProcessExecutor.cs create mode 100644 src/processes/Processes.Worker.Library/ProcessIdentityIdService.cs create mode 100644 src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj create mode 100644 src/processes/Processes.Worker/Processes.Worker.csproj create mode 100644 src/processes/Processes.Worker/Program.cs create mode 100644 src/processes/Processes.Worker/Properties/launchSettings.json create mode 100644 src/processes/Processes.Worker/appsettings.json create mode 100644 src/settings-coverage.xml create mode 100644 tests/Tests.Shared/Extensions/HttpExtensions.cs create mode 100644 tests/Tests.Shared/FakeIdentity.cs create mode 100644 tests/Tests.Shared/FakeIdentityIdService.cs create mode 100644 tests/Tests.Shared/FakeIdentityService.cs create mode 100644 tests/Tests.Shared/HttpMessageHandlerMock.cs create mode 100644 tests/Tests.Shared/NoAuditHandler.cs create mode 100644 tests/Tests.Shared/Tests.Shared.csproj create mode 100644 tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs create mode 100644 tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/GlobalUsings.cs create mode 100644 tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/SsiCredentialIssuer.Expiry.App.Tests.csproj create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/ContentTypeMapperExtensionsTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/DocumentRepositoryTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerDbContextTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerRepositoriesTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/ProcessStepRepositoryTests.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_details.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/documents.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/process_steps.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/processes.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_external_type_detail_versions.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_type_assigned_use_cases.test.json create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/Setup/TestDbFixture.cs create mode 100644 tests/database/SsiCredentialIssuer.DbAccess.Tests/SsiCredentialIssuer.DbAccess.Tests.csproj create mode 100644 tests/externalservices/Portal.Service.Tests/Portal.Service.Tests.csproj create mode 100644 tests/externalservices/Portal.Service.Tests/PortalServiceTests.cs create mode 100644 tests/externalservices/Wallet.Service.Tests/BusinessLogic/WalletBusinessLogicTests.cs create mode 100644 tests/externalservices/Wallet.Service.Tests/Services/BasicAuthTokenServiceTests.cs create mode 100644 tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs create mode 100644 tests/externalservices/Wallet.Service.Tests/Wallet.Service.Tests.csproj create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Authentication/KeycloakClaimsTransformationTests.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/CredentialControllerTests.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Identity/MandatoryIdentityClaimHandlerTests.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumerableStub.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumeratorStub.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncQueryProviderStub.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/FakePolicyEvaluator.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/IntegrationTestFactory.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/SsiCredentialIssuer.Service.Tests.csproj create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/Usings.cs create mode 100644 tests/issuer/SsiCredentialIssuer.Service.Tests/appsettings.IntegrationTests.json create mode 100644 tests/processes/CredentialProcess.Library.Tests/CredentialProcess.Library.Tests.csproj create mode 100644 tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs create mode 100644 tests/processes/CredentialProcess.Worker.Tests/CredentialProcess.Worker.Tests.csproj create mode 100644 tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs create mode 100644 tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs create mode 100644 tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj create mode 100644 tests/processes/Processes.Library.Tests/Usings.cs create mode 100644 tests/processes/Processes.Worker.Library.Tests/MockLogger.cs create mode 100644 tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs create mode 100644 tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs create mode 100644 tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj create mode 100644 tests/processes/Processes.Worker.Library.Tests/Usings.cs diff --git a/DEPENDENCIES b/DEPENDENCIES index e69de29b..fcae7a8a 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -0,0 +1,57 @@ +nuget/nuget/-/AutoFixture.AutoFakeItEasy/4.18.0, MIT, approved, #10064 +nuget/nuget/-/AutoFixture.Xunit/4.18.0, MIT, approved, #10082 +nuget/nuget/-/AutoFixture/4.18.0, MIT, approved, #10057 +nuget/nuget/-/Castle.Core/4.3.1, Apache-2.0, approved, clearlydefined +nuget/nuget/-/EFCore.NamingConventions/7.0.2, Apache-2.0, approved, #10067 +nuget/nuget/-/FakeItEasy/7.4.0, MIT, approved, #10080 +nuget/nuget/-/Fare/2.1.1, MIT, approved, clearlydefined +nuget/nuget/-/FluentAssertions/6.11.0, Apache-2.0 AND MIT, approved, #10061 +nuget/nuget/-/Flurl.Http.Signed/3.2.4, MIT, approved, #3503 +nuget/nuget/-/Flurl.Signed/3.0.6, MIT, approved, #3501 +nuget/nuget/-/Humanizer.Core/2.14.1, MIT, approved, #10060 +nuget/nuget/-/Json.More.Net/2.0.0, MIT, approved, clearlydefined +nuget/nuget/-/JsonPointer.Net/4.0.0, MIT, approved, clearlydefined +nuget/nuget/-/JsonSchema.Net/6.0.3, MIT AND OFL-1.1 AND CC-BY-SA-4.0, approved, #13370 +nuget/nuget/-/Laraue.EfCoreTriggers.Common/7.1.0, MIT, approved, #10247 +nuget/nuget/-/Laraue.EfCoreTriggers.PostgreSql/7.1.0, MIT, approved, #10248 +nuget/nuget/-/Mono.TextTemplating/2.2.1, MIT, approved, clearlydefined +nuget/nuget/-/Newtonsoft.Json/12.0.2, MIT AND BSD-3-Clause, approved, #11114 +nuget/nuget/-/Newtonsoft.Json/13.0.1, MIT AND BSD-3-Clause, approved, #3266 +nuget/nuget/-/Newtonsoft.Json/13.0.3, MIT AND BSD-3-Clause, approved, #3266 +nuget/nuget/-/Npgsql.EntityFrameworkCore.PostgreSQL/7.0.11, PostgreSQL AND MIT AND Apache-2.0, approved, #10081 +nuget/nuget/-/Npgsql/7.0.6, PostgreSQL, approved, #10062 +nuget/nuget/-/PasswordGenerator/2.1.0, MIT, approved, #3192 +nuget/nuget/-/Portable.BouncyCastle/1.9.0, MIT, approved, clearlydefined +nuget/nuget/-/SSH.NET/2020.0.2, MIT AND ISC AND LicenseRef-Public-domain AND (MIT AND MS-PL), approved, #10073 +nuget/nuget/-/Serilog.AspNetCore/7.0.0, Apache-2.0 AND MIT, approved, #10084 +nuget/nuget/-/Serilog.Enrichers.CorrelationId/3.0.1, MIT, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Environment/2.2.0, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Process/2.0.2, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Sensitive/1.7.3, MIT, approved, clearlydefined +nuget/nuget/-/Serilog.Enrichers.Thread/3.1.0, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Extensions.Hosting/7.0.0, Apache-2.0, approved, #10078 +nuget/nuget/-/Serilog.Extensions.Logging/7.0.0, Apache-2.0, approved, #10070 +nuget/nuget/-/Serilog.Formatting.Compact/1.1.0, Apache-2.0, approved, #11115 +nuget/nuget/-/Serilog.Settings.Configuration/7.0.0, Apache-2.0, approved, #10069 +nuget/nuget/-/Serilog.Sinks.Console/4.1.0, Apache-2.0, approved, #8434 +nuget/nuget/-/Serilog.Sinks.Debug/2.0.0, Apache-2.0, approved, clearlydefined +nuget/nuget/-/Serilog.Sinks.File/5.0.0, Apache-2.0, approved, #11116 +nuget/nuget/-/Serilog/3.0.1, Apache-2.0, approved, #10063 +nuget/nuget/-/SharpZipLib/1.4.2, MIT AND GFDL-1.3-or-later AND (Apache-2.0 AND MIT) AND WTFPL AND bzip2-1.0.6 AND LicenseRef-Permissive-license-with-conditions AND LicenseRef-Permission-Notice, approved, #10058 +nuget/nuget/-/SshNet.Security.Cryptography/1.3.0, MIT, approved, clearlydefined +nuget/nuget/-/Swashbuckle.AspNetCore.Swagger/6.5.0, MIT AND Apache-2.0, approved, #7160 +nuget/nuget/-/Swashbuckle.AspNetCore.SwaggerGen/6.5.0, MIT AND Apache-2.0, approved, #7156 +nuget/nuget/-/Swashbuckle.AspNetCore.SwaggerUI/6.5.0, MIT AND Apache-2.0, approved, #7158 +nuget/nuget/-/Swashbuckle.AspNetCore/6.5.0, MIT AND Apache-2.0, approved, #7159 +nuget/nuget/-/Testcontainers.PostgreSql/3.4.0, MIT, approved, #10056 +nuget/nuget/-/Testcontainers/3.4.0, MIT, approved, #10083 +nuget/nuget/-/Xunit.Extensions.AssemblyFixture/2.4.1, MIT, approved, #3502 +nuget/nuget/-/coverlet.collector/6.0.0, MIT, approved, #10075 +nuget/nuget/-/xunit.abstractions/2.0.3, Apache-2.0, approved, clearlydefined +nuget/nuget/-/xunit.analyzers/1.2.0, Apache-2.0 AND MIT, approved, #10068 +nuget/nuget/-/xunit.assert/2.5.0, Apache-2.0 AND MIT, approved, #10071 +nuget/nuget/-/xunit.core/2.5.0, Apache-2.0 AND MIT, approved, #10059 +nuget/nuget/-/xunit.extensibility.core/2.5.0, Apache-2.0 AND MIT, approved, #10077 +nuget/nuget/-/xunit.extensibility.execution/2.5.0, Apache-2.0 AND MIT, approved, #10074 +nuget/nuget/-/xunit.runner.visualstudio/2.5.0, Apache-2.0 AND MIT, approved, #10065 +nuget/nuget/-/xunit/2.5.0, Apache-2.0 AND MIT, approved, #10072 diff --git a/FILEHEADER.md b/FILEHEADER.md index 79d8879b..7b905c2c 100644 --- a/FILEHEADER.md +++ b/FILEHEADER.md @@ -12,4 +12,4 @@ Every time you create a new file or edit a file that you created and doesn't yet Currently the following templates are available: * cx_header_default -* cx_header_with_# \ No newline at end of file +* cx_header_with_# diff --git a/README.md b/README.md index 2c922dea..186d3821 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,4 @@ dotnet run ## License Distributed under the Apache 2.0 License. -See [LICENSE](./LICENSE) for more information \ No newline at end of file +See [LICENSE](./LICENSE) for more information diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 00000000..8f19d9c4 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,25 @@ + + + + + 0.1.0 + rc.1 + + diff --git a/src/SsiCredentialIssuer.sln b/src/SsiCredentialIssuer.sln new file mode 100644 index 00000000..45b97db1 --- /dev/null +++ b/src/SsiCredentialIssuer.sln @@ -0,0 +1,177 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "database", "database", "{E9E08CE9-985A-4507-BBD3-9470623986CF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issuer", "issuer", "{32D0AE23-BFAA-4D65-AF9D-2DF951BA5A3B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Service", "issuer\SsiCredentialIssuer.Service\SsiCredentialIssuer.Service.csproj", "{6905B6DF-722B-4882-A2CB-5E6BFD0244F2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Entities", "database\SsiCredentialIssuer.Entities\SsiCredentialIssuer.Entities.csproj", "{5627AA4F-A64B-4878-A139-F3ED6734BDEC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.DbAccess", "database\SsiCredentialIssuer.DbAccess\SsiCredentialIssuer.DbAccess.csproj", "{55F32819-B53D-4908-A4C1-F663871474C3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "credentials", "credentials", "{A79FF417-08E7-4175-8089-5F21054F5BDE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Migrations", "database\SsiCredentialIssuer.Migrations\SsiCredentialIssuer.Migrations.csproj", "{F74763E9-231A-46C9-A160-72863963D17F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "externalservices", "externalservices", "{A37A220C-4242-4FB2-98ED-EF4B602EF6C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallet.Service", "externalservices\Wallet.Service\Wallet.Service.csproj", "{73705ADF-C348-483D-931C-5CDFAB848596}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "processes", "processes", "{A4745993-CEC3-48E9-9765-0401F5B0516C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library", "processes\Processes.Library\Processes.Library.csproj", "{1D005706-65BF-4A99-A847-69E656CF7529}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker", "processes\Processes.Worker\Processes.Worker.csproj", "{DA615C88-8859-41CD-A696-0BE542117008}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library", "processes\Processes.Worker.Library\Processes.Worker.Library.csproj", "{7F6B407F-A698-4568-A711-42FD53F06129}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialProcess.Worker", "processes\CredentialProcess.Worker\CredentialProcess.Worker.csproj", "{30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialProcess.Library", "processes\CredentialProcess.Library\CredentialProcess.Library.csproj", "{28919880-0F47-4D54-B253-E1381984C840}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portal.Service", "externalservices\Portal.Service\Portal.Service.csproj", "{8AF8FC7D-3448-422A-8739-0690AA700DAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Expiry.App", "credentials\SsiCredentialIssuer.Expiry.App\SsiCredentialIssuer.Expiry.App.csproj", "{9F9FFBEE-4256-494F-BEF7-8856145D45EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Expiry.App.Tests", "..\tests\credentials\SsiCredentialIssuer.Expiry.App.Tests\SsiCredentialIssuer.Expiry.App.Tests.csproj", "{FFB1423F-C41F-46A9-8D5D-115B3BC46682}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Library.Tests", "..\tests\processes\Processes.Library.Tests\Processes.Library.Tests.csproj", "{3AADCF6D-746E-49C8-89DC-96474F70B130}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Processes.Worker.Library.Tests", "..\tests\processes\Processes.Worker.Library.Tests\Processes.Worker.Library.Tests.csproj", "{5CEA19ED-04F0-4CC6-8D5D-C54629436EDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.Service.Tests", "..\tests\issuer\SsiCredentialIssuer.Service.Tests\SsiCredentialIssuer.Service.Tests.csproj", "{D88007E0-603A-49B7-B4E4-AD255B06D252}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portal.Service.Tests", "..\tests\externalservices\Portal.Service.Tests\Portal.Service.Tests.csproj", "{5A0321D9-F716-47AF-AA96-E5AADC0C26EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.Shared", "..\tests\Tests.Shared\Tests.Shared.csproj", "{6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wallet.Service.Tests", "..\tests\externalservices\Wallet.Service.Tests\Wallet.Service.Tests.csproj", "{F46B828C-3E95-49D0-A3D2-F5A083F5FE7A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SsiCredentialIssuer.DbAccess.Tests", "..\tests\database\SsiCredentialIssuer.DbAccess.Tests\SsiCredentialIssuer.DbAccess.Tests.csproj", "{3967B3E7-093B-438C-81FE-A7CAE5FFAD31}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialProcess.Worker.Tests", "..\tests\processes\CredentialProcess.Worker.Tests\CredentialProcess.Worker.Tests.csproj", "{2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CredentialProcess.Library.Tests", "..\tests\processes\CredentialProcess.Library.Tests\CredentialProcess.Library.Tests.csproj", "{0A9C65F7-62B6-421F-ADA5-709A1EE10901}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6905B6DF-722B-4882-A2CB-5E6BFD0244F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6905B6DF-722B-4882-A2CB-5E6BFD0244F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6905B6DF-722B-4882-A2CB-5E6BFD0244F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6905B6DF-722B-4882-A2CB-5E6BFD0244F2}.Release|Any CPU.Build.0 = Release|Any CPU + {5627AA4F-A64B-4878-A139-F3ED6734BDEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5627AA4F-A64B-4878-A139-F3ED6734BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5627AA4F-A64B-4878-A139-F3ED6734BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5627AA4F-A64B-4878-A139-F3ED6734BDEC}.Release|Any CPU.Build.0 = Release|Any CPU + {55F32819-B53D-4908-A4C1-F663871474C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {55F32819-B53D-4908-A4C1-F663871474C3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {55F32819-B53D-4908-A4C1-F663871474C3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {55F32819-B53D-4908-A4C1-F663871474C3}.Release|Any CPU.Build.0 = Release|Any CPU + {F74763E9-231A-46C9-A160-72863963D17F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F74763E9-231A-46C9-A160-72863963D17F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F74763E9-231A-46C9-A160-72863963D17F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F74763E9-231A-46C9-A160-72863963D17F}.Release|Any CPU.Build.0 = Release|Any CPU + {73705ADF-C348-483D-931C-5CDFAB848596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73705ADF-C348-483D-931C-5CDFAB848596}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73705ADF-C348-483D-931C-5CDFAB848596}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73705ADF-C348-483D-931C-5CDFAB848596}.Release|Any CPU.Build.0 = Release|Any CPU + {1D005706-65BF-4A99-A847-69E656CF7529}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D005706-65BF-4A99-A847-69E656CF7529}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D005706-65BF-4A99-A847-69E656CF7529}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D005706-65BF-4A99-A847-69E656CF7529}.Release|Any CPU.Build.0 = Release|Any CPU + {DA615C88-8859-41CD-A696-0BE542117008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA615C88-8859-41CD-A696-0BE542117008}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA615C88-8859-41CD-A696-0BE542117008}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA615C88-8859-41CD-A696-0BE542117008}.Release|Any CPU.Build.0 = Release|Any CPU + {7F6B407F-A698-4568-A711-42FD53F06129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F6B407F-A698-4568-A711-42FD53F06129}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F6B407F-A698-4568-A711-42FD53F06129}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F6B407F-A698-4568-A711-42FD53F06129}.Release|Any CPU.Build.0 = Release|Any CPU + {30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76}.Release|Any CPU.Build.0 = Release|Any CPU + {28919880-0F47-4D54-B253-E1381984C840}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {28919880-0F47-4D54-B253-E1381984C840}.Debug|Any CPU.Build.0 = Debug|Any CPU + {28919880-0F47-4D54-B253-E1381984C840}.Release|Any CPU.ActiveCfg = Release|Any CPU + {28919880-0F47-4D54-B253-E1381984C840}.Release|Any CPU.Build.0 = Release|Any CPU + {8AF8FC7D-3448-422A-8739-0690AA700DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AF8FC7D-3448-422A-8739-0690AA700DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AF8FC7D-3448-422A-8739-0690AA700DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AF8FC7D-3448-422A-8739-0690AA700DAD}.Release|Any CPU.Build.0 = Release|Any CPU + {9F9FFBEE-4256-494F-BEF7-8856145D45EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F9FFBEE-4256-494F-BEF7-8856145D45EC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F9FFBEE-4256-494F-BEF7-8856145D45EC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F9FFBEE-4256-494F-BEF7-8856145D45EC}.Release|Any CPU.Build.0 = Release|Any CPU + {FFB1423F-C41F-46A9-8D5D-115B3BC46682}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FFB1423F-C41F-46A9-8D5D-115B3BC46682}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FFB1423F-C41F-46A9-8D5D-115B3BC46682}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FFB1423F-C41F-46A9-8D5D-115B3BC46682}.Release|Any CPU.Build.0 = Release|Any CPU + {3AADCF6D-746E-49C8-89DC-96474F70B130}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AADCF6D-746E-49C8-89DC-96474F70B130}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AADCF6D-746E-49C8-89DC-96474F70B130}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AADCF6D-746E-49C8-89DC-96474F70B130}.Release|Any CPU.Build.0 = Release|Any CPU + {5CEA19ED-04F0-4CC6-8D5D-C54629436EDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CEA19ED-04F0-4CC6-8D5D-C54629436EDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CEA19ED-04F0-4CC6-8D5D-C54629436EDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CEA19ED-04F0-4CC6-8D5D-C54629436EDA}.Release|Any CPU.Build.0 = Release|Any CPU + {D88007E0-603A-49B7-B4E4-AD255B06D252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D88007E0-603A-49B7-B4E4-AD255B06D252}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D88007E0-603A-49B7-B4E4-AD255B06D252}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D88007E0-603A-49B7-B4E4-AD255B06D252}.Release|Any CPU.Build.0 = Release|Any CPU + {5A0321D9-F716-47AF-AA96-E5AADC0C26EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A0321D9-F716-47AF-AA96-E5AADC0C26EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A0321D9-F716-47AF-AA96-E5AADC0C26EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A0321D9-F716-47AF-AA96-E5AADC0C26EE}.Release|Any CPU.Build.0 = Release|Any CPU + {6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A}.Release|Any CPU.Build.0 = Release|Any CPU + {F46B828C-3E95-49D0-A3D2-F5A083F5FE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F46B828C-3E95-49D0-A3D2-F5A083F5FE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F46B828C-3E95-49D0-A3D2-F5A083F5FE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F46B828C-3E95-49D0-A3D2-F5A083F5FE7A}.Release|Any CPU.Build.0 = Release|Any CPU + {3967B3E7-093B-438C-81FE-A7CAE5FFAD31}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3967B3E7-093B-438C-81FE-A7CAE5FFAD31}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3967B3E7-093B-438C-81FE-A7CAE5FFAD31}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3967B3E7-093B-438C-81FE-A7CAE5FFAD31}.Release|Any CPU.Build.0 = Release|Any CPU + {2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87}.Release|Any CPU.Build.0 = Release|Any CPU + {0A9C65F7-62B6-421F-ADA5-709A1EE10901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A9C65F7-62B6-421F-ADA5-709A1EE10901}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A9C65F7-62B6-421F-ADA5-709A1EE10901}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A9C65F7-62B6-421F-ADA5-709A1EE10901}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6905B6DF-722B-4882-A2CB-5E6BFD0244F2} = {32D0AE23-BFAA-4D65-AF9D-2DF951BA5A3B} + {5627AA4F-A64B-4878-A139-F3ED6734BDEC} = {E9E08CE9-985A-4507-BBD3-9470623986CF} + {55F32819-B53D-4908-A4C1-F663871474C3} = {E9E08CE9-985A-4507-BBD3-9470623986CF} + {F74763E9-231A-46C9-A160-72863963D17F} = {E9E08CE9-985A-4507-BBD3-9470623986CF} + {73705ADF-C348-483D-931C-5CDFAB848596} = {A37A220C-4242-4FB2-98ED-EF4B602EF6C1} + {1D005706-65BF-4A99-A847-69E656CF7529} = {A4745993-CEC3-48E9-9765-0401F5B0516C} + {DA615C88-8859-41CD-A696-0BE542117008} = {A4745993-CEC3-48E9-9765-0401F5B0516C} + {7F6B407F-A698-4568-A711-42FD53F06129} = {A4745993-CEC3-48E9-9765-0401F5B0516C} + {30C2CCFC-6C76-4A68-8CA1-BA6A4EA28E76} = {A4745993-CEC3-48E9-9765-0401F5B0516C} + {28919880-0F47-4D54-B253-E1381984C840} = {A4745993-CEC3-48E9-9765-0401F5B0516C} + {8AF8FC7D-3448-422A-8739-0690AA700DAD} = {A37A220C-4242-4FB2-98ED-EF4B602EF6C1} + {9F9FFBEE-4256-494F-BEF7-8856145D45EC} = {A79FF417-08E7-4175-8089-5F21054F5BDE} + {FFB1423F-C41F-46A9-8D5D-115B3BC46682} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {3AADCF6D-746E-49C8-89DC-96474F70B130} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {5CEA19ED-04F0-4CC6-8D5D-C54629436EDA} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {D88007E0-603A-49B7-B4E4-AD255B06D252} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {5A0321D9-F716-47AF-AA96-E5AADC0C26EE} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {6A1D91F3-B9EC-40CD-9239-1D80FADF8E1A} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {F46B828C-3E95-49D0-A3D2-F5A083F5FE7A} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {3967B3E7-093B-438C-81FE-A7CAE5FFAD31} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {2D42186B-5BC8-4F60-92D3-FD1FF0F5FF87} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + {0A9C65F7-62B6-421F-ADA5-709A1EE10901} = {A2FC3E0F-5AFE-44FA-909B-B8016DD1EB44} + EndGlobalSection +EndGlobal diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceExtensions.cs b/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceExtensions.cs new file mode 100644 index 00000000..8ee30dd2 --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceExtensions.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.DependencyInjection; + +/// +/// Extension method to register the expiry check service and dependent services +/// +[ExcludeFromCodeCoverage] +public static class ExpiryCheckServiceExtensions +{ + /// + /// Adds the expiry check service + /// + /// the services + /// the configuration section to setup the settings + /// the enriched service collection + public static IServiceCollection AddExpiryCheckService(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions().Bind(section); + services + .AddTransient() + .AddTransient(); + return services; + } +} diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceSettings.cs b/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceSettings.cs new file mode 100644 index 00000000..e544c53c --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/DependencyInjection/ExpiryCheckServiceSettings.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.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.DependencyInjection; + +/// +/// Settings for the ExpiryCheckService +/// +public class ExpiryCheckServiceSettings +{ + /// + /// Vcs which are older than the given value will be deleted when expired + /// + [Required] + public int ExpiredVcsToDeleteInMonth { get; init; } + + /// + /// Vcs which are older than the given value will be deleted when declined + /// + [Required] + public int InactiveVcsToDeleteInWeeks { get; init; } +} diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs b/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs new file mode 100644 index 00000000..a5017dd0 --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs @@ -0,0 +1,196 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App; + +/// +/// Service to delete the pending and inactive documents as well as the depending consents from the database +/// +public class ExpiryCheckService +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private readonly IServiceScopeFactory _serviceScopeFactory; + private readonly ILogger _logger; + private readonly ExpiryCheckServiceSettings _settings; + + /// + /// Creates a new instance of + /// + /// access to the services + /// the logger + /// The options + public ExpiryCheckService( + IServiceScopeFactory serviceScopeFactory, + ILogger logger, + IOptions options) + { + _serviceScopeFactory = serviceScopeFactory; + _logger = logger; + _settings = options.Value; + } + + /// + /// Handles the + /// + /// Cancellation Token + public async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!stoppingToken.IsCancellationRequested) + { + try + { + using var processServiceScope = _serviceScopeFactory.CreateScope(); + var repositories = processServiceScope.ServiceProvider.GetRequiredService(); + var dateTimeProvider = processServiceScope.ServiceProvider.GetRequiredService(); + var portalService = processServiceScope.ServiceProvider.GetRequiredService(); + + using var outerLoopScope = _serviceScopeFactory.CreateScope(); + var outerLoopRepositories = outerLoopScope.ServiceProvider.GetRequiredService(); + + var now = dateTimeProvider.OffsetNow; + var companySsiDetailsRepository = repositories.GetInstance(); + var inactiveVcsToDelete = now.AddDays(-(_settings.InactiveVcsToDeleteInWeeks * 7)); + var expiredVcsToDelete = now.AddMonths(-_settings.ExpiredVcsToDeleteInMonth); + + var credentials = outerLoopRepositories.GetInstance() + .GetExpiryData(now, inactiveVcsToDelete, expiredVcsToDelete); + await foreach (var credential in credentials.WithCancellation(stoppingToken).ConfigureAwait(false)) + { + await ProcessCredentials(credential, companySsiDetailsRepository, repositories, portalService, + stoppingToken); + } + } + catch (Exception ex) + { + Environment.ExitCode = 1; + _logger.LogError("Verified Credential expiry check failed with error: {Errors}", ex.Message); + } + } + } + + private static async Task ProcessCredentials( + CredentialExpiryData data, + ICompanySsiDetailsRepository companySsiDetailsRepository, + IIssuerRepositories repositories, + IPortalService portalService, + CancellationToken cancellationToken) + { + if (data.ScheduleData.IsVcToDelete) + { + companySsiDetailsRepository.RemoveSsiDetail(data.Id); + } + else if (data.ScheduleData.IsVcToDecline) + { + await HandleDecline(data, companySsiDetailsRepository, portalService, cancellationToken).ConfigureAwait(false); + } + else + { + await HandleNotification(data, companySsiDetailsRepository, portalService, cancellationToken).ConfigureAwait(false); + } + + // Saving here to make sure the each credential is handled by there own + await repositories.SaveAsync().ConfigureAwait(false); + } + + private static async ValueTask HandleDecline( + CredentialExpiryData data, + ICompanySsiDetailsRepository companySsiDetailsRepository, + IPortalService portalService, + CancellationToken cancellationToken) + { + var content = JsonSerializer.Serialize(new { Type = data.VerifiedCredentialTypeId, CredentialId = data.Id }, Options); + await portalService.AddNotification(content, data.Bpnl, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken); + companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(data.Id, c => + { + c.CompanySsiDetailStatusId = data.CompanySsiDetailStatusId; + }, + c => + { + c.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + }); + + var typeValue = data.VerifiedCredentialTypeId.GetEnumValue() ?? throw new UnexpectedConditionException($"VerifiedCredentialType {data.VerifiedCredentialTypeId} does not exists"); + var mailParameters = new Dictionary + { + { "requestName", typeValue }, + { "reason", "The credential is already expired" } + }; + await portalService.TriggerMail("CredentialRejected", data.Bpnl, mailParameters, cancellationToken); + } + + private static async ValueTask HandleNotification( + CredentialExpiryData data, + ICompanySsiDetailsRepository companySsiDetailsRepository, + IPortalService portalService, + CancellationToken cancellationToken) + { + var newExpiryCheckTypeId = data.ScheduleData switch + { + { IsOneDayNotification: true } => ExpiryCheckTypeId.ONE_DAY, + { IsTwoWeeksNotification: true } => ExpiryCheckTypeId.TWO_WEEKS, + { IsOneMonthNotification: true } => ExpiryCheckTypeId.ONE_MONTH, + _ => throw new UnexpectedConditionException("one of IsVcToDelete, IsOneDayNotification, IsTwoWeeksNotification, IsOneMonthNotification, IsVcToDecline is expected to be true") + }; + + companySsiDetailsRepository.AttachAndModifyCompanySsiDetails( + data.Id, + csd => + { + csd.ExpiryCheckTypeId = data.ExpiryCheckTypeId; + }, + csd => + { + csd.ExpiryCheckTypeId = newExpiryCheckTypeId; + }); + + var content = JsonSerializer.Serialize(new + { + Type = data.VerifiedCredentialTypeId, + ExpiryDate = data.ExpiryDate?.ToString("O") ?? throw new ConflictException("Expiry Date must be set here"), + Version = data.DetailVersion, + CredentialId = data.Id, + ExpiryCheckTypeId = newExpiryCheckTypeId + }, Options); + await portalService.AddNotification(content, data.Bpnl, NotificationTypeId.CREDENTIAL_EXPIRY, cancellationToken); + var typeValue = data.VerifiedCredentialTypeId.GetEnumValue() ?? throw new UnexpectedConditionException($"VerifiedCredentialType {data.VerifiedCredentialTypeId} does not exists"); + var mailParameters = new Dictionary + { + { "typeId", typeValue }, + { "version", data.DetailVersion ?? "no version" }, + { "expiryDate", data.ExpiryDate?.ToString("dd MMMM yyyy") ?? throw new ConflictException("Expiry Date must be set here") } + }; + + await portalService.TriggerMail("CredentialExpiry", data.Bpnl, mailParameters, cancellationToken); + } +} diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/Program.cs b/src/credentials/SsiCredentialIssuer.Expiry.App/Program.cs new file mode 100644 index 00000000..5adce1cb --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/Program.cs @@ -0,0 +1,68 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Hosting; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; +using Serilog; +using System.Diagnostics.CodeAnalysis; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Building worker"); +try +{ + var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddExpiryCheckService(hostContext.Configuration.GetSection("Expiry")) + .AddPortalService(hostContext.Configuration.GetSection("Portal")) + .AddIssuerRepositories(hostContext.Configuration); + }) + .AddLogging() + .Build(); + Log.Information("Building worker completed"); + + using 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/credentials/SsiCredentialIssuer.Expiry.App/Properties/launchSettings.json b/src/credentials/SsiCredentialIssuer.Expiry.App/Properties/launchSettings.json new file mode 100644 index 00000000..bfcfee34 --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Credential.App": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/SsiCredentialIssuer.Expiry.App.csproj b/src/credentials/SsiCredentialIssuer.Expiry.App/SsiCredentialIssuer.Expiry.App.csproj new file mode 100644 index 00000000..4526d1a3 --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/SsiCredentialIssuer.Expiry.App.csproj @@ -0,0 +1,79 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App + Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App + net7.0 + enable + enable + 37a22764-0a60-4c2e-a692-d59c4f14abbf + Exe + Linux + ..\..\.. + True + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + Always + + + + + + Program.cs + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + + + diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/appsettings.json b/src/credentials/SsiCredentialIssuer.Expiry.App/appsettings.json new file mode 100644 index 00000000..d559eeb4 --- /dev/null +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/appsettings.json @@ -0,0 +1,57 @@ +{ + "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": "Org.Eclipse.TractusX.Portal.Backend.Maintenance.App" + } + }, + "ConnectionStrings": { + "PortalDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Expiry": { + "ExpiredVcsToDeleteInMonth": 12, + "InactiveVcsToDeleteInWeeks": 12 + }, + "MailingService": { + "Templates": [ + { + "Name": "CredentialRejected", + "Setting": { + "Subject": "Company Wallet - SSI Credential Request Rejected", + "EmailTemplateType": "CredentialRejected" + } + }, + { + "Name": "CredentialExpiry", + "Setting": { + "Subject": "Company Wallet - SSI Credential Expired", + "EmailTemplateType": "CredentialExpiry" + } + } + ], + "Mail": { + "SmtpHost": "", + "SmtpPort": 587, + "SmtpUser": "", + "SmtpPassword": "" + } + }, + "ProcessIdentity": { + "ProcessUserId": "" + } +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Extensions/MediaTypeIdExtensions.cs b/src/database/SsiCredentialIssuer.DbAccess/Extensions/MediaTypeIdExtensions.cs new file mode 100644 index 00000000..c96345d8 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Extensions/MediaTypeIdExtensions.cs @@ -0,0 +1,65 @@ +/******************************************************************************** + * 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.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Net.Mime; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Extensions; + +public static class MediaTypeIdExtensions +{ + public static string MapToMediaType(this MediaTypeId mediaTypeId) + { + return mediaTypeId switch + { + MediaTypeId.JPEG => MediaTypeNames.Image.Jpeg, + MediaTypeId.GIF => MediaTypeNames.Image.Gif, + MediaTypeId.PNG => "image/png", + MediaTypeId.SVG => "image/svg+xml", + MediaTypeId.TIFF => MediaTypeNames.Image.Tiff, + MediaTypeId.PDF => MediaTypeNames.Application.Pdf, + MediaTypeId.JSON => MediaTypeNames.Application.Json, + MediaTypeId.PEM => "application/x-pem-file", + MediaTypeId.CA_CERT => "application/x-x509-ca-cert", + MediaTypeId.PKX_CER => "application/pkix-cert", + MediaTypeId.OCTET => MediaTypeNames.Application.Octet, + _ => throw new ConflictException($"document mediatype {mediaTypeId} is not supported") + }; + } + + public static MediaTypeId ParseMediaTypeId(this string mediaType) + { + return mediaType.ToLower() switch + { + MediaTypeNames.Image.Jpeg => MediaTypeId.JPEG, + "image/png" => MediaTypeId.PNG, + MediaTypeNames.Image.Gif => MediaTypeId.GIF, + "image/svg+xml" => MediaTypeId.SVG, + MediaTypeNames.Image.Tiff => MediaTypeId.TIFF, + MediaTypeNames.Application.Pdf => MediaTypeId.PDF, + MediaTypeNames.Application.Json => MediaTypeId.JSON, + "application/x-pem-file" => MediaTypeId.PEM, + "application/x-x509-ca-cert" => MediaTypeId.CA_CERT, + "application/pkix-cert" => MediaTypeId.PKX_CER, + MediaTypeNames.Application.Octet => MediaTypeId.OCTET, + _ => throw new UnsupportedMediaTypeException($"mediaType '{mediaType}' is not supported") + }; + } +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/IIssuerRepositories.cs b/src/database/SsiCredentialIssuer.DbAccess/IIssuerRepositories.cs new file mode 100644 index 00000000..e0f19e3e --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/IIssuerRepositories.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 Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; + +public interface IIssuerRepositories +{ + /// + /// Attaches the given Entity to the database + /// + /// the entity that should be attached to the database + /// + /// Type of the entity + /// Returns the attached entity + TEntity Attach(TEntity entity, Action? setOptionalParameters = null) + where TEntity : class; + + public T GetInstance(); + + public Task SaveAsync(); + void Clear(); +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/IssuerRepositories.cs b/src/database/SsiCredentialIssuer.DbAccess/IssuerRepositories.cs new file mode 100644 index 00000000..b662182d --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/IssuerRepositories.cs @@ -0,0 +1,78 @@ +/******************************************************************************** + * 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; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; + +public class IssuerRepositories : IIssuerRepositories +{ + private readonly IssuerDbContext _dbContext; + + private static readonly IReadOnlyDictionary> Types = new Dictionary> { + { typeof(ICompanySsiDetailsRepository), context => new CompanySsiDetailsRepository(context) }, + { typeof(ICredentialRepository), context => new CredentialRepository(context) }, + { typeof(IDocumentRepository), context => new DocumentRepository(context) }, + { typeof(IProcessStepRepository), context => new ProcessStepRepository(context) }, + }.ToImmutableDictionary(); + + public IssuerRepositories(IssuerDbContext dbContext) + { + _dbContext = dbContext; + } + + 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 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/SsiCredentialIssuer.DbAccess/IssuerRepositoriesStartupServiceExtensions.cs b/src/database/SsiCredentialIssuer.DbAccess/IssuerRepositoriesStartupServiceExtensions.cs new file mode 100644 index 00000000..42d0a7fe --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/IssuerRepositoriesStartupServiceExtensions.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 + ********************************************************************************/ + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.DependencyInjection; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; + +public static class IssuerRepositoriesStartupServiceExtensions +{ + [ExcludeFromCodeCoverage] + public static IServiceCollection AddIssuerRepositories(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped() + .AddDbAuditing() + .AddDbContext(o => o + .UseNpgsql(configuration.GetConnectionString("IssuerDb"))) + .AddHealthChecks() + .AddDbContextCheck("IssuerDbContext", tags: new[] { "issuerdb" }); + return services; + } +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiDetailSorting.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiDetailSorting.cs new file mode 100644 index 00000000..b1fa5105 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/CompanySsiDetailSorting.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +/// +/// Possible sorting options for the pagination +/// +public enum CompanySsiDetailSorting +{ + /// + /// Ascending by bpnl + /// + BpnlAsc = 1, + + /// + /// Descending by bpnl + /// + BpnlDesc = 2, +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialDetailData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialDetailData.cs new file mode 100644 index 00000000..e04c59d8 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialDetailData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record CredentialDetailData +( + Guid CredentialDetailId, + string Bpnl, + VerifiedCredentialTypeId CredentialType, + string? UseCase, + CompanySsiDetailStatusId ParticipantStatus, + DateTimeOffset? ExpiryDate, + IEnumerable Documents, + ExternalTypeDetailData? ExternalTypeDetail +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialExpiryData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialExpiryData.cs new file mode 100644 index 00000000..3369d496 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/CredentialExpiryData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record CredentialExpiryData( + Guid Id, + DateTimeOffset? ExpiryDate, + ExpiryCheckTypeId? ExpiryCheckTypeId, + string? DetailVersion, + string Bpnl, + CompanySsiDetailStatusId CompanySsiDetailStatusId, + VerifiedCredentialTypeId VerifiedCredentialTypeId, + CredentialScheduleData ScheduleData); + +public record CredentialScheduleData( + bool IsVcToDelete, + bool IsOneDayNotification, + bool IsTwoWeeksNotification, + bool IsOneMonthNotification, + bool IsVcToDecline +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs new file mode 100644 index 00000000..5f2202d4 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record DocumentData +( + Guid DocumentId, + string DocumentName, + DocumentTypeId argDocumentTypeId); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/EncryptionTransformationData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/EncryptionTransformationData.cs new file mode 100644 index 00000000..d343eba7 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/EncryptionTransformationData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record EncryptionTransformationData( + byte[]? Secret, + byte[]? InitializationVector, + int? EncryptionMode +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/ExternalTypeDetailData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/ExternalTypeDetailData.cs new file mode 100644 index 00000000..6765c6b2 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/ExternalTypeDetailData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record ExternalTypeDetailData( + Guid Id, + VerifiedCredentialExternalTypeId VerifiedCredentialExternalTypeId, + string? Version, + string? Template, + DateTimeOffset? ValidFrom, + DateTimeOffset? Expiry +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.cs new file mode 100644 index 00000000..5f7c46ea --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiApprovalData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record SsiApprovalData( + CompanySsiDetailStatusId Status, + VerifiedCredentialTypeId Type, + VerifiedCredentialTypeKindId? Kind, + string? Bpn, + DetailData? DetailData +); + +public record DetailData( + VerifiedCredentialExternalTypeId VerifiedCredentialExternalTypeId, + string? Template, + string? Version, + DateTimeOffset ExpiryDate +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/SsiCertificateTransferData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiCertificateTransferData.cs new file mode 100644 index 00000000..9b92cb09 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/SsiCertificateTransferData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record SsiCertificateTransferData +( + VerifiedCredentialTypeId CredentialType, + IEnumerable Credentials +); + +public record SsiCertificateExternalTypeDetailTransferData +( + ExternalTypeDetailData ExternalDetailData, + IEnumerable SsiDetailData +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/TechnicalUserDetails.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/TechnicalUserDetails.cs new file mode 100644 index 00000000..9250b3bf --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/TechnicalUserDetails.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 System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record TechnicalUserDetails( + [property: JsonPropertyName("walletUrl")] string WalletUrl, + [property: JsonPropertyName("clientId")] string ClientId, + [property: JsonPropertyName("clientSecret")] string ClientSecret +); + +public record HolderWalletData( + [property: JsonPropertyName("walletUrl")] string? WalletUrl, + [property: JsonPropertyName("clientId")] string? ClientId +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/UseCaseParticipationTransferData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/UseCaseParticipationTransferData.cs new file mode 100644 index 00000000..18f61106 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/UseCaseParticipationTransferData.cs @@ -0,0 +1,44 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; + +public record UseCaseParticipationTransferData +( + string? UseCase, + string? Description, + VerifiedCredentialTypeId CredentialType, + IEnumerable VerifiedCredentials +); + +public record CompanySsiExternalTypeDetailTransferData +( + ExternalTypeDetailData ExternalDetailData, + IEnumerable SsiDetailData +); + +public record CompanySsiDetailTransferData +( + Guid CredentialId, + CompanySsiDetailStatusId ParticipationStatus, + DateTimeOffset? ExpiryDate, + IEnumerable Documents +); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs new file mode 100644 index 00000000..9f6bffa7 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CompanySsiDetailsRepository.cs @@ -0,0 +1,285 @@ +/******************************************************************************** + * 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; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +public class CompanySsiDetailsRepository : ICompanySsiDetailsRepository +{ + private readonly IssuerDbContext _context; + + /// + /// Constructor. + /// + /// DB context. + public CompanySsiDetailsRepository(IssuerDbContext dbContext) + { + _context = dbContext; + } + + /// + public IAsyncEnumerable GetUseCaseParticipationForCompany(string bpnl, DateTimeOffset minExpiry) => + _context.VerifiedCredentialTypes + .Where(t => t.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId == VerifiedCredentialTypeKindId.FRAMEWORK) + .Select(t => new + { + UseCase = t.VerifiedCredentialTypeAssignedUseCase!.UseCase, + TypeId = t.Id, + ExternalTypeDetails = t.VerifiedCredentialTypeAssignedExternalType!.VerifiedCredentialExternalType!.VerifiedCredentialExternalTypeDetailVersions + }) + .Select(x => new UseCaseParticipationTransferData( + x.UseCase!.Name, + x.UseCase.Shortname, + x.TypeId, + x.ExternalTypeDetails + .Select(e => + new CompanySsiExternalTypeDetailTransferData( + new ExternalTypeDetailData( + e.Id, + e.VerifiedCredentialExternalTypeId, + e.Version, + e.Template, + e.ValidFrom, + e.Expiry), + e.CompanySsiDetails + .Where(ssi => + ssi.Bpnl == bpnl && + ssi.VerifiedCredentialTypeId == x.TypeId && + ssi.CompanySsiDetailStatusId != CompanySsiDetailStatusId.INACTIVE && + ssi.VerifiedCredentialExternalTypeDetailVersionId == e.Id && + ssi.ExpiryDate > minExpiry) + .Select(ssi => + new CompanySsiDetailTransferData( + ssi.Id, + ssi.CompanySsiDetailStatusId, + ssi.ExpiryDate, + ssi.Documents.Select(d => new DocumentData( + d.Id, + d.DocumentName, + d.DocumentTypeId)))) + .Take(2) + )) + )) + .ToAsyncEnumerable(); + + /// + public IAsyncEnumerable GetSsiCertificates(string bpnl, DateTimeOffset minExpiry) => + _context.VerifiedCredentialTypes + .Where(types => types.VerifiedCredentialTypeAssignedKind != null && types.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId != VerifiedCredentialTypeKindId.FRAMEWORK) + .Select(t => new + { + TypeId = t.Id, + ExternalTypeDetails = t.VerifiedCredentialTypeAssignedExternalType!.VerifiedCredentialExternalType!.VerifiedCredentialExternalTypeDetailVersions + }) + .Select(x => new SsiCertificateTransferData( + x.TypeId, + x.ExternalTypeDetails + .Select(e => + new SsiCertificateExternalTypeDetailTransferData( + new ExternalTypeDetailData( + e.Id, + e.VerifiedCredentialExternalTypeId, + e.Version, + e.Template, + e.ValidFrom, + e.Expiry), + e.CompanySsiDetails + .Where(ssi => + ssi.Bpnl == bpnl && + ssi.VerifiedCredentialTypeId == x.TypeId && + ssi.CompanySsiDetailStatusId != CompanySsiDetailStatusId.INACTIVE && + ssi.ExpiryDate > minExpiry) + .Select(ssi => + new CompanySsiDetailTransferData( + ssi.Id, + ssi.CompanySsiDetailStatusId, + ssi.ExpiryDate, + ssi.Documents.Select(d => new DocumentData( + d.Id, + d.DocumentName, + d.DocumentTypeId)))) + .Take(2) + )) + )) + .ToAsyncEnumerable(); + + /// + public CompanySsiDetail CreateSsiDetails(string bpnl, VerifiedCredentialTypeId verifiedCredentialTypeId, Guid docId, CompanySsiDetailStatusId companySsiDetailStatusId, Guid userId, Action? setOptionalFields) + { + var detail = new CompanySsiDetail(Guid.NewGuid(), bpnl, verifiedCredentialTypeId, companySsiDetailStatusId, userId, DateTimeOffset.UtcNow); + setOptionalFields?.Invoke(detail); + return _context.CompanySsiDetails.Add(detail).Entity; + } + + /// + public Task CheckSsiDetailsExistsForCompany(string bpnl, VerifiedCredentialTypeId verifiedCredentialTypeId, VerifiedCredentialTypeKindId kindId, Guid? verifiedCredentialExternalTypeUseCaseDetailId) => + _context.CompanySsiDetails + .AnyAsync(x => + x.Bpnl == bpnl && + x.VerifiedCredentialTypeId == verifiedCredentialTypeId && + x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId == kindId && + x.CompanySsiDetailStatusId != CompanySsiDetailStatusId.INACTIVE && + (verifiedCredentialExternalTypeUseCaseDetailId == null || x.VerifiedCredentialExternalTypeDetailVersionId == verifiedCredentialExternalTypeUseCaseDetailId)); + + /// + public Task<(bool Exists, string? Version, string? Template, IEnumerable UseCase, DateTimeOffset Expiry)> CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(Guid verifiedCredentialExternalTypeUseCaseDetailId, VerifiedCredentialTypeId verifiedCredentialTypeId) => + _context.VerifiedCredentialExternalTypeDetailVersions + .Where(x => + x.Id == verifiedCredentialExternalTypeUseCaseDetailId && + x.VerifiedCredentialExternalType!.VerifiedCredentialTypeAssignedExternalTypes.Any(y => y.VerifiedCredentialTypeId == verifiedCredentialTypeId)) + .Select(x => new ValueTuple, DateTimeOffset>( + true, + x.Version, + x.Template, + x.VerifiedCredentialExternalType!.VerifiedCredentialTypeAssignedExternalTypes.Select(y => y.VerifiedCredentialType!.VerifiedCredentialTypeAssignedUseCase!.UseCase!.Shortname), + x.Expiry)) + .SingleOrDefaultAsync(); + + /// + public Task<(bool Exists, IEnumerable DetailVersionIds)> CheckSsiCertificateType(VerifiedCredentialTypeId credentialTypeId) => + _context.VerifiedCredentialTypeAssignedKinds + .Where(x => + x.VerifiedCredentialTypeId == credentialTypeId && + x.VerifiedCredentialTypeKindId != VerifiedCredentialTypeKindId.FRAMEWORK) + .Select(x => new ValueTuple>( + true, + x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedExternalType!.VerifiedCredentialExternalType!.VerifiedCredentialExternalTypeDetailVersions.Select(x => x.Id) + )) + .SingleOrDefaultAsync(); + + /// + public IQueryable GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId) => + _context.CompanySsiDetails.AsNoTracking() + .Where(c => + (!companySsiDetailStatusId.HasValue || c.CompanySsiDetailStatusId == companySsiDetailStatusId.Value) && + (!credentialTypeId.HasValue || c.VerifiedCredentialTypeId == credentialTypeId)); + + /// + public Task<(bool exists, SsiApprovalData data)> GetSsiApprovalData(Guid credentialId) => + _context.CompanySsiDetails + .Where(x => x.Id == credentialId) + .Select(x => new ValueTuple( + true, + new SsiApprovalData( + x.CompanySsiDetailStatusId, + x.VerifiedCredentialTypeId, + x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind == null ? null : x.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId, + x.Bpnl, + x.VerifiedCredentialExternalTypeDetailVersion == null ? + null : + new DetailData( + x.VerifiedCredentialExternalTypeDetailVersion!.VerifiedCredentialExternalTypeId, + x.VerifiedCredentialExternalTypeDetailVersion.Template, + x.VerifiedCredentialExternalTypeDetailVersion.Version, + x.VerifiedCredentialExternalTypeDetailVersion.Expiry + ) + ) + )) + .SingleOrDefaultAsync(); + + /// + public Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type)> GetSsiRejectionData(Guid credentialId) => + _context.CompanySsiDetails + .Where(x => x.Id == credentialId) + .Select(x => new ValueTuple( + true, + x.CompanySsiDetailStatusId, + x.VerifiedCredentialTypeId + )) + .SingleOrDefaultAsync(); + + /// + public void AttachAndModifyCompanySsiDetails(Guid id, Action? initialize, Action updateFields) + { + var entity = new CompanySsiDetail(id, null!, default, default, Guid.Empty, default); + initialize?.Invoke(entity); + _context.Attach(entity); + updateFields.Invoke(entity); + } + + /// + public IAsyncEnumerable GetCertificateTypes(string bpnl) => + _context.VerifiedCredentialTypes + .Where(x => + x.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId != VerifiedCredentialTypeKindId.FRAMEWORK && + !x.CompanySsiDetails.Any(ssi => + ssi.Bpnl == bpnl && + (ssi.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING || ssi.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE))) + .Select(x => x.Id) + .ToAsyncEnumerable(); + + public IAsyncEnumerable GetExpiryData(DateTimeOffset now, DateTimeOffset inactiveVcsToDelete, DateTimeOffset expiredVcsToDelete) + { + var oneDay = now.AddDays(1); + var twoWeeks = now.AddDays(14); + var oneMonth = now.AddMonths(2); + + return _context.CompanySsiDetails + .Select(x => new + { + Details = x, + IsVcToDecline = x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING && x.VerifiedCredentialExternalTypeDetailVersion!.Expiry < now, + IsVcToDelete = x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE && x.DateCreated < inactiveVcsToDelete || (x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE || x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE) && x.ExpiryDate < expiredVcsToDelete, + IsOneDayNotification = x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && x.ExpiryDate <= oneDay && (x.ExpiryCheckTypeId == ExpiryCheckTypeId.TWO_WEEKS || x.ExpiryCheckTypeId == ExpiryCheckTypeId.ONE_MONTH || x.ExpiryCheckTypeId == null), + IsTwoWeeksNotification = x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && x.ExpiryDate > oneDay && x.ExpiryDate <= twoWeeks && (x.ExpiryCheckTypeId == ExpiryCheckTypeId.ONE_MONTH || x.ExpiryCheckTypeId == null), + IsOneMonthNotification = x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && x.ExpiryDate > twoWeeks && x.ExpiryDate <= oneMonth && x.ExpiryCheckTypeId == null + }) + .Where(x => x.IsVcToDecline || x.IsVcToDelete || x.IsOneDayNotification || x.IsTwoWeeksNotification || x.IsOneMonthNotification) + .Select(x => new CredentialExpiryData( + x.Details.Id, + x.Details.ExpiryDate, + x.Details.ExpiryCheckTypeId, + x.Details.VerifiedCredentialExternalTypeDetailVersion!.Version, + x.Details.Bpnl, + x.Details.CompanySsiDetailStatusId, + x.Details.VerifiedCredentialTypeId, + new CredentialScheduleData( + x.IsVcToDelete, + x.IsOneDayNotification, + x.IsTwoWeeksNotification, + x.IsOneMonthNotification, + x.IsVcToDecline + ))) + .ToAsyncEnumerable(); + } + + public void RemoveSsiDetail(Guid companySsiDetailId) => + _context.CompanySsiDetails.Remove(new CompanySsiDetail(companySsiDetailId, null!, default, default, Guid.Empty, default)); + + public void CreateProcessData(Guid companySsiDetailId, JsonDocument schema, VerifiedCredentialTypeKindId credentialTypeKindId, Action? setOptionalFields) + { + var companySsiDetailData = new CompanySsiProcessData(companySsiDetailId, schema, credentialTypeKindId); + _context.CompanySsiProcessData.Add(companySsiDetailData); + setOptionalFields?.Invoke(companySsiDetailData); + } + + public void AttachAndModifyProcessData(Guid companySsiDetailId, Action? initialize, Action setOptionalFields) + { + var companySsiDetailData = new CompanySsiProcessData(companySsiDetailId, null!, default); + initialize?.Invoke(companySsiDetailData); + _context.CompanySsiProcessData.Attach(companySsiDetailData); + setOptionalFields(companySsiDetailData); + } +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs new file mode 100644 index 00000000..735d77d9 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs @@ -0,0 +1,68 @@ +/******************************************************************************** + * 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; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +public class CredentialRepository : ICredentialRepository +{ + private readonly IssuerDbContext _dbContext; + + public CredentialRepository(IssuerDbContext dbContext) + { + _dbContext = dbContext; + } + + public Task GetWalletCredentialId(Guid credentialId) => + _dbContext.CompanySsiDetails.Where(x => x.Id == credentialId) + .Select(x => x.ExternalCredentialId) + .SingleOrDefaultAsync(); + + public Task<(HolderWalletData HolderWalletData, string? Credential, EncryptionTransformationData EncryptionInformation)> GetCredentialData(Guid credentialId) => + _dbContext.CompanySsiDetails + .Where(x => x.Id == credentialId) + .Select(x => new ValueTuple( + new HolderWalletData(x.CompanySsiProcessData!.HolderWalletUrl, x.CompanySsiProcessData.ClientId), + x.Credential, + new EncryptionTransformationData(x.CompanySsiProcessData!.ClientSecret, x.CompanySsiProcessData.InitializationVector, x.CompanySsiProcessData.EncryptionMode))) + .SingleOrDefaultAsync(); + + public Task<(bool Exists, Guid CredentialId)> GetDataForProcessId(Guid processId) => + _dbContext.CompanySsiDetails + .Where(c => c.ProcessId == processId) + .Select(c => new ValueTuple(true, c.Id)) + .SingleOrDefaultAsync(); + + public Task<(VerifiedCredentialTypeKindId CredentialTypeKindId, JsonDocument Schema)> GetCredentialStorageInformationById(Guid credentialId) => + _dbContext.CompanySsiDetails + .Where(c => c.Id == credentialId) + .Select(c => new ValueTuple(c.CompanySsiProcessData!.CredentialTypeKindId, c.CompanySsiProcessData.Schema)) + .SingleOrDefaultAsync(); + + public Task<(Guid? ExternalCredentialId, VerifiedCredentialTypeKindId KindId)> GetExternalCredentialAndKindId(Guid credentialId) => + _dbContext.CompanySsiDetails + .Where(c => c.Id == credentialId) + .Select(c => new ValueTuple(c.ExternalCredentialId, c.VerifiedCredentialType!.VerifiedCredentialTypeAssignedKind!.VerifiedCredentialTypeKindId)) + .SingleOrDefaultAsync(); +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs new file mode 100644 index 00000000..b6bf1b6d --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +/// Implementation of accessing database with EF Core. +public class DocumentRepository : IDocumentRepository +{ + private readonly IssuerDbContext _dbContext; + + /// + /// Constructor. + /// + /// PortalDb context. + public DocumentRepository(IssuerDbContext dbContext) + { + this._dbContext = dbContext; + } + + /// + public Document CreateDocument(string documentName, byte[] documentContent, byte[] hash, MediaTypeId mediaTypeId, DocumentTypeId documentTypeId, Action? setupOptionalFields) + { + var document = new Document( + Guid.NewGuid(), + documentContent, + hash, + documentName, + mediaTypeId, + DateTimeOffset.UtcNow, + DocumentStatusId.ACTIVE, + documentTypeId); + + setupOptionalFields?.Invoke(document); + return _dbContext.Documents.Add(document).Entity; + } + + /// + public void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDetailId) + { + var document = new CompanySsiDetailAssignedDocument(documentId, companySsiDetailId); + _dbContext.CompanySsiDetailAssignedDocuments.Add(document); + } +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs new file mode 100644 index 00000000..d8240733 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICompanySsiDetailsRepository.cs @@ -0,0 +1,99 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +public interface ICompanySsiDetailsRepository +{ + /// + /// Gets the company credential details for the given company id + /// + /// Bpnl of the company + /// The minimum datetime the expiry date should have + /// AsyncEnumerable of UseCaseParticipation + IAsyncEnumerable GetUseCaseParticipationForCompany(string bpnl, DateTimeOffset minExpiry); + + /// + /// Gets the company credential details for the given company id + /// + /// Bpnl of the company + /// The minimum datetime the expiry date should have + /// AsyncEnumerable of SsiCertificateData + IAsyncEnumerable GetSsiCertificates(string bpnl, DateTimeOffset minExpiry); + + /// + /// Creates the credential details + /// + /// Id of the company + /// Id of the credential types + /// id of the document + /// id of detail status + /// Id of the creator + /// Id of the linked Process + /// sets the optional fields + /// The created entity + CompanySsiDetail CreateSsiDetails(string bpnl, VerifiedCredentialTypeId verifiedCredentialTypeId, Guid docId, CompanySsiDetailStatusId companySsiDetailStatusId, Guid userId, Action? setOptionalFields); + + /// + /// Checks whether the credential details are already exists for the company and the given version + /// + /// Bpnl of the company + /// Id of the verifiedCredentialType + /// Id of the credentialTypeKind + /// Id of the verifiedCredentialExternalType Detail Id + /// true if the details already exists, otherwise false + Task CheckSsiDetailsExistsForCompany(string bpnl, VerifiedCredentialTypeId verifiedCredentialTypeId, VerifiedCredentialTypeKindId kindId, Guid? verifiedCredentialExternalTypeUseCaseDetailId); + + /// + /// Checks whether the given externalTypeDetail exists and returns the CredentialTypeId + /// + /// Id of vc external type use case detail id + /// Id of the vc type + /// Returns a valueTuple with identifiers if the externalTypeUseCaseDetailId exists and the corresponding credentialTypeId + Task<(bool Exists, string? Version, string? Template, IEnumerable UseCase, DateTimeOffset Expiry)> CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(Guid verifiedCredentialExternalTypeUseCaseDetailId, VerifiedCredentialTypeId verifiedCredentialTypeId); + + /// + /// Checks whether the given credentialTypeId is a Certificate + /// + /// Id of the credentialTypeId + /// true if the tpye is a certificate, otherwise false + Task<(bool Exists, IEnumerable DetailVersionIds)> CheckSsiCertificateType(VerifiedCredentialTypeId credentialTypeId); + + /// + /// Gets all credential details + /// + /// The status of the details + /// OPTIONAL: The type of the credential that should be returned + /// Returns data to create the pagination + IQueryable GetAllCredentialDetails(CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId); + + Task<(bool exists, SsiApprovalData data)> GetSsiApprovalData(Guid credentialId); + Task<(bool Exists, CompanySsiDetailStatusId Status, VerifiedCredentialTypeId Type)> GetSsiRejectionData(Guid credentialId); + void AttachAndModifyCompanySsiDetails(Guid id, Action? initialize, Action updateFields); + IAsyncEnumerable GetCertificateTypes(string bpnl); + IAsyncEnumerable GetExpiryData(DateTimeOffset now, DateTimeOffset inactiveVcsToDelete, DateTimeOffset expiredVcsToDelete); + void RemoveSsiDetail(Guid companySsiDetailId); + void CreateProcessData(Guid companySsiDetailId, JsonDocument schema, VerifiedCredentialTypeKindId credentialTypeKindId, Action? setOptionalFields); + void AttachAndModifyProcessData(Guid companySsiDetailId, Action? initialize, Action setOptionalFields); +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs new file mode 100644 index 00000000..9af61ddc --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.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 Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +public interface ICredentialRepository +{ + Task GetWalletCredentialId(Guid credentialId); + Task<(HolderWalletData HolderWalletData, string? Credential, EncryptionTransformationData EncryptionInformation)> GetCredentialData(Guid credentialId); + Task<(bool Exists, Guid CredentialId)> GetDataForProcessId(Guid processId); + Task<(VerifiedCredentialTypeKindId CredentialTypeKindId, JsonDocument Schema)> GetCredentialStorageInformationById(Guid credentialId); + Task<(Guid? ExternalCredentialId, VerifiedCredentialTypeKindId KindId)> GetExternalCredentialAndKindId(Guid credentialId); +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs new file mode 100644 index 00000000..5b5c105b --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +/// +/// Repository for writing documents on persistence layer. +/// +public interface IDocumentRepository +{ + /// + /// Creates a document in the persistence layer. + /// + /// The documents name + /// The document itself + /// Hash of the document + /// The documents mediaType + /// the document type id + /// Action to setup the additional fields + /// Returns the created document + Document CreateDocument(string documentName, byte[] documentContent, byte[] hash, MediaTypeId mediaTypeId, DocumentTypeId documentTypeId, Action? setupOptionalFields); + + void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDetailId); +} diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/IProcessStepRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/IProcessStepRepository.cs new file mode 100644 index 00000000..3e1419f9 --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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/SsiCredentialIssuer.DbAccess/Repositories/ProcessStepRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ProcessStepRepository.cs new file mode 100644 index 00000000..177eddf3 --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using ProcessTypeId = Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums.ProcessTypeId; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; + +public class ProcessStepRepository : IProcessStepRepository +{ + private readonly IssuerDbContext _context; + + /// + /// Constructor + /// + /// PortalDb context. + public ProcessStepRepository(IssuerDbContext portalDbContext) + { + _context = portalDbContext; + } + + public Process CreateProcess(ProcessTypeId processTypeId) => + _context.Add(new Process(Guid.NewGuid(), processTypeId, Guid.NewGuid())).Entity; + + public ProcessStep CreateProcessStep(ProcessStepTypeId processStepTypeId, ProcessStepStatusId processStepStatusId, Guid processId) => + _context.Add(new ProcessStep(Guid.NewGuid(), processStepTypeId, processStepStatusId, processId, DateTimeOffset.UtcNow)).Entity; + + public IEnumerable CreateProcessStepRange(IEnumerable<(ProcessStepTypeId ProcessStepTypeId, 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/SsiCredentialIssuer.DbAccess/SsiCredentialIssuer.DbAccess.csproj b/src/database/SsiCredentialIssuer.DbAccess/SsiCredentialIssuer.DbAccess.csproj new file mode 100644 index 00000000..6cb5fa65 --- /dev/null +++ b/src/database/SsiCredentialIssuer.DbAccess/SsiCredentialIssuer.DbAccess.csproj @@ -0,0 +1,42 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess + Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess + net7.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditCompanySsiDetail20240228.cs b/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditCompanySsiDetail20240228.cs new file mode 100644 index 00000000..2697cae5 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditCompanySsiDetail20240228.cs @@ -0,0 +1,59 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities; + +[ExcludeFromCodeCoverage] +public class AuditCompanySsiDetail20240228 : IAuditEntityV1 +{ + /// + [Key] + public Guid AuditV1Id { get; set; } + + public Guid Id { get; set; } + public string Bpnl { get; set; } = null!; + public VerifiedCredentialTypeId VerifiedCredentialTypeId { get; set; } + public CompanySsiDetailStatusId CompanySsiDetailStatusId { get; set; } + public DateTimeOffset DateCreated { get; private set; } + public Guid CreatorUserId { get; set; } + public DateTimeOffset? ExpiryDate { get; set; } + public Guid? VerifiedCredentialExternalTypeDetailVersionId { get; set; } + + public ExpiryCheckTypeId? ExpiryCheckTypeId { get; set; } + public Guid? ProcessId { get; set; } + public Guid? ExternalCredentialId { get; set; } + public string? Credential { get; set; } + public DateTimeOffset? DateLastChanged { get; set; } + public Guid? LastEditorId { get; set; } + + /// + public Guid? AuditV1LastEditorId { get; set; } + + /// + public AuditOperationId AuditV1OperationId { get; set; } + + /// + public DateTimeOffset AuditV1DateLastChanged { get; set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditDocument20240305.cs b/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditDocument20240305.cs new file mode 100644 index 00000000..bbfe37ae --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/AuditEntities/AuditDocument20240305.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities; + +[ExcludeFromCodeCoverage] +public class AuditDocument20240305 : IAuditEntityV1 +{ + /// + [Key] + public Guid AuditV1Id { get; set; } + + public Guid Id { get; private set; } + + public DateTimeOffset? DateCreated { get; private set; } + + public byte[]? DocumentHash { get; set; } + + public byte[]? DocumentContent { get; set; } + + public string? DocumentName { get; set; } + + public MediaTypeId? MediaTypeId { get; set; } + + public DocumentTypeId? DocumentTypeId { get; set; } + + public DocumentStatusId? DocumentStatusId { get; set; } + + public Guid? CompanyUserId { get; set; } + public DateTimeOffset? DateLastChanged { get; set; } + public Guid? LastEditorId { get; private set; } + + /// + public DateTimeOffset AuditV1DateLastChanged { get; set; } + + /// + public Guid? AuditV1LastEditorId { get; set; } + + /// + public AuditOperationId AuditV1OperationId { get; set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditEntityV1Attribute.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditEntityV1Attribute.cs new file mode 100644 index 00000000..b9d84ea4 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditEntityV1Attribute.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; + +/// +/// Attribute to Provide the needed methods to setup an audit trigger +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +[AttributeUsage(AttributeTargets.Class)] +public class AuditEntityV1Attribute : Attribute +{ + public AuditEntityV1Attribute(Type auditEntityType) + { + if (!typeof(IAuditEntityV1).IsAssignableFrom(auditEntityType)) + { + throw new ArgumentException($"Entity must derive from {nameof(IAuditEntityV1)}", nameof(auditEntityType)); + } + + AuditEntityType = auditEntityType; + } + + public virtual Type AuditEntityType { get; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditInsertEditorV1Attribute.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditInsertEditorV1Attribute.cs new file mode 100644 index 00000000..4ccebc8b --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/AuditInsertEditorV1Attribute.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; + +/// +/// Attribute to mark the creators id in the base class. +/// The usage is optional. If not set +/// is being used to determine the creators id. +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +[AttributeUsage(AttributeTargets.Property)] +public class AuditInsertEditorV1Attribute : Attribute +{ +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastChangedV1Attribute.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastChangedV1Attribute.cs new file mode 100644 index 00000000..8028d45e --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastChangedV1Attribute.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; + +/// +/// Attribute to mark the last editor id in the base class +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LastChangedV1Attribute : Attribute +{ +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastEditorV1Attribute.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastEditorV1Attribute.cs new file mode 100644 index 00000000..6af18970 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Attributes/LastEditorV1Attribute.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; + +/// +/// Attribute to mark the last editor id in the base class +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +[AttributeUsage(AttributeTargets.Property)] +public class LastEditorV1Attribute : Attribute +{ +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/DependencyInjection/AuditingDependencyInjection.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/DependencyInjection/AuditingDependencyInjection.cs new file mode 100644 index 00000000..ba3e0698 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/DependencyInjection/AuditingDependencyInjection.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 Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.DependencyInjection; + +public static class AuditingDependencyInjection +{ + public static IServiceCollection AddDbAuditing(this IServiceCollection services) + { + return services.AddTransient() + .AddTransient(); + } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditOperationId.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditOperationId.cs new file mode 100644 index 00000000..975bb728 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditOperationId.cs @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; + +/// +/// Possible operations for the audit table +/// +public enum AuditOperationId +{ + /// + /// The entity has been inserted + /// + INSERT = 1, + + /// + /// The entity has been updated + /// + UPDATE = 2, + + /// + /// The entity has been deleted + /// + DELETE = 3, +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditPropertyV1Names.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditPropertyV1Names.cs new file mode 100644 index 00000000..ea04951b --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Enums/AuditPropertyV1Names.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; + +public enum AuditPropertyV1Names +{ + AuditV1Id, + AuditV1OperationId, + AuditV1DateLastChanged, + AuditV1LastEditorId +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/AuditExtensions.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/AuditExtensions.cs new file mode 100644 index 00000000..e881df0c --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/AuditExtensions.cs @@ -0,0 +1,86 @@ +/******************************************************************************** + * 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; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; +using System.Collections.Immutable; +using System.Reflection; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Extensions; + +public static class AuditExtensions +{ + public record AuditPropertyInformation + ( + Type AuditEntityType, + IEnumerable SourceProperties, + IEnumerable AuditProperties, + IEnumerable TargetProperties + ); + + public static AuditOperationId ToAuditOperation(this EntityState state) => + state switch + { + EntityState.Added => AuditOperationId.INSERT, + EntityState.Deleted => AuditOperationId.DELETE, + EntityState.Modified => AuditOperationId.UPDATE, + _ => throw new ConflictException($"Entries with state {state} should not be audited") + }; + + public static AuditPropertyInformation? GetAuditPropertyInformation(this Type auditableEntityType) + { + var auditEntityAttribute = + (AuditEntityV1Attribute?)Attribute.GetCustomAttribute(auditableEntityType, typeof(AuditEntityV1Attribute)); + if (auditEntityAttribute == null) + { + return null; + } + + var auditEntityType = auditEntityAttribute.AuditEntityType; + if (!typeof(IAuditEntityV1).IsAssignableFrom(auditEntityType)) + { + throw new ConflictException($"{auditEntityType} must inherit from {nameof(IAuditEntityV1)}"); + } + + var sourceProperties = (typeof(IBaseEntity).IsAssignableFrom(auditableEntityType) + ? typeof(IBaseEntity).GetProperties() + : Enumerable.Empty()) + .Concat(auditableEntityType + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Where(p => !(p.GetGetMethod()?.IsVirtual ?? false))) + .ToImmutableList(); + var auditProperties = typeof(IAuditEntityV1).GetProperties(); + var targetProperties = auditEntityType.GetProperties().ExceptBy(auditProperties.Select(x => x.Name), p => p.Name).ToImmutableList(); + + targetProperties + .ExceptBy(Enumerable.Repeat(nameof(IBaseEntity.Id), 1), p => p.Name) + .Where(x => x.PropertyType == typeof(Nullable<>)) + .IfAny(notNullableProperties => + throw new ConfigurationException($"Properties {string.Join(",", notNullableProperties.Select(x => x.Name))} of type {auditEntityType.Name} are not nullable")); + + return new AuditPropertyInformation( + auditEntityType, + sourceProperties, + auditProperties, + targetProperties); + } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/EntityTypeBuilderV1Extension.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/EntityTypeBuilderV1Extension.cs new file mode 100644 index 00000000..8d3270b8 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Extensions/EntityTypeBuilderV1Extension.cs @@ -0,0 +1,129 @@ +/******************************************************************************** + * 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 Laraue.EfCoreTriggers.Common.Extensions; +using Laraue.EfCoreTriggers.Common.TriggerBuilders.TableRefs; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; +using System.ComponentModel.DataAnnotations; +using System.Linq.Expressions; +using System.Reflection; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Extensions; + +public static class EntityTypeBuilderV1Extension +{ + public static EntityTypeBuilder HasAuditV1Triggers(this EntityTypeBuilder builder) where TEntity : class, IAuditableV1 where TAuditEntity : class, IAuditEntityV1 + { + var (auditEntityType, sourceProperties, auditProperties, targetProperties) = typeof(TEntity).GetAuditPropertyInformation() ?? throw new ConfigurationException($"{typeof(TEntity)} must be annotated with {nameof(AuditEntityV1Attribute)}"); + if (typeof(TAuditEntity) != auditEntityType) + { + throw new ConfigurationException($"{typeof(TEntity).Name} is annotated with {nameof(AuditEntityV1Attribute)} referring to a different audit entity type {auditEntityType.Name} then {typeof(TAuditEntity).Name}"); + } + + sourceProperties.IntersectBy(auditProperties.Select(x => x.Name), p => p.Name).IfAny( + illegalProperties => throw new ConfigurationException($"{typeof(TEntity).Name} is must not declare any of the following properties: {string.Join(", ", illegalProperties.Select(x => x.Name))}")); + + sourceProperties.ExceptBy(targetProperties.Select(x => x.Name), p => p.Name).IfAny( + missingProperties => throw new ArgumentException($"{typeof(TAuditEntity).Name} is missing the following properties: {string.Join(", ", missingProperties.Select(x => x.Name))}")); + + if (!Array.Exists( + typeof(TAuditEntity).GetProperties(), + p => p.Name == AuditPropertyV1Names.AuditV1Id.ToString() && p.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute)))) + { + throw new ConfigurationException($"{typeof(TAuditEntity).Name}.{AuditPropertyV1Names.AuditV1Id} must be marked as primary key by attribute {typeof(KeyAttribute).Name}"); + } + + var insertEditorProperty = sourceProperties.SingleOrDefault(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(AuditInsertEditorV1Attribute))); + var lastEditorProperty = sourceProperties.SingleOrDefault(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(LastEditorV1Attribute))); + + return builder + .AfterInsert(trigger => + trigger.Action(action => + action.Insert(CreateNewAuditEntityExpression(sourceProperties, insertEditorProperty ?? lastEditorProperty)))) + .AfterUpdate(trigger => + trigger.Action(action => + action.Insert(CreateUpdateAuditEntityExpression(sourceProperties, lastEditorProperty)))); + } + + private static Expression, TAuditEntity>> CreateNewAuditEntityExpression(IEnumerable sourceProperties, PropertyInfo? lastEditorProperty) where TEntity : class + { + var entity = Expression.Parameter(typeof(NewTableRef), "entity"); + + var newPropertyInfo = typeof(NewTableRef).GetProperty("New"); + if (newPropertyInfo == null) + { + throw new UnexpectedConditionException($"{nameof(NewTableRef)} must have property New"); + } + + var propertyExpression = Expression.Property(entity, newPropertyInfo); + return Expression.Lambda, TAuditEntity>>( + CreateAuditEntityExpression(sourceProperties, AuditOperationId.INSERT, propertyExpression, lastEditorProperty), + entity); + } + + private static Expression, TAuditEntity>> CreateUpdateAuditEntityExpression(IEnumerable sourceProperties, PropertyInfo? lastEditorProperty) where TEntity : class + { + var entity = Expression.Parameter(typeof(OldAndNewTableRefs), "entity"); + + var newPropertyInfo = typeof(OldAndNewTableRefs).GetProperty("New"); + if (newPropertyInfo == null) + { + throw new UnexpectedConditionException($"{nameof(OldAndNewTableRefs)} must have property New"); + } + + var propertyExpression = Expression.Property(entity, newPropertyInfo); + return Expression.Lambda, TAuditEntity>>( + CreateAuditEntityExpression(sourceProperties, AuditOperationId.UPDATE, propertyExpression, lastEditorProperty), + entity); + } + + private static MemberInitExpression CreateAuditEntityExpression(IEnumerable sourceProperties, AuditOperationId auditOperationId, Expression entity, PropertyInfo? lastEditorProperty) + { + var memberBindings = sourceProperties.Select(p => + CreateMemberAssignment(typeof(TAuditEntity).GetMember(p.Name)[0], Expression.Property(entity, p))) + .Append(CreateMemberAssignment(typeof(TAuditEntity).GetMember(AuditPropertyV1Names.AuditV1Id.ToString())[0], Expression.New(typeof(Guid)))) + .Append(CreateMemberAssignment(typeof(TAuditEntity).GetMember(AuditPropertyV1Names.AuditV1OperationId.ToString())[0], Expression.Constant(auditOperationId))) + .Append(CreateMemberAssignment(typeof(TAuditEntity).GetMember(AuditPropertyV1Names.AuditV1DateLastChanged.ToString())[0], Expression.New(typeof(DateTimeOffset)))); + + if (lastEditorProperty != null) + { + memberBindings = memberBindings.Append(CreateMemberAssignment(typeof(TAuditEntity).GetMember(AuditPropertyV1Names.AuditV1LastEditorId.ToString())[0], Expression.Property(entity, lastEditorProperty))); + } + + return Expression.MemberInit( + Expression.New(typeof(TAuditEntity)), + memberBindings); + } + + private static MemberAssignment CreateMemberAssignment(MemberInfo member, Expression expression) + { + try + { + return Expression.Bind(member, expression); + } + catch (Exception e) + { + throw new ArgumentException($"{member.DeclaringType?.Name}.{member.Name} is not assignable from {expression}, {e.Message}", e); + } + } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/AuditHandlerV1.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/AuditHandlerV1.cs new file mode 100644 index 00000000..ce651a6e --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/AuditHandlerV1.cs @@ -0,0 +1,109 @@ +/******************************************************************************** + * 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; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Extensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; +using System.Collections.Immutable; +using System.Reflection; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; + +public class AuditHandlerV1 : IAuditHandler +{ + private readonly IIdentityIdService _identityService; + private readonly IDateTimeProvider _dateTimeProvider; + + public AuditHandlerV1(IIdentityIdService identityService, IDateTimeProvider dateTimeProvider) + { + _identityService = identityService; + _dateTimeProvider = dateTimeProvider; + } + + public void HandleAuditForChangedEntries(IEnumerable changedEntries, DbContext context) + { + var now = _dateTimeProvider.OffsetNow; + foreach (var groupedEntries in changedEntries + .GroupBy(entry => entry.Metadata.ClrType)) + { + var lastEditorNames = groupedEntries.Key.GetProperties() + .Where(x => Attribute.IsDefined(x, typeof(LastEditorV1Attribute))) + .Select(x => x.Name) + .ToImmutableHashSet(); + var lastChangedNames = groupedEntries.Key.GetProperties() + .Where(x => Attribute.IsDefined(x, typeof(LastChangedV1Attribute))) + .Select(x => x.Name) + .ToImmutableHashSet(); + + foreach (var properties in groupedEntries.Where(entry => entry.State != EntityState.Deleted).Select(entry => entry.Properties)) + { + foreach (var prop in properties.IntersectBy( + lastEditorNames, + property => property.Metadata.Name)) + { + prop.CurrentValue = _identityService.IdentityId; + } + + foreach (var prop in properties.IntersectBy( + lastChangedNames, + property => property.Metadata.Name)) + { + prop.CurrentValue = now; + } + } + + var auditPropertyInformation = groupedEntries.Key.GetAuditPropertyInformation(); + if (auditPropertyInformation == null) + continue; + var (auditEntityType, sourceProperties, _, targetProperties) = auditPropertyInformation; + + foreach (var entry in groupedEntries.Where(entry => entry.State == EntityState.Deleted)) + { + AddAuditEntry(entry, entry.Metadata.ClrType, context, auditEntityType, sourceProperties, targetProperties); + } + } + } + + private void AddAuditEntry(EntityEntry entityEntry, Type entityType, DbContext context, Type auditEntityType, IEnumerable sourceProperties, IEnumerable targetProperties) + { + if (Activator.CreateInstance(auditEntityType) is not IAuditEntityV1 newAuditEntity) + throw new UnexpectedConditionException($"AuditEntityV1Attribute can only be used on types implementing IAuditEntityV1 but Type {entityType} isn't"); + + var propertyValues = entityEntry.CurrentValues; + foreach (var joined in targetProperties.Join( + sourceProperties, + t => t.Name, + s => s.Name, + (t, s) => (Target: t, Value: propertyValues?[s.Name]))) + { + joined.Target.SetValue(newAuditEntity, joined.Value); + } + + newAuditEntity.AuditV1Id = Guid.NewGuid(); + newAuditEntity.AuditV1OperationId = entityEntry.State.ToAuditOperation(); + newAuditEntity.AuditV1DateLastChanged = _dateTimeProvider.OffsetNow; + newAuditEntity.AuditV1LastEditorId = _identityService.IdentityId; + + context.Add(newAuditEntity); + } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/IAuditHandler.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/IAuditHandler.cs new file mode 100644 index 00000000..02c25572 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Handler/IAuditHandler.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 Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; + +public interface IAuditHandler +{ + void HandleAuditForChangedEntries(IEnumerable changedEntries, DbContext context); +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditEntityV1.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditEntityV1.cs new file mode 100644 index 00000000..18471c8e --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditEntityV1.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; + +/// +/// Marker interface to define that the entity is an audit entity +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +public interface IAuditEntityV1 +{ + /// + /// Id of the audited entity + /// + Guid AuditV1Id { get; set; } + + /// + /// Date Time of the last change of the entity + /// + DateTimeOffset AuditV1DateLastChanged { get; set; } + + /// + /// Reference to the that changed the entity + /// + Guid? AuditV1LastEditorId { get; set; } + + /// + /// Id of the audit operation + /// + AuditOperationId AuditV1OperationId { get; set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditableV1.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditableV1.cs new file mode 100644 index 00000000..99f65976 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/IAuditableV1.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; + +/// +/// Marker interface to define that a entity is auditable +/// +/// +/// The implementation of this Attribute must not be changed. +/// When changes are needed create a V2 of it. +/// +public interface IAuditableV1 +{ +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/IBaseEntity.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/IBaseEntity.cs new file mode 100644 index 00000000..5ee0e0c3 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/IBaseEntity.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; + +/// +/// Marker interface +/// +public interface IBaseEntity +{ + Guid Id { get; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Auditing/Identity/IIdentityIdService.cs b/src/database/SsiCredentialIssuer.Entities/Auditing/Identity/IIdentityIdService.cs new file mode 100644 index 00000000..26544541 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Auditing/Identity/IIdentityIdService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +public interface IIdentityIdService +{ + Guid IdentityId { get; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetail.cs b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetail.cs new file mode 100644 index 00000000..95facf0c --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetail.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +[AuditEntityV1(typeof(AuditCompanySsiDetail20240228))] +public class CompanySsiDetail : IAuditableV1, IBaseEntity +{ + private CompanySsiDetail() + { + Bpnl = null!; + Documents = new HashSet(); + } + + public CompanySsiDetail(Guid id, string bpnl, VerifiedCredentialTypeId verifiedCredentialTypeId, CompanySsiDetailStatusId companySsiDetailStatusId, Guid creatorUserId, DateTimeOffset dateCreated) + : this() + { + Id = id; + Bpnl = bpnl; + VerifiedCredentialTypeId = verifiedCredentialTypeId; + CompanySsiDetailStatusId = companySsiDetailStatusId; + CreatorUserId = creatorUserId; + DateCreated = dateCreated; + } + + public Guid Id { get; set; } + public string Bpnl { get; set; } + public VerifiedCredentialTypeId VerifiedCredentialTypeId { get; set; } + public CompanySsiDetailStatusId CompanySsiDetailStatusId { get; set; } + public DateTimeOffset DateCreated { get; set; } + public Guid CreatorUserId { get; set; } + public DateTimeOffset? ExpiryDate { get; set; } + public Guid? VerifiedCredentialExternalTypeDetailVersionId { get; set; } + + public ExpiryCheckTypeId? ExpiryCheckTypeId { get; set; } + public Guid? ProcessId { get; set; } + public Guid? ExternalCredentialId { get; set; } + public string? Credential { get; set; } + + [LastChangedV1] + public DateTimeOffset? DateLastChanged { get; set; } + + [LastEditorV1] + public Guid? LastEditorId { get; private set; } + + // Navigation Properties + public virtual VerifiedCredentialType? VerifiedCredentialType { get; set; } + public virtual ExpiryCheckType? ExpiryCheckType { get; set; } + public virtual CompanySsiDetailStatus? CompanySsiDetailStatus { get; set; } + public virtual Process? Process { get; set; } + public virtual VerifiedCredentialExternalTypeDetailVersion? VerifiedCredentialExternalTypeDetailVersion { get; set; } + public virtual CompanySsiProcessData? CompanySsiProcessData { get; set; } + public virtual ICollection Documents { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailAssignedDocument.cs b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailAssignedDocument.cs new file mode 100644 index 00000000..79f88bdf --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailAssignedDocument.cs @@ -0,0 +1,16 @@ +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class CompanySsiDetailAssignedDocument +{ + public CompanySsiDetailAssignedDocument(Guid documentId, Guid companySsiDetailId) + { + DocumentId = documentId; + CompanySsiDetailId = companySsiDetailId; + } + + public Guid DocumentId { get; set; } + public Guid CompanySsiDetailId { get; set; } + + public virtual Document? Document { get; set; } + public virtual CompanySsiDetail? CompanySsiDetail { get; set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailStatus.cs b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailStatus.cs new file mode 100644 index 00000000..7c4be658 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiDetailStatus.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class CompanySsiDetailStatus +{ + private CompanySsiDetailStatus() + { + Label = null!; + CompanySsiDetails = new HashSet(); + } + + public CompanySsiDetailStatus(CompanySsiDetailStatusId companySsiDetailStatusId) + : this() + { + Id = companySsiDetailStatusId; + Label = companySsiDetailStatusId.ToString(); + } + + public CompanySsiDetailStatusId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiProcessData.cs b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiProcessData.cs new file mode 100644 index 00000000..aa14e404 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/CompanySsiProcessData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class CompanySsiProcessData +{ + private CompanySsiProcessData() + { + Schema = null!; + } + + public CompanySsiProcessData(Guid companySsiDetailId, JsonDocument schema, VerifiedCredentialTypeKindId credentialTypeKindId) + : this() + { + CompanySsiDetailId = companySsiDetailId; + Schema = schema; + CredentialTypeKindId = credentialTypeKindId; + } + + public Guid CompanySsiDetailId { get; set; } + public JsonDocument Schema { get; set; } + public VerifiedCredentialTypeKindId CredentialTypeKindId { get; set; } + public string? ClientId { get; set; } + public byte[]? ClientSecret { get; set; } + public byte[]? InitializationVector { get; set; } + public int EncryptionMode { get; set; } + public string? HolderWalletUrl { get; set; } + public virtual CompanySsiDetail? CompanySsiDetail { get; private set; } + public virtual VerifiedCredentialTypeKind? CredentialTypeKind { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/Document.cs b/src/database/SsiCredentialIssuer.Entities/Entities/Document.cs new file mode 100644 index 00000000..ac70ee87 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/Document.cs @@ -0,0 +1,83 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.AuditEntities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +[AuditEntityV1(typeof(AuditDocument20240305))] +public class Document : IAuditableV1, IBaseEntity +{ + private Document() + { + DocumentHash = null!; + DocumentName = null!; + DocumentContent = null!; + CompanySsiDetails = new HashSet(); + } + + public Document(Guid id, byte[] documentContent, byte[] documentHash, string documentName, MediaTypeId mediaTypeId, DateTimeOffset dateCreated, DocumentStatusId documentStatusId, DocumentTypeId documentTypeId) + : this() + { + Id = id; + DocumentContent = documentContent; + DocumentHash = documentHash; + DocumentName = documentName; + DateCreated = dateCreated; + DocumentStatusId = documentStatusId; + DocumentTypeId = documentTypeId; + MediaTypeId = mediaTypeId; + } + + public Guid Id { get; private set; } + + public DateTimeOffset DateCreated { get; private set; } + + public byte[] DocumentHash { get; set; } + + public byte[] DocumentContent { get; set; } + + [MaxLength(255)] + public string DocumentName { get; set; } + + public MediaTypeId MediaTypeId { get; set; } + + public DocumentTypeId DocumentTypeId { get; set; } + + public DocumentStatusId DocumentStatusId { get; set; } + + public Guid? CompanyUserId { get; set; } + + [LastChangedV1] + public DateTimeOffset? DateLastChanged { get; set; } + + [LastEditorV1] + public Guid? LastEditorId { get; private set; } + + // Navigation properties + public virtual DocumentType? DocumentType { get; set; } + public virtual MediaType? MediaType { get; set; } + public virtual DocumentStatus? DocumentStatus { get; set; } + + public virtual ICollection CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/DocumentStatus.cs b/src/database/SsiCredentialIssuer.Entities/Entities/DocumentStatus.cs new file mode 100644 index 00000000..425bb472 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/DocumentStatus.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class DocumentStatus +{ + private DocumentStatus() + { + Label = null!; + Documents = new HashSet(); + } + + public DocumentStatus(DocumentStatusId documentStatusId) : this() + { + Id = documentStatusId; + Label = documentStatusId.ToString(); + } + + public DocumentStatusId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection Documents { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/DocumentType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/DocumentType.cs new file mode 100644 index 00000000..602c03c9 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/DocumentType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class DocumentType +{ + private DocumentType() + { + Label = null!; + Documents = new HashSet(); + } + + public DocumentType(DocumentTypeId documentTypeId) : this() + { + Id = documentTypeId; + Label = documentTypeId.ToString(); + } + + public DocumentTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection Documents { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/ExpiryCheckType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/ExpiryCheckType.cs new file mode 100644 index 00000000..3288b6ea --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/ExpiryCheckType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class ExpiryCheckType +{ + private ExpiryCheckType() + { + Label = null!; + CompanySsiDetails = new HashSet(); + } + + public ExpiryCheckType(ExpiryCheckTypeId expiryCheckId) + : this() + { + Id = expiryCheckId; + Label = expiryCheckId.ToString(); + CompanySsiDetails = new HashSet(); + } + + public ExpiryCheckTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + + public virtual ICollection CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/MediaType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/MediaType.cs new file mode 100644 index 00000000..274f3881 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/MediaType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class MediaType +{ + private MediaType() + { + Label = null!; + Documents = new HashSet(); + } + + public MediaType(MediaTypeId mediaTypeId) : this() + { + Id = mediaTypeId; + Label = mediaTypeId.ToString(); + } + + public MediaTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection Documents { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/Process.cs b/src/database/SsiCredentialIssuer.Entities/Entities/Process.cs new file mode 100644 index 00000000..41448123 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/Process.cs @@ -0,0 +1,55 @@ +/******************************************************************************** + * 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.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class Process : IBaseEntity, ILockableEntity +{ + private Process() + { + ProcessSteps = new HashSet(); + CompanySsiDetails = 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 CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/ProcessStep.cs b/src/database/SsiCredentialIssuer.Entities/Entities/ProcessStep.cs new file mode 100644 index 00000000..13da78d4 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/ProcessStep.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Attributes; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class ProcessStep : IBaseEntity +{ + 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; } + + [LastChangedV1] + 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/SsiCredentialIssuer.Entities/Entities/ProcessStepStatus.cs b/src/database/SsiCredentialIssuer.Entities/Entities/ProcessStepStatus.cs new file mode 100644 index 00000000..a62480b7 --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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/SsiCredentialIssuer.Entities/Entities/ProcessStepType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/ProcessStepType.cs new file mode 100644 index 00000000..d175f667 --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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/SsiCredentialIssuer.Entities/Entities/ProcessType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/ProcessType.cs new file mode 100644 index 00000000..87c8860a --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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/SsiCredentialIssuer.Entities/Entities/UseCase.cs b/src/database/SsiCredentialIssuer.Entities/Entities/UseCase.cs new file mode 100644 index 00000000..69be03e8 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/UseCase.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class UseCase : IBaseEntity +{ + private UseCase() + { + Name = null!; + Shortname = null!; + } + + public UseCase(Guid id, string name, string shortname) : this() + { + Id = id; + Name = name; + Shortname = shortname; + } + + public Guid Id { get; private set; } + + [MaxLength(255)] + public string Name { get; set; } + + [MaxLength(255)] + public string Shortname { get; set; } + + // Navigation properties + public virtual VerifiedCredentialTypeAssignedUseCase? VerifiedCredentialAssignedUseCase { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalType.cs new file mode 100644 index 00000000..512124d7 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialExternalType +{ + public VerifiedCredentialExternalType(VerifiedCredentialExternalTypeId id) + { + Id = id; + Label = id.ToString(); + + VerifiedCredentialTypeAssignedExternalTypes = new HashSet(); + VerifiedCredentialExternalTypeDetailVersions = new HashSet(); + } + + public VerifiedCredentialExternalTypeId Id { get; private set; } + + public string Label { get; private set; } + + // Navigation properties + public virtual ICollection VerifiedCredentialTypeAssignedExternalTypes { get; private set; } + + public virtual ICollection VerifiedCredentialExternalTypeDetailVersions { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalTypeDetailVersion.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalTypeDetailVersion.cs new file mode 100644 index 00000000..f4a8f9b6 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialExternalTypeDetailVersion.cs @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialExternalTypeDetailVersion : IBaseEntity +{ + public VerifiedCredentialExternalTypeDetailVersion() + { + CompanySsiDetails = new HashSet(); + } + + public Guid Id { get; set; } + public VerifiedCredentialExternalTypeId VerifiedCredentialExternalTypeId { get; set; } + public string? Version { get; set; } + public string? Template { get; set; } + public DateTimeOffset ValidFrom { get; set; } + public DateTimeOffset Expiry { get; set; } + + public virtual VerifiedCredentialExternalType? VerifiedCredentialExternalType { get; private set; } + public virtual ICollection CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialType.cs new file mode 100644 index 00000000..863d8421 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialType +{ + private VerifiedCredentialType() + { + Label = null!; + CompanySsiDetails = new HashSet(); + } + + public VerifiedCredentialType(VerifiedCredentialTypeId verifiedCredentialTypeId) + { + Id = verifiedCredentialTypeId; + Label = verifiedCredentialTypeId.ToString(); + CompanySsiDetails = new HashSet(); + } + + public VerifiedCredentialTypeId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation properties + public virtual VerifiedCredentialTypeAssignedExternalType? VerifiedCredentialTypeAssignedExternalType { get; private set; } + public virtual VerifiedCredentialTypeAssignedKind? VerifiedCredentialTypeAssignedKind { get; set; } + + public virtual VerifiedCredentialTypeAssignedUseCase? VerifiedCredentialTypeAssignedUseCase { get; set; } + + public virtual ICollection CompanySsiDetails { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedExternalType.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedExternalType.cs new file mode 100644 index 00000000..2ae5f28b --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedExternalType.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialTypeAssignedExternalType +{ + public VerifiedCredentialTypeAssignedExternalType(VerifiedCredentialTypeId verifiedCredentialTypeId, VerifiedCredentialExternalTypeId verifiedCredentialExternalTypeId) + { + VerifiedCredentialTypeId = verifiedCredentialTypeId; + VerifiedCredentialExternalTypeId = verifiedCredentialExternalTypeId; + } + + /// + /// Id of the credential type. + /// + public VerifiedCredentialTypeId VerifiedCredentialTypeId { get; private set; } + + /// + /// Id of the credential type kind. + /// + public VerifiedCredentialExternalTypeId VerifiedCredentialExternalTypeId { get; private set; } + + // Navigation properties + public virtual VerifiedCredentialType? VerifiedCredentialType { get; private set; } + + public virtual VerifiedCredentialExternalType? VerifiedCredentialExternalType { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedKind.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedKind.cs new file mode 100644 index 00000000..b1a174d5 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedKind.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialTypeAssignedKind +{ + public VerifiedCredentialTypeAssignedKind(VerifiedCredentialTypeId verifiedCredentialTypeId, VerifiedCredentialTypeKindId verifiedCredentialTypeKindId) + { + VerifiedCredentialTypeId = verifiedCredentialTypeId; + VerifiedCredentialTypeKindId = verifiedCredentialTypeKindId; + } + + /// + /// Id of the credential type. + /// + public VerifiedCredentialTypeId VerifiedCredentialTypeId { get; private set; } + + /// + /// Id of the credential type kind. + /// + public VerifiedCredentialTypeKindId VerifiedCredentialTypeKindId { get; private set; } + + // Navigation properties + public virtual VerifiedCredentialType? VerifiedCredentialType { get; private set; } + + public virtual VerifiedCredentialTypeKind? VerifiedCredentialTypeKind { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedUseCase.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedUseCase.cs new file mode 100644 index 00000000..8924c076 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeAssignedUseCase.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialTypeAssignedUseCase +{ + public VerifiedCredentialTypeAssignedUseCase(VerifiedCredentialTypeId verifiedCredentialTypeId, Guid useCaseId) + { + VerifiedCredentialTypeId = verifiedCredentialTypeId; + UseCaseId = useCaseId; + } + + public VerifiedCredentialTypeId VerifiedCredentialTypeId { get; set; } + public Guid UseCaseId { get; set; } + + // Navigation Properties + public virtual VerifiedCredentialType? VerifiedCredentialType { get; set; } + public virtual UseCase? UseCase { get; set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeKind.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeKind.cs new file mode 100644 index 00000000..0814fb02 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Entities/VerifiedCredentialTypeKind.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public class VerifiedCredentialTypeKind +{ + private VerifiedCredentialTypeKind() + { + Label = null!; + VerifiedCredentialTypeAssignedKinds = new HashSet(); + CompanySsiProcessData = new HashSet(); + } + + public VerifiedCredentialTypeKind(VerifiedCredentialTypeKindId verifiedCredentialTypeKindId) + : this() + { + Id = verifiedCredentialTypeKindId; + Label = verifiedCredentialTypeKindId.ToString(); + VerifiedCredentialTypeAssignedKinds = new HashSet(); + } + + public VerifiedCredentialTypeKindId Id { get; private set; } + + [MaxLength(255)] + public string Label { get; private set; } + + // Navigation Properties + public virtual ICollection VerifiedCredentialTypeAssignedKinds { get; private set; } + public virtual ICollection CompanySsiProcessData { get; private set; } +} diff --git a/src/database/SsiCredentialIssuer.Entities/Entities/VerifyProcessData.cs b/src/database/SsiCredentialIssuer.Entities/Entities/VerifyProcessData.cs new file mode 100644 index 00000000..20004488 --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +public record VerifyProcessData( + Process? Process, + IEnumerable? ProcessSteps +); diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/CompanySsiDetailStatusId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/CompanySsiDetailStatusId.cs new file mode 100644 index 00000000..ace621fc --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/CompanySsiDetailStatusId.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum CompanySsiDetailStatusId +{ + PENDING = 1, + ACTIVE = 2, + INACTIVE = 3 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/DocumentStatusId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/DocumentStatusId.cs new file mode 100644 index 00000000..96d3d43a --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/DocumentStatusId.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +/// +/// Possible status for documents +/// +public enum DocumentStatusId +{ + /// + /// The document is active for changes + /// + ACTIVE = 2, + + /// + /// The document was deleted by the user + /// + INACTIVE = 3, +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/DocumentTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/DocumentTypeId.cs new file mode 100644 index 00000000..d9502810 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/DocumentTypeId.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum DocumentTypeId +{ + PRESENTATION = 1, + CREDENTIAL = 2, + VERIFIED_CREDENTIAL = 3 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ExpiryCheckTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ExpiryCheckTypeId.cs new file mode 100644 index 00000000..3280b9c9 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/ExpiryCheckTypeId.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum ExpiryCheckTypeId +{ + ONE_MONTH = 1, + TWO_WEEKS = 2, + ONE_DAY = 3 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/MediaTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/MediaTypeId.cs new file mode 100644 index 00000000..f322bdf6 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/MediaTypeId.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum MediaTypeId +{ + JPEG = 1, + GIF = 2, + PNG = 3, + SVG = 4, + TIFF = 5, + PDF = 6, + JSON = 7, + PEM = 8, + CA_CERT = 9, + PKX_CER = 10, + OCTET = 11 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepStatusId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepStatusId.cs new file mode 100644 index 00000000..9450d24b --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum ProcessStepStatusId +{ + TODO = 1, + DONE = 2, + SKIPPED = 3, + FAILED = 4, + DUPLICATE = 5 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs new file mode 100644 index 00000000..9e65e8b5 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum ProcessStepTypeId +{ + // ApplicationChecklist Process + CREATE_CREDENTIAL = 1, + SIGN_CREDENTIAL = 2, + SAVE_CREDENTIAL_DOCUMENT = 3, + CREATE_CREDENTIAL_FOR_HOLDER = 4 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs new file mode 100644 index 00000000..a07c1bca --- /dev/null +++ b/src/database/SsiCredentialIssuer.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum ProcessTypeId +{ + CREATE_CREDENTIAL = 1, +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialExternalTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialExternalTypeId.cs new file mode 100644 index 00000000..fbb6178c --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialExternalTypeId.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 System.Runtime.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum VerifiedCredentialExternalTypeId +{ + [EnumMember(Value = "TraceabilityCredential")] + TRACEABILITY_CREDENTIAL = 1, + + [EnumMember(Value = "PcfCredential")] + PCF_CREDENTIAL = 2, + + [EnumMember(Value = "BehaviorTwinCredential")] + BEHAVIOR_TWIN_CREDENTIAL = 3, + + [EnumMember(Value = "vehicleDismantle")] + VEHICLE_DISMANTLE = 4, + + [EnumMember(Value = "SustainabilityCredential")] + SUSTAINABILITY_CREDENTIAL = 5, + + [EnumMember(Value = "QualityCredential")] + QUALITY_CREDENTIAL = 6, + + [EnumMember(Value = "BusinessPartnerCredential")] + BUSINESS_PARTNER_NUMBER = 7 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeId.cs new file mode 100644 index 00000000..725f9d22 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeId.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 System.Runtime.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum VerifiedCredentialTypeId +{ + [EnumMember(Value = "Traceability Framework")] + TRACEABILITY_FRAMEWORK = 1, + + [EnumMember(Value = "PCF Framework")] + PCF_FRAMEWORK = 2, + + [EnumMember(Value = "Behavior Twin Framework")] + BEHAVIOR_TWIN_FRAMEWORK = 3, + + [EnumMember(Value = "Dismantler Certificate")] + DISMANTLER_CERTIFICATE = 4, + + [EnumMember(Value = "Sustainability Framework")] + SUSTAINABILITY_FRAMEWORK = 5, + + [EnumMember(Value = "frameworkAgreement.quality")] + FRAMEWORK_AGREEMENT_QUALITY = 6, + + [EnumMember(Value = "BusinessPartnerCredential")] + BUSINESS_PARTNER_NUMBER = 7 +} diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeKindId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeKindId.cs new file mode 100644 index 00000000..23581709 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/Enums/VerifiedCredentialTypeKindId.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +public enum VerifiedCredentialTypeKindId +{ + FRAMEWORK = 1, + MEMBERSHIP = 2, + BPN = 3 +} diff --git a/src/database/SsiCredentialIssuer.Entities/IssuerDbContext.cs b/src/database/SsiCredentialIssuer.Entities/IssuerDbContext.cs new file mode 100644 index 00000000..65274bdc --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/IssuerDbContext.cs @@ -0,0 +1,318 @@ +/******************************************************************************** + * 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; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Extensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; + +public class IssuerDbContext : DbContext +{ + private readonly IAuditHandler _auditHandler; + + protected IssuerDbContext() + { + throw new InvalidOperationException("IdentityService should never be null"); + } + + public IssuerDbContext(DbContextOptions options, IAuditHandler auditHandler) + : base(options) + { + _auditHandler = auditHandler; + } + + public virtual DbSet AuditCompanySsiDetail20240228 { get; set; } = default!; + public virtual DbSet AuditDocument20240305 { get; set; } = default!; + public virtual DbSet CompanySsiDetails { get; set; } = default!; + public virtual DbSet CompanySsiDetailAssignedDocuments { get; set; } = default!; + public virtual DbSet CompanySsiDetailStatuses { get; set; } = default!; + public virtual DbSet CompanySsiProcessData { get; set; } = default!; + public virtual DbSet Documents { get; set; } = default!; + public virtual DbSet DocumentStatus { get; set; } = default!; + public virtual DbSet DocumentTypes { get; set; } = default!; + public virtual DbSet ExpiryCheckTypes { get; set; } = default!; + public virtual DbSet MediaTypes { get; set; } = default!; + 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 UseCases { get; set; } = default!; + public virtual DbSet VerifiedCredentialExternalTypes { get; set; } = default!; + public virtual DbSet VerifiedCredentialExternalTypeDetailVersions { get; set; } = default!; + public virtual DbSet VerifiedCredentialTypes { get; set; } = default!; + public virtual DbSet VerifiedCredentialTypeAssignedExternalTypes { get; set; } = default!; + public virtual DbSet VerifiedCredentialTypeAssignedKinds { get; set; } = default!; + public virtual DbSet VerifiedCredentialTypeAssignedUseCases { get; set; } = default!; + public virtual DbSet VerifiedCredentialTypeKinds { 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("issuer"); + + modelBuilder.Entity(entity => + { + entity.HasOne(c => c.Process) + .WithMany(c => c.CompanySsiDetails) + .HasForeignKey(t => t.ProcessId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(c => c.VerifiedCredentialType) + .WithMany(c => c.CompanySsiDetails) + .HasForeignKey(t => t.VerifiedCredentialTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(c => c.CompanySsiDetailStatus) + .WithMany(c => c.CompanySsiDetails) + .HasForeignKey(t => t.CompanySsiDetailStatusId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(c => c.VerifiedCredentialExternalTypeDetailVersion) + .WithMany(c => c.CompanySsiDetails) + .HasForeignKey(t => t.VerifiedCredentialExternalTypeDetailVersionId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasMany(t => t.Documents) + .WithMany(o => o.CompanySsiDetails) + .UsingEntity( + j => j + .HasOne(d => d.Document!) + .WithMany() + .HasForeignKey(d => d.DocumentId) + .OnDelete(DeleteBehavior.ClientSetNull), + j => j + .HasOne(d => d.CompanySsiDetail!) + .WithMany() + .HasForeignKey(d => d.CompanySsiDetailId) + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey(e => new { e.DocumentId, e.CompanySsiDetailId }); + }); + + entity.HasAuditV1Triggers(); + }); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(CompanySsiDetailStatusId)) + .Cast() + .Select(e => new CompanySsiDetailStatus(e)) + ); + + modelBuilder.Entity(e => + { + e.HasKey(x => x.CompanySsiDetailId); + + e.HasOne(x => x.CompanySsiDetail) + .WithOne(x => x.CompanySsiProcessData) + .HasForeignKey(x => x.CompanySsiDetailId); + + e.HasOne(x => x.CredentialTypeKind) + .WithMany(x => x.CompanySsiProcessData) + .HasForeignKey(x => x.CredentialTypeKindId); + }); + + modelBuilder.Entity(entity => + { + entity.HasAuditV1Triggers(); + }); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(DocumentStatusId)) + .Cast() + .Select(e => new DocumentStatus(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(DocumentTypeId)) + .Cast() + .Select(e => new DocumentType(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(ExpiryCheckTypeId)) + .Cast() + .Select(e => new ExpiryCheckType(e)) + ); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(MediaTypeId)) + .Cast() + .Select(e => new MediaType(e)) + ); + + 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(); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(VerifiedCredentialExternalTypeId)) + .Cast() + .Select(e => new VerifiedCredentialExternalType(e)) + ); + + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.VerifiedCredentialExternalType) + .WithMany(x => x.VerifiedCredentialExternalTypeDetailVersions) + .HasForeignKey(d => d.VerifiedCredentialExternalTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasIndex(e => new { e.VerifiedCredentialExternalTypeId, e.Version }) + .IsUnique(); + }); + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(VerifiedCredentialTypeId)) + .Cast() + .Select(e => new VerifiedCredentialType(e)) + ); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => new { e.VerifiedCredentialTypeId, e.VerifiedCredentialExternalTypeId }); + + entity.HasOne(d => d.VerifiedCredentialType) + .WithOne(x => x.VerifiedCredentialTypeAssignedExternalType) + .HasForeignKey(d => d.VerifiedCredentialTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.VerifiedCredentialExternalType) + .WithMany(x => x.VerifiedCredentialTypeAssignedExternalTypes) + .HasForeignKey(d => d.VerifiedCredentialExternalTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => new { e.VerifiedCredentialTypeId, e.VerifiedCredentialTypeKindId }); + + entity.HasOne(d => d.VerifiedCredentialTypeKind) + .WithMany(x => x.VerifiedCredentialTypeAssignedKinds) + .HasForeignKey(d => d.VerifiedCredentialTypeKindId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.VerifiedCredentialType) + .WithOne(x => x.VerifiedCredentialTypeAssignedKind) + .HasForeignKey(d => d.VerifiedCredentialTypeId) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasIndex(x => x.VerifiedCredentialTypeId) + .IsUnique(false); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(x => new { x.VerifiedCredentialTypeId, x.UseCaseId }); + + entity.HasOne(c => c.VerifiedCredentialType) + .WithOne(c => c.VerifiedCredentialTypeAssignedUseCase) + .HasForeignKey(c => c.VerifiedCredentialTypeId); + + entity.HasOne(c => c.UseCase) + .WithOne(c => c.VerifiedCredentialAssignedUseCase) + .HasForeignKey(c => c.UseCaseId); + }); + + modelBuilder.Entity() + .HasData( + Enum.GetValues(typeof(VerifiedCredentialTypeKindId)) + .Cast() + .Select(e => new VerifiedCredentialTypeKind(e)) + ); + } + + /// + public override Task SaveChangesAsync(CancellationToken cancellationToken = new()) + { + EnhanceChangedEntries(); + return base.SaveChangesAsync(cancellationToken); + } + + /// + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + EnhanceChangedEntries(); + return base.SaveChanges(acceptAllChangesOnSuccess); + } + + public override int SaveChanges() + { + EnhanceChangedEntries(); + return base.SaveChanges(); + } + + private void EnhanceChangedEntries() + { + _auditHandler.HandleAuditForChangedEntries( + ChangeTracker.Entries().Where(entry => + entry.State != EntityState.Unchanged && entry.State != EntityState.Detached && + entry.Entity is IAuditableV1).ToImmutableList(), + ChangeTracker.Context); + } +} diff --git a/src/database/SsiCredentialIssuer.Entities/SsiCredentialIssuer.Entities.csproj b/src/database/SsiCredentialIssuer.Entities/SsiCredentialIssuer.Entities.csproj new file mode 100644 index 00000000..6d397738 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Entities/SsiCredentialIssuer.Entities.csproj @@ -0,0 +1,46 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Entities + Org.Eclipse.TractusX.SsiCredentialIssuer.Entities + net7.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + **/IssuerDbContext.cs + + + diff --git a/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.Designer.cs b/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.Designer.cs new file mode 100644 index 00000000..507f1af5 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.Designer.cs @@ -0,0 +1,1403 @@ +/******************************************************************************** +// * 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 System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; + +#nullable disable + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Migrations +{ + [DbContext(typeof(IssuerDbContext))] + [Migration("20240308143713_0.1.0-rc.1")] + partial class _010rc1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("issuer") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities.AuditCompanySsiDetail20240228", b => + { + b.Property("AuditV1Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("audit_v1id"); + + b.Property("AuditV1DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("audit_v1date_last_changed"); + + b.Property("AuditV1LastEditorId") + .HasColumnType("uuid") + .HasColumnName("audit_v1last_editor_id"); + + b.Property("AuditV1OperationId") + .HasColumnType("integer") + .HasColumnName("audit_v1operation_id"); + + b.Property("Bpnl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpnl"); + + b.Property("CompanySsiDetailStatusId") + .HasColumnType("integer") + .HasColumnName("company_ssi_detail_status_id"); + + b.Property("CreatorUserId") + .HasColumnType("uuid") + .HasColumnName("creator_user_id"); + + b.Property("Credential") + .HasColumnType("text") + .HasColumnName("credential"); + + 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("ExpiryCheckTypeId") + .HasColumnType("integer") + .HasColumnName("expiry_check_type_id"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ExternalCredentialId") + .HasColumnType("uuid") + .HasColumnName("external_credential_id"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("VerifiedCredentialExternalTypeDetailVersionId") + .HasColumnType("uuid") + .HasColumnName("verified_credential_external_type_detail_version_id"); + + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.HasKey("AuditV1Id") + .HasName("pk_audit_company_ssi_detail20240228"); + + b.ToTable("audit_company_ssi_detail20240228", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities.AuditDocument20240305", b => + { + b.Property("AuditV1Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("audit_v1id"); + + b.Property("AuditV1DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("audit_v1date_last_changed"); + + b.Property("AuditV1LastEditorId") + .HasColumnType("uuid") + .HasColumnName("audit_v1last_editor_id"); + + b.Property("AuditV1OperationId") + .HasColumnType("integer") + .HasColumnName("audit_v1operation_id"); + + b.Property("CompanyUserId") + .HasColumnType("uuid") + .HasColumnName("company_user_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("DocumentContent") + .HasColumnType("bytea") + .HasColumnName("document_content"); + + b.Property("DocumentHash") + .HasColumnType("bytea") + .HasColumnName("document_hash"); + + b.Property("DocumentName") + .HasColumnType("text") + .HasColumnName("document_name"); + + b.Property("DocumentStatusId") + .HasColumnType("integer") + .HasColumnName("document_status_id"); + + b.Property("DocumentTypeId") + .HasColumnType("integer") + .HasColumnName("document_type_id"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.HasKey("AuditV1Id") + .HasName("pk_audit_document20240305"); + + b.ToTable("audit_document20240305", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Bpnl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpnl"); + + b.Property("CompanySsiDetailStatusId") + .HasColumnType("integer") + .HasColumnName("company_ssi_detail_status_id"); + + b.Property("CreatorUserId") + .HasColumnType("uuid") + .HasColumnName("creator_user_id"); + + b.Property("Credential") + .HasColumnType("text") + .HasColumnName("credential"); + + 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("ExpiryCheckTypeId") + .HasColumnType("integer") + .HasColumnName("expiry_check_type_id"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ExternalCredentialId") + .HasColumnType("uuid") + .HasColumnName("external_credential_id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("VerifiedCredentialExternalTypeDetailVersionId") + .HasColumnType("uuid") + .HasColumnName("verified_credential_external_type_detail_version_id"); + + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.HasKey("Id") + .HasName("pk_company_ssi_details"); + + b.HasIndex("CompanySsiDetailStatusId") + .HasDatabaseName("ix_company_ssi_details_company_ssi_detail_status_id"); + + b.HasIndex("ExpiryCheckTypeId") + .HasDatabaseName("ix_company_ssi_details_expiry_check_type_id"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_company_ssi_details_process_id"); + + b.HasIndex("VerifiedCredentialExternalTypeDetailVersionId") + .HasDatabaseName("ix_company_ssi_details_verified_credential_external_type_detai"); + + b.HasIndex("VerifiedCredentialTypeId") + .HasDatabaseName("ix_company_ssi_details_verified_credential_type_id"); + + b.ToTable("company_ssi_details", "issuer", t => + { + t.HasTrigger("LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL"); + + t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL"); + }); + + b + .HasAnnotation("LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL AFTER INSERT\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"();") + .HasAnnotation("LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL AFTER UPDATE\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"();"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailAssignedDocument", b => + { + b.Property("DocumentId") + .HasColumnType("uuid") + .HasColumnName("document_id"); + + b.Property("CompanySsiDetailId") + .HasColumnType("uuid") + .HasColumnName("company_ssi_detail_id"); + + b.HasKey("DocumentId", "CompanySsiDetailId") + .HasName("pk_company_ssi_detail_assigned_documents"); + + b.HasIndex("CompanySsiDetailId") + .HasDatabaseName("ix_company_ssi_detail_assigned_documents_company_ssi_detail_id"); + + b.ToTable("company_ssi_detail_assigned_documents", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", 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_company_ssi_detail_statuses"); + + b.ToTable("company_ssi_detail_statuses", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "PENDING" + }, + new + { + Id = 2, + Label = "ACTIVE" + }, + new + { + Id = 3, + Label = "INACTIVE" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", b => + { + b.Property("CompanySsiDetailId") + .HasColumnType("uuid") + .HasColumnName("company_ssi_detail_id"); + + b.Property("ClientId") + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("bytea") + .HasColumnName("client_secret"); + + b.Property("CredentialTypeKindId") + .HasColumnType("integer") + .HasColumnName("credential_type_kind_id"); + + b.Property("EncryptionMode") + .HasColumnType("integer") + .HasColumnName("encryption_mode"); + + b.Property("HolderWalletUrl") + .HasColumnType("text") + .HasColumnName("holder_wallet_url"); + + b.Property("InitializationVector") + .HasColumnType("bytea") + .HasColumnName("initialization_vector"); + + b.Property("Schema") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("schema"); + + b.HasKey("CompanySsiDetailId") + .HasName("pk_company_ssi_process_data"); + + b.HasIndex("CredentialTypeKindId") + .HasDatabaseName("ix_company_ssi_process_data_credential_type_kind_id"); + + b.ToTable("company_ssi_process_data", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CompanyUserId") + .HasColumnType("uuid") + .HasColumnName("company_user_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("DocumentContent") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("document_content"); + + b.Property("DocumentHash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("document_hash"); + + b.Property("DocumentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("document_name"); + + b.Property("DocumentStatusId") + .HasColumnType("integer") + .HasColumnName("document_status_id"); + + b.Property("DocumentTypeId") + .HasColumnType("integer") + .HasColumnName("document_type_id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.HasKey("Id") + .HasName("pk_documents"); + + b.HasIndex("DocumentStatusId") + .HasDatabaseName("ix_documents_document_status_id"); + + b.HasIndex("DocumentTypeId") + .HasDatabaseName("ix_documents_document_type_id"); + + b.HasIndex("MediaTypeId") + .HasDatabaseName("ix_documents_media_type_id"); + + b.ToTable("documents", "issuer", t => + { + t.HasTrigger("LC_TRIGGER_AFTER_INSERT_DOCUMENT"); + + t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_DOCUMENT"); + }); + + b + .HasAnnotation("LC_TRIGGER_AFTER_INSERT_DOCUMENT", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_DOCUMENT AFTER INSERT\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"();") + .HasAnnotation("LC_TRIGGER_AFTER_UPDATE_DOCUMENT", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_DOCUMENT AFTER UPDATE\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"();"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", 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_document_status"); + + b.ToTable("document_status", "issuer"); + + b.HasData( + new + { + Id = 2, + Label = "ACTIVE" + }, + new + { + Id = 3, + Label = "INACTIVE" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", 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_document_types"); + + b.ToTable("document_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "PRESENTATION" + }, + new + { + Id = 2, + Label = "CREDENTIAL" + }, + new + { + Id = 3, + Label = "VERIFIED_CREDENTIAL" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", 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_expiry_check_types"); + + b.ToTable("expiry_check_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "ONE_MONTH" + }, + new + { + Id = 2, + Label = "TWO_WEEKS" + }, + new + { + Id = 3, + Label = "ONE_DAY" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", 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_media_types"); + + b.ToTable("media_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "JPEG" + }, + new + { + Id = 2, + Label = "GIF" + }, + new + { + Id = 3, + Label = "PNG" + }, + new + { + Id = 4, + Label = "SVG" + }, + new + { + Id = 5, + Label = "TIFF" + }, + new + { + Id = 6, + Label = "PDF" + }, + new + { + Id = 7, + Label = "JSON" + }, + new + { + Id = 8, + Label = "PEM" + }, + new + { + Id = 9, + Label = "CA_CERT" + }, + new + { + Id = 10, + Label = "PKX_CER" + }, + new + { + Id = 11, + Label = "OCTET" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + 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("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_CREDENTIAL" + }, + new + { + Id = 2, + Label = "SIGN_CREDENTIAL" + }, + new + { + Id = 3, + Label = "SAVE_CREDENTIAL_DOCUMENT" + }, + new + { + Id = 4, + Label = "CREATE_CREDENTIAL_FOR_HOLDER" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_CREDENTIAL" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Shortname") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("shortname"); + + b.HasKey("Id") + .HasName("pk_use_cases"); + + b.ToTable("use_cases", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_verified_credential_external_types"); + + b.ToTable("verified_credential_external_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "TRACEABILITY_CREDENTIAL" + }, + new + { + Id = 2, + Label = "PCF_CREDENTIAL" + }, + new + { + Id = 3, + Label = "BEHAVIOR_TWIN_CREDENTIAL" + }, + new + { + Id = 4, + Label = "VEHICLE_DISMANTLE" + }, + new + { + Id = 5, + Label = "SUSTAINABILITY_CREDENTIAL" + }, + new + { + Id = 6, + Label = "Quality_Credential" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Expiry") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry"); + + b.Property("Template") + .HasColumnType("text") + .HasColumnName("template"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_from"); + + b.Property("VerifiedCredentialExternalTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_external_type_id"); + + b.Property("Version") + .HasColumnType("text") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_verified_credential_external_type_detail_versions"); + + b.HasIndex("VerifiedCredentialExternalTypeId", "Version") + .IsUnique() + .HasDatabaseName("ix_verified_credential_external_type_detail_versions_verified_"); + + b.ToTable("verified_credential_external_type_detail_versions", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", 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_verified_credential_types"); + + b.ToTable("verified_credential_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "TRACEABILITY_FRAMEWORK" + }, + new + { + Id = 2, + Label = "PCF_FRAMEWORK" + }, + new + { + Id = 3, + Label = "BEHAVIOR_TWIN_FRAMEWORK" + }, + new + { + Id = 4, + Label = "DISMANTLER_CERTIFICATE" + }, + new + { + Id = 5, + Label = "SUSTAINABILITY_FRAMEWORK" + }, + new + { + Id = 6, + Label = "FRAMEWORK_AGREEMENT_QUALITY" + }, + new + { + Id = 7, + Label = "BUSINESS_PARTNER_NUMBER" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("VerifiedCredentialExternalTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_external_type_id"); + + b.HasKey("VerifiedCredentialTypeId", "VerifiedCredentialExternalTypeId") + .HasName("pk_verified_credential_type_assigned_external_types"); + + b.HasIndex("VerifiedCredentialExternalTypeId") + .HasDatabaseName("ix_verified_credential_type_assigned_external_types_verified_c"); + + b.HasIndex("VerifiedCredentialTypeId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_external_types_verified_c1"); + + b.ToTable("verified_credential_type_assigned_external_types", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("VerifiedCredentialTypeKindId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_kind_id"); + + b.HasKey("VerifiedCredentialTypeId", "VerifiedCredentialTypeKindId") + .HasName("pk_verified_credential_type_assigned_kinds"); + + b.HasIndex("VerifiedCredentialTypeId") + .HasDatabaseName("ix_verified_credential_type_assigned_kinds_verified_credential"); + + b.HasIndex("VerifiedCredentialTypeKindId") + .HasDatabaseName("ix_verified_credential_type_assigned_kinds_verified_credential1"); + + b.ToTable("verified_credential_type_assigned_kinds", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("UseCaseId") + .HasColumnType("uuid") + .HasColumnName("use_case_id"); + + b.HasKey("VerifiedCredentialTypeId", "UseCaseId") + .HasName("pk_verified_credential_type_assigned_use_cases"); + + b.HasIndex("UseCaseId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_use_cases_use_case_id"); + + b.HasIndex("VerifiedCredentialTypeId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_use_cases_verified_creden"); + + b.ToTable("verified_credential_type_assigned_use_cases", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", 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_verified_credential_type_kinds"); + + b.ToTable("verified_credential_type_kinds", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "FRAMEWORK" + }, + new + { + Id = 2, + Label = "MEMBERSHIP" + }, + new + { + Id = 3, + Label = "BPN" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", "CompanySsiDetailStatus") + .WithMany("CompanySsiDetails") + .HasForeignKey("CompanySsiDetailStatusId") + .IsRequired() + .HasConstraintName("fk_company_ssi_details_company_ssi_detail_statuses_company_ssi"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", "ExpiryCheckType") + .WithMany("CompanySsiDetails") + .HasForeignKey("ExpiryCheckTypeId") + .HasConstraintName("fk_company_ssi_details_expiry_check_types_expiry_check_type_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", "Process") + .WithMany("CompanySsiDetails") + .HasForeignKey("ProcessId") + .HasConstraintName("fk_company_ssi_details_processes_process_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", "VerifiedCredentialExternalTypeDetailVersion") + .WithMany("CompanySsiDetails") + .HasForeignKey("VerifiedCredentialExternalTypeDetailVersionId") + .HasConstraintName("fk_company_ssi_details_verified_credential_external_type_detai"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithMany("CompanySsiDetails") + .HasForeignKey("VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_company_ssi_details_verified_credential_types_verified_cred"); + + b.Navigation("CompanySsiDetailStatus"); + + b.Navigation("ExpiryCheckType"); + + b.Navigation("Process"); + + b.Navigation("VerifiedCredentialExternalTypeDetailVersion"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailAssignedDocument", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", "CompanySsiDetail") + .WithMany() + .HasForeignKey("CompanySsiDetailId") + .IsRequired() + .HasConstraintName("fk_company_ssi_detail_assigned_documents_company_ssi_details_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .IsRequired() + .HasConstraintName("fk_company_ssi_detail_assigned_documents_documents_document_id"); + + b.Navigation("CompanySsiDetail"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", "CompanySsiDetail") + .WithOne("CompanySsiProcessData") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", "CompanySsiDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_company_ssi_process_data_company_ssi_details_company_ssi_de"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", "CredentialTypeKind") + .WithMany("CompanySsiProcessData") + .HasForeignKey("CredentialTypeKindId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_company_ssi_process_data_verified_credential_type_kinds_cre"); + + b.Navigation("CompanySsiDetail"); + + b.Navigation("CredentialTypeKind"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", "DocumentStatus") + .WithMany("Documents") + .HasForeignKey("DocumentStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_document_status_document_status_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", "DocumentType") + .WithMany("Documents") + .HasForeignKey("DocumentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_document_types_document_type_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", "MediaType") + .WithMany("Documents") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_media_types_media_type_id"); + + b.Navigation("DocumentStatus"); + + b.Navigation("DocumentType"); + + b.Navigation("MediaType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessType", "ProcessType") + .WithMany("Processes") + .HasForeignKey("ProcessTypeId") + .IsRequired() + .HasConstraintName("fk_processes_process_types_process_type_id"); + + b.Navigation("ProcessType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStep", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", "Process") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_process_steps_processes_process_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.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("Org.Eclipse.TractusX.SsiCredentialIssuer.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("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", "VerifiedCredentialExternalType") + .WithMany("VerifiedCredentialExternalTypeDetailVersions") + .HasForeignKey("VerifiedCredentialExternalTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_external_type_detail_versions_verified_"); + + b.Navigation("VerifiedCredentialExternalType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", "VerifiedCredentialExternalType") + .WithMany("VerifiedCredentialTypeAssignedExternalTypes") + .HasForeignKey("VerifiedCredentialExternalTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_external_types_verified_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedExternalType") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", "VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_external_types_verified_c1"); + + b.Navigation("VerifiedCredentialExternalType"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedKind") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", "VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_kinds_verified_credential"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", "VerifiedCredentialTypeKind") + .WithMany("VerifiedCredentialTypeAssignedKinds") + .HasForeignKey("VerifiedCredentialTypeKindId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_kinds_verified_credential1"); + + b.Navigation("VerifiedCredentialType"); + + b.Navigation("VerifiedCredentialTypeKind"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", "UseCase") + .WithOne("VerifiedCredentialAssignedUseCase") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", "UseCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_use_cases_use_cases_use_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedUseCase") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", "VerifiedCredentialTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_use_cases_verified_creden"); + + b.Navigation("UseCase"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.Navigation("CompanySsiProcessData"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", b => + { + b.Navigation("CompanySsiDetails"); + + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStepStatus", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStepType", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessType", b => + { + b.Navigation("Processes"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", b => + { + b.Navigation("VerifiedCredentialAssignedUseCase"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", b => + { + b.Navigation("VerifiedCredentialExternalTypeDetailVersions"); + + b.Navigation("VerifiedCredentialTypeAssignedExternalTypes"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", b => + { + b.Navigation("CompanySsiDetails"); + + b.Navigation("VerifiedCredentialTypeAssignedExternalType"); + + b.Navigation("VerifiedCredentialTypeAssignedKind"); + + b.Navigation("VerifiedCredentialTypeAssignedUseCase"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", b => + { + b.Navigation("CompanySsiProcessData"); + + b.Navigation("VerifiedCredentialTypeAssignedKinds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.cs b/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.cs new file mode 100644 index 00000000..02a59bae --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Migrations/20240308143713_0.1.0-rc.1.cs @@ -0,0 +1,946 @@ +/******************************************************************************** + * 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; +using System.Text.Json; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Migrations +{ + /// + public partial class _010rc1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "issuer"); + + migrationBuilder.CreateTable( + name: "audit_company_ssi_detail20240228", + schema: "issuer", + columns: table => new + { + audit_v1id = table.Column(type: "uuid", nullable: false), + id = table.Column(type: "uuid", nullable: false), + bpnl = table.Column(type: "text", nullable: false), + verified_credential_type_id = table.Column(type: "integer", nullable: false), + company_ssi_detail_status_id = table.Column(type: "integer", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: false), + creator_user_id = table.Column(type: "uuid", nullable: false), + expiry_date = table.Column(type: "timestamp with time zone", nullable: true), + verified_credential_external_type_detail_version_id = table.Column(type: "uuid", nullable: true), + expiry_check_type_id = table.Column(type: "integer", nullable: true), + process_id = table.Column(type: "uuid", nullable: true), + external_credential_id = table.Column(type: "uuid", nullable: true), + credential = table.Column(type: "text", nullable: true), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + last_editor_id = table.Column(type: "uuid", nullable: true), + audit_v1last_editor_id = table.Column(type: "uuid", nullable: true), + audit_v1operation_id = table.Column(type: "integer", nullable: false), + audit_v1date_last_changed = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_audit_company_ssi_detail20240228", x => x.audit_v1id); + }); + + migrationBuilder.CreateTable( + name: "audit_document20240305", + schema: "issuer", + columns: table => new + { + audit_v1id = table.Column(type: "uuid", nullable: false), + id = table.Column(type: "uuid", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: true), + document_hash = table.Column(type: "bytea", nullable: true), + document_content = table.Column(type: "bytea", nullable: true), + document_name = table.Column(type: "text", nullable: true), + media_type_id = table.Column(type: "integer", nullable: true), + document_type_id = table.Column(type: "integer", nullable: true), + document_status_id = table.Column(type: "integer", nullable: true), + company_user_id = table.Column(type: "uuid", nullable: true), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + last_editor_id = table.Column(type: "uuid", nullable: true), + audit_v1date_last_changed = table.Column(type: "timestamp with time zone", nullable: false), + audit_v1last_editor_id = table.Column(type: "uuid", nullable: true), + audit_v1operation_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_audit_document20240305", x => x.audit_v1id); + }); + + migrationBuilder.CreateTable( + name: "company_ssi_detail_statuses", + schema: "issuer", + 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_company_ssi_detail_statuses", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "document_status", + schema: "issuer", + 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_document_status", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "document_types", + schema: "issuer", + 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_document_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "expiry_check_types", + schema: "issuer", + 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_expiry_check_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "media_types", + schema: "issuer", + 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_media_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "process_step_statuses", + schema: "issuer", + 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: "issuer", + 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: "issuer", + 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: "use_cases", + schema: "issuer", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + shortname = table.Column(type: "character varying(255)", maxLength: 255, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_use_cases", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_external_types", + schema: "issuer", + columns: table => new + { + id = table.Column(type: "integer", nullable: false), + label = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_verified_credential_external_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_type_kinds", + schema: "issuer", + 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_verified_credential_type_kinds", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_types", + schema: "issuer", + 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_verified_credential_types", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "documents", + schema: "issuer", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: false), + document_hash = table.Column(type: "bytea", nullable: false), + document_content = table.Column(type: "bytea", nullable: false), + document_name = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + media_type_id = table.Column(type: "integer", nullable: false), + document_type_id = table.Column(type: "integer", nullable: false), + document_status_id = table.Column(type: "integer", nullable: false), + company_user_id = table.Column(type: "uuid", nullable: true), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + last_editor_id = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_documents", x => x.id); + table.ForeignKey( + name: "fk_documents_document_status_document_status_id", + column: x => x.document_status_id, + principalSchema: "issuer", + principalTable: "document_status", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_documents_document_types_document_type_id", + column: x => x.document_type_id, + principalSchema: "issuer", + principalTable: "document_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_documents_media_types_media_type_id", + column: x => x.media_type_id, + principalSchema: "issuer", + principalTable: "media_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "processes", + schema: "issuer", + 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: "issuer", + principalTable: "process_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_external_type_detail_versions", + schema: "issuer", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + verified_credential_external_type_id = table.Column(type: "integer", nullable: false), + version = table.Column(type: "text", nullable: true), + template = table.Column(type: "text", nullable: true), + valid_from = table.Column(type: "timestamp with time zone", nullable: false), + expiry = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_verified_credential_external_type_detail_versions", x => x.id); + table.ForeignKey( + name: "fk_verified_credential_external_type_detail_versions_verified_", + column: x => x.verified_credential_external_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_external_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_type_assigned_external_types", + schema: "issuer", + columns: table => new + { + verified_credential_type_id = table.Column(type: "integer", nullable: false), + verified_credential_external_type_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_verified_credential_type_assigned_external_types", x => new { x.verified_credential_type_id, x.verified_credential_external_type_id }); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_external_types_verified_c", + column: x => x.verified_credential_external_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_external_types", + principalColumn: "id"); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_external_types_verified_c1", + column: x => x.verified_credential_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_type_assigned_kinds", + schema: "issuer", + columns: table => new + { + verified_credential_type_id = table.Column(type: "integer", nullable: false), + verified_credential_type_kind_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_verified_credential_type_assigned_kinds", x => new { x.verified_credential_type_id, x.verified_credential_type_kind_id }); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_kinds_verified_credential", + column: x => x.verified_credential_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_types", + principalColumn: "id"); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_kinds_verified_credential1", + column: x => x.verified_credential_type_kind_id, + principalSchema: "issuer", + principalTable: "verified_credential_type_kinds", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "verified_credential_type_assigned_use_cases", + schema: "issuer", + columns: table => new + { + verified_credential_type_id = table.Column(type: "integer", nullable: false), + use_case_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_verified_credential_type_assigned_use_cases", x => new { x.verified_credential_type_id, x.use_case_id }); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_use_cases_use_cases_use_c", + column: x => x.use_case_id, + principalSchema: "issuer", + principalTable: "use_cases", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_verified_credential_type_assigned_use_cases_verified_creden", + column: x => x.verified_credential_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "process_steps", + schema: "issuer", + 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: "issuer", + 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: "issuer", + principalTable: "process_step_types", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_process_steps_processes_process_id", + column: x => x.process_id, + principalSchema: "issuer", + principalTable: "processes", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "company_ssi_details", + schema: "issuer", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + bpnl = table.Column(type: "text", nullable: false), + verified_credential_type_id = table.Column(type: "integer", nullable: false), + company_ssi_detail_status_id = table.Column(type: "integer", nullable: false), + date_created = table.Column(type: "timestamp with time zone", nullable: false), + creator_user_id = table.Column(type: "uuid", nullable: false), + expiry_date = table.Column(type: "timestamp with time zone", nullable: true), + verified_credential_external_type_detail_version_id = table.Column(type: "uuid", nullable: true), + expiry_check_type_id = table.Column(type: "integer", nullable: true), + process_id = table.Column(type: "uuid", nullable: true), + external_credential_id = table.Column(type: "uuid", nullable: true), + credential = table.Column(type: "text", nullable: true), + date_last_changed = table.Column(type: "timestamp with time zone", nullable: true), + last_editor_id = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_company_ssi_details", x => x.id); + table.ForeignKey( + name: "fk_company_ssi_details_company_ssi_detail_statuses_company_ssi", + column: x => x.company_ssi_detail_status_id, + principalSchema: "issuer", + principalTable: "company_ssi_detail_statuses", + principalColumn: "id"); + table.ForeignKey( + name: "fk_company_ssi_details_expiry_check_types_expiry_check_type_id", + column: x => x.expiry_check_type_id, + principalSchema: "issuer", + principalTable: "expiry_check_types", + principalColumn: "id"); + table.ForeignKey( + name: "fk_company_ssi_details_processes_process_id", + column: x => x.process_id, + principalSchema: "issuer", + principalTable: "processes", + principalColumn: "id"); + table.ForeignKey( + name: "fk_company_ssi_details_verified_credential_external_type_detai", + column: x => x.verified_credential_external_type_detail_version_id, + principalSchema: "issuer", + principalTable: "verified_credential_external_type_detail_versions", + principalColumn: "id"); + table.ForeignKey( + name: "fk_company_ssi_details_verified_credential_types_verified_cred", + column: x => x.verified_credential_type_id, + principalSchema: "issuer", + principalTable: "verified_credential_types", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "company_ssi_detail_assigned_documents", + schema: "issuer", + columns: table => new + { + document_id = table.Column(type: "uuid", nullable: false), + company_ssi_detail_id = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_company_ssi_detail_assigned_documents", x => new { x.document_id, x.company_ssi_detail_id }); + table.ForeignKey( + name: "fk_company_ssi_detail_assigned_documents_company_ssi_details_c", + column: x => x.company_ssi_detail_id, + principalSchema: "issuer", + principalTable: "company_ssi_details", + principalColumn: "id"); + table.ForeignKey( + name: "fk_company_ssi_detail_assigned_documents_documents_document_id", + column: x => x.document_id, + principalSchema: "issuer", + principalTable: "documents", + principalColumn: "id"); + }); + + migrationBuilder.CreateTable( + name: "company_ssi_process_data", + schema: "issuer", + columns: table => new + { + company_ssi_detail_id = table.Column(type: "uuid", nullable: false), + schema = table.Column(type: "jsonb", nullable: false), + credential_type_kind_id = table.Column(type: "integer", nullable: false), + client_id = table.Column(type: "text", nullable: true), + client_secret = table.Column(type: "bytea", nullable: true), + initialization_vector = table.Column(type: "bytea", nullable: true), + encryption_mode = table.Column(type: "integer", nullable: false), + holder_wallet_url = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_company_ssi_process_data", x => x.company_ssi_detail_id); + table.ForeignKey( + name: "fk_company_ssi_process_data_company_ssi_details_company_ssi_de", + column: x => x.company_ssi_detail_id, + principalSchema: "issuer", + principalTable: "company_ssi_details", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_company_ssi_process_data_verified_credential_type_kinds_cre", + column: x => x.credential_type_kind_id, + principalSchema: "issuer", + principalTable: "verified_credential_type_kinds", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "company_ssi_detail_statuses", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "PENDING" }, + { 2, "ACTIVE" }, + { 3, "INACTIVE" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "document_status", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 2, "ACTIVE" }, + { 3, "INACTIVE" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "document_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "PRESENTATION" }, + { 2, "CREDENTIAL" }, + { 3, "VERIFIED_CREDENTIAL" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "expiry_check_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "ONE_MONTH" }, + { 2, "TWO_WEEKS" }, + { 3, "ONE_DAY" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "media_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "JPEG" }, + { 2, "GIF" }, + { 3, "PNG" }, + { 4, "SVG" }, + { 5, "TIFF" }, + { 6, "PDF" }, + { 7, "JSON" }, + { 8, "PEM" }, + { 9, "CA_CERT" }, + { 10, "PKX_CER" }, + { 11, "OCTET" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "process_step_statuses", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "TODO" }, + { 2, "DONE" }, + { 3, "SKIPPED" }, + { 4, "FAILED" }, + { 5, "DUPLICATE" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "process_step_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "CREATE_CREDENTIAL" }, + { 2, "SIGN_CREDENTIAL" }, + { 3, "SAVE_CREDENTIAL_DOCUMENT" }, + { 4, "CREATE_CREDENTIAL_FOR_HOLDER" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "process_types", + columns: new[] { "id", "label" }, + values: new object[] { 1, "CREATE_CREDENTIAL" }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "verified_credential_external_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "TRACEABILITY_CREDENTIAL" }, + { 2, "PCF_CREDENTIAL" }, + { 3, "BEHAVIOR_TWIN_CREDENTIAL" }, + { 4, "VEHICLE_DISMANTLE" }, + { 5, "SUSTAINABILITY_CREDENTIAL" }, + { 6, "Quality_Credential" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "verified_credential_type_kinds", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "FRAMEWORK" }, + { 2, "MEMBERSHIP" }, + { 3, "BPN" } + }); + + migrationBuilder.InsertData( + schema: "issuer", + table: "verified_credential_types", + columns: new[] { "id", "label" }, + values: new object[,] + { + { 1, "TRACEABILITY_FRAMEWORK" }, + { 2, "PCF_FRAMEWORK" }, + { 3, "BEHAVIOR_TWIN_FRAMEWORK" }, + { 4, "DISMANTLER_CERTIFICATE" }, + { 5, "SUSTAINABILITY_FRAMEWORK" }, + { 6, "FRAMEWORK_AGREEMENT_QUALITY" }, + { 7, "BUSINESS_PARTNER_NUMBER" } + }); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_detail_assigned_documents_company_ssi_detail_id", + schema: "issuer", + table: "company_ssi_detail_assigned_documents", + column: "company_ssi_detail_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_details_company_ssi_detail_status_id", + schema: "issuer", + table: "company_ssi_details", + column: "company_ssi_detail_status_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_details_expiry_check_type_id", + schema: "issuer", + table: "company_ssi_details", + column: "expiry_check_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_details_process_id", + schema: "issuer", + table: "company_ssi_details", + column: "process_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_details_verified_credential_external_type_detai", + schema: "issuer", + table: "company_ssi_details", + column: "verified_credential_external_type_detail_version_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_details_verified_credential_type_id", + schema: "issuer", + table: "company_ssi_details", + column: "verified_credential_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_company_ssi_process_data_credential_type_kind_id", + schema: "issuer", + table: "company_ssi_process_data", + column: "credential_type_kind_id"); + + migrationBuilder.CreateIndex( + name: "ix_documents_document_status_id", + schema: "issuer", + table: "documents", + column: "document_status_id"); + + migrationBuilder.CreateIndex( + name: "ix_documents_document_type_id", + schema: "issuer", + table: "documents", + column: "document_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_documents_media_type_id", + schema: "issuer", + table: "documents", + column: "media_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_id", + schema: "issuer", + table: "process_steps", + column: "process_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_status_id", + schema: "issuer", + table: "process_steps", + column: "process_step_status_id"); + + migrationBuilder.CreateIndex( + name: "ix_process_steps_process_step_type_id", + schema: "issuer", + table: "process_steps", + column: "process_step_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_processes_process_type_id", + schema: "issuer", + table: "processes", + column: "process_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_external_type_detail_versions_verified_", + schema: "issuer", + table: "verified_credential_external_type_detail_versions", + columns: new[] { "verified_credential_external_type_id", "version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_external_types_verified_c", + schema: "issuer", + table: "verified_credential_type_assigned_external_types", + column: "verified_credential_external_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_external_types_verified_c1", + schema: "issuer", + table: "verified_credential_type_assigned_external_types", + column: "verified_credential_type_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_kinds_verified_credential", + schema: "issuer", + table: "verified_credential_type_assigned_kinds", + column: "verified_credential_type_id"); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_kinds_verified_credential1", + schema: "issuer", + table: "verified_credential_type_assigned_kinds", + column: "verified_credential_type_kind_id"); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_use_cases_use_case_id", + schema: "issuer", + table: "verified_credential_type_assigned_use_cases", + column: "use_case_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_verified_credential_type_assigned_use_cases_verified_creden", + schema: "issuer", + table: "verified_credential_type_assigned_use_cases", + column: "verified_credential_type_id", + unique: true); + + migrationBuilder.Sql("CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL AFTER INSERT\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"();"); + + migrationBuilder.Sql("CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL AFTER UPDATE\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"();"); + + migrationBuilder.Sql("CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_DOCUMENT AFTER INSERT\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"();"); + + migrationBuilder.Sql("CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_DOCUMENT AFTER UPDATE\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"();"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"() CASCADE;"); + + migrationBuilder.Sql("DROP FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"() CASCADE;"); + + migrationBuilder.Sql("DROP FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"() CASCADE;"); + + migrationBuilder.Sql("DROP FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"() CASCADE;"); + + migrationBuilder.DropTable( + name: "audit_company_ssi_detail20240228", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "audit_document20240305", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "company_ssi_detail_assigned_documents", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "company_ssi_process_data", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "process_steps", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_type_assigned_external_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_type_assigned_kinds", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_type_assigned_use_cases", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "documents", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "company_ssi_details", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "process_step_statuses", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "process_step_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_type_kinds", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "use_cases", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "document_status", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "document_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "media_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "company_ssi_detail_statuses", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "expiry_check_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "processes", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_external_type_detail_versions", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "process_types", + schema: "issuer"); + + migrationBuilder.DropTable( + name: "verified_credential_external_types", + schema: "issuer"); + } + } +} diff --git a/src/database/SsiCredentialIssuer.Migrations/Migrations/IssuerDbContextModelSnapshot.cs b/src/database/SsiCredentialIssuer.Migrations/Migrations/IssuerDbContextModelSnapshot.cs new file mode 100644 index 00000000..83769ae6 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Migrations/IssuerDbContextModelSnapshot.cs @@ -0,0 +1,1395 @@ +/******************************************************************************** +// * 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 Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Migrations +{ + [DbContext(typeof(IssuerDbContext))] + partial class IssuerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("issuer") + .UseCollation("en_US.utf8") + .HasAnnotation("ProductVersion", "7.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities.AuditCompanySsiDetail20240228", b => + { + b.Property("AuditV1Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("audit_v1id"); + + b.Property("AuditV1DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("audit_v1date_last_changed"); + + b.Property("AuditV1LastEditorId") + .HasColumnType("uuid") + .HasColumnName("audit_v1last_editor_id"); + + b.Property("AuditV1OperationId") + .HasColumnType("integer") + .HasColumnName("audit_v1operation_id"); + + b.Property("Bpnl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpnl"); + + b.Property("CompanySsiDetailStatusId") + .HasColumnType("integer") + .HasColumnName("company_ssi_detail_status_id"); + + b.Property("CreatorUserId") + .HasColumnType("uuid") + .HasColumnName("creator_user_id"); + + b.Property("Credential") + .HasColumnType("text") + .HasColumnName("credential"); + + 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("ExpiryCheckTypeId") + .HasColumnType("integer") + .HasColumnName("expiry_check_type_id"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ExternalCredentialId") + .HasColumnType("uuid") + .HasColumnName("external_credential_id"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("VerifiedCredentialExternalTypeDetailVersionId") + .HasColumnType("uuid") + .HasColumnName("verified_credential_external_type_detail_version_id"); + + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.HasKey("AuditV1Id") + .HasName("pk_audit_company_ssi_detail20240228"); + + b.ToTable("audit_company_ssi_detail20240228", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities.AuditDocument20240305", b => + { + b.Property("AuditV1Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("audit_v1id"); + + b.Property("AuditV1DateLastChanged") + .HasColumnType("timestamp with time zone") + .HasColumnName("audit_v1date_last_changed"); + + b.Property("AuditV1LastEditorId") + .HasColumnType("uuid") + .HasColumnName("audit_v1last_editor_id"); + + b.Property("AuditV1OperationId") + .HasColumnType("integer") + .HasColumnName("audit_v1operation_id"); + + b.Property("CompanyUserId") + .HasColumnType("uuid") + .HasColumnName("company_user_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("DocumentContent") + .HasColumnType("bytea") + .HasColumnName("document_content"); + + b.Property("DocumentHash") + .HasColumnType("bytea") + .HasColumnName("document_hash"); + + b.Property("DocumentName") + .HasColumnType("text") + .HasColumnName("document_name"); + + b.Property("DocumentStatusId") + .HasColumnType("integer") + .HasColumnName("document_status_id"); + + b.Property("DocumentTypeId") + .HasColumnType("integer") + .HasColumnName("document_type_id"); + + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.HasKey("AuditV1Id") + .HasName("pk_audit_document20240305"); + + b.ToTable("audit_document20240305", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Bpnl") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bpnl"); + + b.Property("CompanySsiDetailStatusId") + .HasColumnType("integer") + .HasColumnName("company_ssi_detail_status_id"); + + b.Property("CreatorUserId") + .HasColumnType("uuid") + .HasColumnName("creator_user_id"); + + b.Property("Credential") + .HasColumnType("text") + .HasColumnName("credential"); + + 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("ExpiryCheckTypeId") + .HasColumnType("integer") + .HasColumnName("expiry_check_type_id"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("ExternalCredentialId") + .HasColumnType("uuid") + .HasColumnName("external_credential_id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("ProcessId") + .HasColumnType("uuid") + .HasColumnName("process_id"); + + b.Property("VerifiedCredentialExternalTypeDetailVersionId") + .HasColumnType("uuid") + .HasColumnName("verified_credential_external_type_detail_version_id"); + + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.HasKey("Id") + .HasName("pk_company_ssi_details"); + + b.HasIndex("CompanySsiDetailStatusId") + .HasDatabaseName("ix_company_ssi_details_company_ssi_detail_status_id"); + + b.HasIndex("ExpiryCheckTypeId") + .HasDatabaseName("ix_company_ssi_details_expiry_check_type_id"); + + b.HasIndex("ProcessId") + .HasDatabaseName("ix_company_ssi_details_process_id"); + + b.HasIndex("VerifiedCredentialExternalTypeDetailVersionId") + .HasDatabaseName("ix_company_ssi_details_verified_credential_external_type_detai"); + + b.HasIndex("VerifiedCredentialTypeId") + .HasDatabaseName("ix_company_ssi_details_verified_credential_type_id"); + + b.ToTable("company_ssi_details", "issuer", t => + { + t.HasTrigger("LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL"); + + t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL"); + }); + + b + .HasAnnotation("LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL AFTER INSERT\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_COMPANYSSIDETAIL\"();") + .HasAnnotation("LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_company_ssi_detail20240228\" (\"id\", \"bpnl\", \"verified_credential_type_id\", \"company_ssi_detail_status_id\", \"date_created\", \"creator_user_id\", \"expiry_date\", \"verified_credential_external_type_detail_version_id\", \"expiry_check_type_id\", \"process_id\", \"external_credential_id\", \"credential\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"bpnl\", \r\n NEW.\"verified_credential_type_id\", \r\n NEW.\"company_ssi_detail_status_id\", \r\n NEW.\"date_created\", \r\n NEW.\"creator_user_id\", \r\n NEW.\"expiry_date\", \r\n NEW.\"verified_credential_external_type_detail_version_id\", \r\n NEW.\"expiry_check_type_id\", \r\n NEW.\"process_id\", \r\n NEW.\"external_credential_id\", \r\n NEW.\"credential\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL AFTER UPDATE\r\nON \"issuer\".\"company_ssi_details\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_COMPANYSSIDETAIL\"();"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailAssignedDocument", b => + { + b.Property("DocumentId") + .HasColumnType("uuid") + .HasColumnName("document_id"); + + b.Property("CompanySsiDetailId") + .HasColumnType("uuid") + .HasColumnName("company_ssi_detail_id"); + + b.HasKey("DocumentId", "CompanySsiDetailId") + .HasName("pk_company_ssi_detail_assigned_documents"); + + b.HasIndex("CompanySsiDetailId") + .HasDatabaseName("ix_company_ssi_detail_assigned_documents_company_ssi_detail_id"); + + b.ToTable("company_ssi_detail_assigned_documents", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", 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_company_ssi_detail_statuses"); + + b.ToTable("company_ssi_detail_statuses", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "PENDING" + }, + new + { + Id = 2, + Label = "ACTIVE" + }, + new + { + Id = 3, + Label = "INACTIVE" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", b => + { + b.Property("CompanySsiDetailId") + .HasColumnType("uuid") + .HasColumnName("company_ssi_detail_id"); + + b.Property("ClientId") + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .HasColumnType("bytea") + .HasColumnName("client_secret"); + + b.Property("CredentialTypeKindId") + .HasColumnType("integer") + .HasColumnName("credential_type_kind_id"); + + b.Property("EncryptionMode") + .HasColumnType("integer") + .HasColumnName("encryption_mode"); + + b.Property("HolderWalletUrl") + .HasColumnType("text") + .HasColumnName("holder_wallet_url"); + + b.Property("InitializationVector") + .HasColumnType("bytea") + .HasColumnName("initialization_vector"); + + b.Property("Schema") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("schema"); + + b.HasKey("CompanySsiDetailId") + .HasName("pk_company_ssi_process_data"); + + b.HasIndex("CredentialTypeKindId") + .HasDatabaseName("ix_company_ssi_process_data_credential_type_kind_id"); + + b.ToTable("company_ssi_process_data", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("CompanyUserId") + .HasColumnType("uuid") + .HasColumnName("company_user_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("DocumentContent") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("document_content"); + + b.Property("DocumentHash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("document_hash"); + + b.Property("DocumentName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("document_name"); + + b.Property("DocumentStatusId") + .HasColumnType("integer") + .HasColumnName("document_status_id"); + + b.Property("DocumentTypeId") + .HasColumnType("integer") + .HasColumnName("document_type_id"); + + b.Property("LastEditorId") + .HasColumnType("uuid") + .HasColumnName("last_editor_id"); + + b.Property("MediaTypeId") + .HasColumnType("integer") + .HasColumnName("media_type_id"); + + b.HasKey("Id") + .HasName("pk_documents"); + + b.HasIndex("DocumentStatusId") + .HasDatabaseName("ix_documents_document_status_id"); + + b.HasIndex("DocumentTypeId") + .HasDatabaseName("ix_documents_document_type_id"); + + b.HasIndex("MediaTypeId") + .HasDatabaseName("ix_documents_media_type_id"); + + b.ToTable("documents", "issuer", t => + { + t.HasTrigger("LC_TRIGGER_AFTER_INSERT_DOCUMENT"); + + t.HasTrigger("LC_TRIGGER_AFTER_UPDATE_DOCUMENT"); + }); + + b + .HasAnnotation("LC_TRIGGER_AFTER_INSERT_DOCUMENT", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_INSERT_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 1, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_INSERT_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_INSERT_DOCUMENT AFTER INSERT\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_INSERT_DOCUMENT\"();") + .HasAnnotation("LC_TRIGGER_AFTER_UPDATE_DOCUMENT", "CREATE FUNCTION \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"() RETURNS trigger as $LC_TRIGGER_AFTER_UPDATE_DOCUMENT$\r\nBEGIN\r\n INSERT INTO \"issuer\".\"audit_document20240305\" (\"id\", \"date_created\", \"document_hash\", \"document_content\", \"document_name\", \"media_type_id\", \"document_type_id\", \"document_status_id\", \"company_user_id\", \"date_last_changed\", \"last_editor_id\", \"audit_v1id\", \"audit_v1operation_id\", \"audit_v1date_last_changed\") SELECT NEW.\"id\", \r\n NEW.\"date_created\", \r\n NEW.\"document_hash\", \r\n NEW.\"document_content\", \r\n NEW.\"document_name\", \r\n NEW.\"media_type_id\", \r\n NEW.\"document_type_id\", \r\n NEW.\"document_status_id\", \r\n NEW.\"company_user_id\", \r\n NEW.\"date_last_changed\", \r\n NEW.\"last_editor_id\", \r\n gen_random_uuid(), \r\n 2, \r\n CURRENT_DATE;\r\nRETURN NEW;\r\nEND;\r\n$LC_TRIGGER_AFTER_UPDATE_DOCUMENT$ LANGUAGE plpgsql;\r\nCREATE TRIGGER LC_TRIGGER_AFTER_UPDATE_DOCUMENT AFTER UPDATE\r\nON \"issuer\".\"documents\"\r\nFOR EACH ROW EXECUTE PROCEDURE \"issuer\".\"LC_TRIGGER_AFTER_UPDATE_DOCUMENT\"();"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", 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_document_status"); + + b.ToTable("document_status", "issuer"); + + b.HasData( + new + { + Id = 2, + Label = "ACTIVE" + }, + new + { + Id = 3, + Label = "INACTIVE" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", 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_document_types"); + + b.ToTable("document_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "PRESENTATION" + }, + new + { + Id = 2, + Label = "CREDENTIAL" + }, + new + { + Id = 3, + Label = "VERIFIED_CREDENTIAL" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", 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_expiry_check_types"); + + b.ToTable("expiry_check_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "ONE_MONTH" + }, + new + { + Id = 2, + Label = "TWO_WEEKS" + }, + new + { + Id = 3, + Label = "ONE_DAY" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", 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_media_types"); + + b.ToTable("media_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "JPEG" + }, + new + { + Id = 2, + Label = "GIF" + }, + new + { + Id = 3, + Label = "PNG" + }, + new + { + Id = 4, + Label = "SVG" + }, + new + { + Id = 5, + Label = "TIFF" + }, + new + { + Id = 6, + Label = "PDF" + }, + new + { + Id = 7, + Label = "JSON" + }, + new + { + Id = 8, + Label = "PEM" + }, + new + { + Id = 9, + Label = "CA_CERT" + }, + new + { + Id = 10, + Label = "PKX_CER" + }, + new + { + Id = 11, + Label = "OCTET" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + 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("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_CREDENTIAL" + }, + new + { + Id = 2, + Label = "SIGN_CREDENTIAL" + }, + new + { + Id = 3, + Label = "SAVE_CREDENTIAL_DOCUMENT" + }, + new + { + Id = 4, + Label = "CREATE_CREDENTIAL_FOR_HOLDER" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.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", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "CREATE_CREDENTIAL" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("name"); + + b.Property("Shortname") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)") + .HasColumnName("shortname"); + + b.HasKey("Id") + .HasName("pk_use_cases"); + + b.ToTable("use_cases", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", b => + { + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("id"); + + b.Property("Label") + .IsRequired() + .HasColumnType("text") + .HasColumnName("label"); + + b.HasKey("Id") + .HasName("pk_verified_credential_external_types"); + + b.ToTable("verified_credential_external_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "TRACEABILITY_CREDENTIAL" + }, + new + { + Id = 2, + Label = "PCF_CREDENTIAL" + }, + new + { + Id = 3, + Label = "BEHAVIOR_TWIN_CREDENTIAL" + }, + new + { + Id = 4, + Label = "VEHICLE_DISMANTLE" + }, + new + { + Id = 5, + Label = "SUSTAINABILITY_CREDENTIAL" + }, + new + { + Id = 6, + Label = "Quality_Credential" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Expiry") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry"); + + b.Property("Template") + .HasColumnType("text") + .HasColumnName("template"); + + b.Property("ValidFrom") + .HasColumnType("timestamp with time zone") + .HasColumnName("valid_from"); + + b.Property("VerifiedCredentialExternalTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_external_type_id"); + + b.Property("Version") + .HasColumnType("text") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_verified_credential_external_type_detail_versions"); + + b.HasIndex("VerifiedCredentialExternalTypeId", "Version") + .IsUnique() + .HasDatabaseName("ix_verified_credential_external_type_detail_versions_verified_"); + + b.ToTable("verified_credential_external_type_detail_versions", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", 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_verified_credential_types"); + + b.ToTable("verified_credential_types", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "TRACEABILITY_FRAMEWORK" + }, + new + { + Id = 2, + Label = "PCF_FRAMEWORK" + }, + new + { + Id = 3, + Label = "BEHAVIOR_TWIN_FRAMEWORK" + }, + new + { + Id = 4, + Label = "DISMANTLER_CERTIFICATE" + }, + new + { + Id = 5, + Label = "SUSTAINABILITY_FRAMEWORK" + }, + new + { + Id = 6, + Label = "FRAMEWORK_AGREEMENT_QUALITY" + }, + new + { + Id = 7, + Label = "BUSINESS_PARTNER_NUMBER" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("VerifiedCredentialExternalTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_external_type_id"); + + b.HasKey("VerifiedCredentialTypeId", "VerifiedCredentialExternalTypeId") + .HasName("pk_verified_credential_type_assigned_external_types"); + + b.HasIndex("VerifiedCredentialExternalTypeId") + .HasDatabaseName("ix_verified_credential_type_assigned_external_types_verified_c"); + + b.HasIndex("VerifiedCredentialTypeId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_external_types_verified_c1"); + + b.ToTable("verified_credential_type_assigned_external_types", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("VerifiedCredentialTypeKindId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_kind_id"); + + b.HasKey("VerifiedCredentialTypeId", "VerifiedCredentialTypeKindId") + .HasName("pk_verified_credential_type_assigned_kinds"); + + b.HasIndex("VerifiedCredentialTypeId") + .HasDatabaseName("ix_verified_credential_type_assigned_kinds_verified_credential"); + + b.HasIndex("VerifiedCredentialTypeKindId") + .HasDatabaseName("ix_verified_credential_type_assigned_kinds_verified_credential1"); + + b.ToTable("verified_credential_type_assigned_kinds", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", b => + { + b.Property("VerifiedCredentialTypeId") + .HasColumnType("integer") + .HasColumnName("verified_credential_type_id"); + + b.Property("UseCaseId") + .HasColumnType("uuid") + .HasColumnName("use_case_id"); + + b.HasKey("VerifiedCredentialTypeId", "UseCaseId") + .HasName("pk_verified_credential_type_assigned_use_cases"); + + b.HasIndex("UseCaseId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_use_cases_use_case_id"); + + b.HasIndex("VerifiedCredentialTypeId") + .IsUnique() + .HasDatabaseName("ix_verified_credential_type_assigned_use_cases_verified_creden"); + + b.ToTable("verified_credential_type_assigned_use_cases", "issuer"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", 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_verified_credential_type_kinds"); + + b.ToTable("verified_credential_type_kinds", "issuer"); + + b.HasData( + new + { + Id = 1, + Label = "FRAMEWORK" + }, + new + { + Id = 2, + Label = "MEMBERSHIP" + }, + new + { + Id = 3, + Label = "BPN" + }); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", "CompanySsiDetailStatus") + .WithMany("CompanySsiDetails") + .HasForeignKey("CompanySsiDetailStatusId") + .IsRequired() + .HasConstraintName("fk_company_ssi_details_company_ssi_detail_statuses_company_ssi"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", "ExpiryCheckType") + .WithMany("CompanySsiDetails") + .HasForeignKey("ExpiryCheckTypeId") + .HasConstraintName("fk_company_ssi_details_expiry_check_types_expiry_check_type_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", "Process") + .WithMany("CompanySsiDetails") + .HasForeignKey("ProcessId") + .HasConstraintName("fk_company_ssi_details_processes_process_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", "VerifiedCredentialExternalTypeDetailVersion") + .WithMany("CompanySsiDetails") + .HasForeignKey("VerifiedCredentialExternalTypeDetailVersionId") + .HasConstraintName("fk_company_ssi_details_verified_credential_external_type_detai"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithMany("CompanySsiDetails") + .HasForeignKey("VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_company_ssi_details_verified_credential_types_verified_cred"); + + b.Navigation("CompanySsiDetailStatus"); + + b.Navigation("ExpiryCheckType"); + + b.Navigation("Process"); + + b.Navigation("VerifiedCredentialExternalTypeDetailVersion"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailAssignedDocument", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", "CompanySsiDetail") + .WithMany() + .HasForeignKey("CompanySsiDetailId") + .IsRequired() + .HasConstraintName("fk_company_ssi_detail_assigned_documents_company_ssi_details_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", "Document") + .WithMany() + .HasForeignKey("DocumentId") + .IsRequired() + .HasConstraintName("fk_company_ssi_detail_assigned_documents_documents_document_id"); + + b.Navigation("CompanySsiDetail"); + + b.Navigation("Document"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", "CompanySsiDetail") + .WithOne("CompanySsiProcessData") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiProcessData", "CompanySsiDetailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_company_ssi_process_data_company_ssi_details_company_ssi_de"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", "CredentialTypeKind") + .WithMany("CompanySsiProcessData") + .HasForeignKey("CredentialTypeKindId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_company_ssi_process_data_verified_credential_type_kinds_cre"); + + b.Navigation("CompanySsiDetail"); + + b.Navigation("CredentialTypeKind"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Document", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", "DocumentStatus") + .WithMany("Documents") + .HasForeignKey("DocumentStatusId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_document_status_document_status_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", "DocumentType") + .WithMany("Documents") + .HasForeignKey("DocumentTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_document_types_document_type_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", "MediaType") + .WithMany("Documents") + .HasForeignKey("MediaTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_documents_media_types_media_type_id"); + + b.Navigation("DocumentStatus"); + + b.Navigation("DocumentType"); + + b.Navigation("MediaType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessType", "ProcessType") + .WithMany("Processes") + .HasForeignKey("ProcessTypeId") + .IsRequired() + .HasConstraintName("fk_processes_process_types_process_type_id"); + + b.Navigation("ProcessType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStep", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", "Process") + .WithMany("ProcessSteps") + .HasForeignKey("ProcessId") + .IsRequired() + .HasConstraintName("fk_process_steps_processes_process_id"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.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("Org.Eclipse.TractusX.SsiCredentialIssuer.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("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", "VerifiedCredentialExternalType") + .WithMany("VerifiedCredentialExternalTypeDetailVersions") + .HasForeignKey("VerifiedCredentialExternalTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_external_type_detail_versions_verified_"); + + b.Navigation("VerifiedCredentialExternalType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", "VerifiedCredentialExternalType") + .WithMany("VerifiedCredentialTypeAssignedExternalTypes") + .HasForeignKey("VerifiedCredentialExternalTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_external_types_verified_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedExternalType") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedExternalType", "VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_external_types_verified_c1"); + + b.Navigation("VerifiedCredentialExternalType"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedKind") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedKind", "VerifiedCredentialTypeId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_kinds_verified_credential"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", "VerifiedCredentialTypeKind") + .WithMany("VerifiedCredentialTypeAssignedKinds") + .HasForeignKey("VerifiedCredentialTypeKindId") + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_kinds_verified_credential1"); + + b.Navigation("VerifiedCredentialType"); + + b.Navigation("VerifiedCredentialTypeKind"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", b => + { + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", "UseCase") + .WithOne("VerifiedCredentialAssignedUseCase") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", "UseCaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_use_cases_use_cases_use_c"); + + b.HasOne("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", "VerifiedCredentialType") + .WithOne("VerifiedCredentialTypeAssignedUseCase") + .HasForeignKey("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeAssignedUseCase", "VerifiedCredentialTypeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_verified_credential_type_assigned_use_cases_verified_creden"); + + b.Navigation("UseCase"); + + b.Navigation("VerifiedCredentialType"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetail", b => + { + b.Navigation("CompanySsiProcessData"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.CompanySsiDetailStatus", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentStatus", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.DocumentType", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ExpiryCheckType", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.MediaType", b => + { + b.Navigation("Documents"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.Process", b => + { + b.Navigation("CompanySsiDetails"); + + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStepStatus", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessStepType", b => + { + b.Navigation("ProcessSteps"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.ProcessType", b => + { + b.Navigation("Processes"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.UseCase", b => + { + b.Navigation("VerifiedCredentialAssignedUseCase"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalType", b => + { + b.Navigation("VerifiedCredentialExternalTypeDetailVersions"); + + b.Navigation("VerifiedCredentialTypeAssignedExternalTypes"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialExternalTypeDetailVersion", b => + { + b.Navigation("CompanySsiDetails"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialType", b => + { + b.Navigation("CompanySsiDetails"); + + b.Navigation("VerifiedCredentialTypeAssignedExternalType"); + + b.Navigation("VerifiedCredentialTypeAssignedKind"); + + b.Navigation("VerifiedCredentialTypeAssignedUseCase"); + }); + + modelBuilder.Entity("Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities.VerifiedCredentialTypeKind", b => + { + b.Navigation("CompanySsiProcessData"); + + b.Navigation("VerifiedCredentialTypeAssignedKinds"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/database/SsiCredentialIssuer.Migrations/Program.cs b/src/database/SsiCredentialIssuer.Migrations/Program.cs new file mode 100644 index 00000000..aea213c9 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Program.cs @@ -0,0 +1,60 @@ +/******************************************************************************** + * 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 Laraue.EfCoreTriggers.PostgreSql.Extensions; +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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Serilog; +using System.Reflection; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Starting process"); +try +{ + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddDbContext(o => + o.UseNpgsql(hostContext.Configuration.GetConnectionString("IssuerDb"), + x => x.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_issuer")) + .UsePostgreSqlTriggers()) + .AddDatabaseInitializer(hostContext.Configuration.GetSection("Seeding")); + }) + .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/SsiCredentialIssuer.Migrations/Properties/launchSettings.json b/src/database/SsiCredentialIssuer.Migrations/Properties/launchSettings.json new file mode 100644 index 00000000..cb41575e --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "SsiCredentialIssuer.Migrations": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs new file mode 100644 index 00000000..0c59abf8 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchInsertSeeder.cs @@ -0,0 +1,108 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Seeder; + +/// +/// Seeder to seed the base entities (those with an id as primary key) +/// +public class BatchInsertSeeder : ICustomSeeder +{ + private readonly IssuerDbContext _context; + private readonly ILogger _logger; + private readonly SeederSettings _settings; + + /// + /// Constructor + /// + /// The database context + /// The logger + /// The options + public BatchInsertSeeder(IssuerDbContext context, ILogger logger, IOptions options) + { + _context = context; + _logger = logger; + _settings = options.Value; + } + + /// + public int Order => 1; + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + if (!_settings.DataPaths.Any()) + { + _logger.LogInformation("There a no data paths configured, therefore the {SeederName} will be skipped", nameof(BatchUpdateSeeder)); + return; + } + + _logger.LogInformation("Start BaseEntityBatch Seeder"); + await SeedBaseEntity(cancellationToken); + await SeedTable("company_ssi_process_datas", x => x.CompanySsiDetailId, cancellationToken).ConfigureAwait(false); + await SeedTable("verified_credential_type_assigned_kinds", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialTypeKindId }, cancellationToken).ConfigureAwait(false); + await SeedTable("verified_credential_type_assigned_use_cases", x => new { x.VerifiedCredentialTypeId, x.UseCaseId }, cancellationToken).ConfigureAwait(false); + await SeedTable("verified_credential_type_assigned_external_types", x => new { x.VerifiedCredentialTypeId, x.VerifiedCredentialExternalTypeId }, cancellationToken).ConfigureAwait(false); + + await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Finished BaseEntityBatch Seeder"); + } + + private async Task SeedBaseEntity(CancellationToken cancellationToken) + { + await SeedTableForBaseEntity("documents", cancellationToken).ConfigureAwait(false); + await SeedTableForBaseEntity("use_cases", cancellationToken).ConfigureAwait(false); + await SeedTableForBaseEntity("process_steps", cancellationToken).ConfigureAwait(false); + await SeedTableForBaseEntity("processes", cancellationToken).ConfigureAwait(false); + await SeedTableForBaseEntity("verified_credential_external_type_detail_versions", cancellationToken).ConfigureAwait(false); + await SeedTableForBaseEntity("company_ssi_details", cancellationToken).ConfigureAwait(false); + } + + private async Task SeedTableForBaseEntity(string fileName, CancellationToken cancellationToken) where T : class, IBaseEntity + { + await SeedTable(fileName, x => x.Id, cancellationToken).ConfigureAwait(false); + } + + private async Task SeedTable(string fileName, Func keySelector, CancellationToken cancellationToken) where T : class + { + _logger.LogInformation("Start seeding {Filename}", fileName); + var additionalEnvironments = _settings.TestDataEnvironments ?? Enumerable.Empty(); + var data = await SeederHelper.GetSeedData(_logger, fileName, _settings.DataPaths, cancellationToken, additionalEnvironments.ToArray()).ConfigureAwait(false); + _logger.LogInformation("Found {ElementCount} data", data.Count); + if (data.Any()) + { + var typeName = typeof(T).Name; + _logger.LogInformation("Started to Seed {TableName}", typeName); + data = data.GroupJoin(_context.Set(), keySelector, keySelector, (d, dbEntry) => new { d, dbEntry }) + .SelectMany(t => t.dbEntry.DefaultIfEmpty(), (t, x) => new { t, x }) + .Where(t => t.x == null) + .Select(t => t.t.d).ToList(); + _logger.LogInformation("Seeding {DataCount} {TableName}", data.Count, typeName); + await _context.Set().AddRangeAsync(data, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Seeded {TableName}", typeName); + } + } +} diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchUpdateSeeder.cs b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchUpdateSeeder.cs new file mode 100644 index 00000000..3fcc80a4 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/BatchUpdateSeeder.cs @@ -0,0 +1,103 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Seeder; + +/// +/// Seeder to seed the base entities (those with an id as primary key) +/// +public class BatchUpdateSeeder : ICustomSeeder +{ + private readonly IssuerDbContext _context; + private readonly ILogger _logger; + private readonly SeederSettings _settings; + + /// + /// Constructor + /// + /// The database context + /// The logger + /// The options + public BatchUpdateSeeder(IssuerDbContext context, ILogger logger, IOptions options) + { + _context = context; + _logger = logger; + _settings = options.Value; + } + + /// + public int Order => 2; + + /// + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + if (!_settings.DataPaths.Any()) + { + _logger.LogInformation("There a no data paths configured, therefore the {SeederName} will be skipped", nameof(BatchUpdateSeeder)); + return; + } + + _logger.LogInformation("Start BaseEntityBatch Seeder"); + + await SeedTable("verified_credential_external_type_detail_versions", + x => x.Id, + x => x.dataEntity.Template != x.dbEntity.Template || x.dataEntity.Expiry != x.dbEntity.Expiry || x.dataEntity.ValidFrom != x.dbEntity.ValidFrom, + (dbEntry, entry) => + { + dbEntry.Template = entry.Template; + dbEntry.Expiry = entry.Expiry; + dbEntry.ValidFrom = entry.ValidFrom; + }, cancellationToken).ConfigureAwait(false); + + await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Finished BaseEntityBatch Seeder"); + } + + private async Task SeedTable(string fileName, Func keySelector, Func<(T dataEntity, T dbEntity), bool> whereClause, Action updateEntries, CancellationToken cancellationToken) where T : class + { + _logger.LogInformation("Start seeding {Filename}", fileName); + var additionalEnvironments = _settings.TestDataEnvironments ?? Enumerable.Empty(); + var data = await SeederHelper.GetSeedData(_logger, fileName, _settings.DataPaths, cancellationToken, additionalEnvironments.ToArray()).ConfigureAwait(false); + _logger.LogInformation("Found {ElementCount} data", data.Count); + if (data.Any()) + { + var typeName = typeof(T).Name; + var entriesForUpdate = data + .Join(_context.Set(), keySelector, keySelector, (dataEntry, dbEntry) => (DataEntry: dataEntry, DbEntry: dbEntry)) + .Where(whereClause.Invoke) + .ToList(); + if (entriesForUpdate.Any()) + { + _logger.LogInformation("Started to Update {EntryCount} entries of {TableName}", entriesForUpdate.Count, typeName); + foreach (var entry in entriesForUpdate) + { + updateEntries.Invoke(entry.DbEntry, entry.DataEntry); + } + + _logger.LogInformation("Updated {TableName}", typeName); + } + } + } +} diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/use_cases.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/use_cases.json new file mode 100644 index 00000000..6c33df1d --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/use_cases.json @@ -0,0 +1,57 @@ +[ + { + "id": "06b243a4-ba51-4bf3-bc40-5d79a2231b86", + "name": "Traceability", + "shortname": "T" + }, + { + "id": "06b243a4-ba51-4bf3-bc40-5d79a2231b87", + "name": "Sustainability & CO2-Footprint", + "shortname": "CO2" + }, + { + "id": "06b243a4-ba51-4bf3-bc40-5d79a2231b88", + "name": "Manufacturing as a Service", + "shortname": "MaaS" + }, + { + "id": "06b243a4-ba51-4bf3-bc40-5d79a2231b89", + "name": "Real-Time Control", + "shortname": "RTC" + }, + { + "id": "06b243a4-ba51-4bf3-bc40-5d79a2231b90", + "name": "Modular Production", + "shortname": "MP" + }, + { + "id": "1aacde78-35ec-4df3-ba1e-f988cddcbbd8", + "name": "Circular Economy", + "shortname": "CE" + }, + { + "id": "1aacde78-35ec-4df3-ba1e-f988cddcbbd9", + "name": "None", + "shortname": "None" + }, + { + "id": "41e4a4c0-aae4-41c0-97c9-ebafde410de4", + "name": "Demand and Capacity Management", + "shortname": "DCM" + }, + { + "id": "6909ccc7-37c8-4088-99ab-790f20702460", + "name": "Business Partner Management", + "shortname": "BPDM" + }, + { + "id": "c065a349-f649-47f8-94d5-1a504a855419", + "name": "Quality Management", + "shortname": "QM" + }, + { + "id": "b3948771-3372-4568-9e0e-acca4e674098", + "name": "Behavior Twin", + "shortname": "BT" + } +] \ No newline at end of file diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.consortia.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.consortia.json new file mode 100644 index 00000000..58bb092c --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.consortia.json @@ -0,0 +1,42 @@ +[ + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d562", + "verified_credential_external_type_id": 3, + "version": "1.0.0", + "template": null, + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-09-30 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d563", + "verified_credential_external_type_id": 1, + "version": "2.0.0", + "template": null, + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-12-23 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d564", + "verified_credential_external_type_id": 1, + "version": "3.0.0", + "template": null, + "valid_from": "2024-01-01 00:00:00.000000 +00:00", + "expiry": "2024-12-31 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d565", + "verified_credential_external_type_id": 5, + "version": "1.0.0", + "template": null, + "valid_from": "2024-01-01 00:00:00.000000 +00:00", + "expiry": "2024-12-31 00:00:00.000000 +00:00" + }, + { + "id": "df3bd7d2-3349-410b-9b30-9a5238eb605e", + "verified_credential_external_type_id": 4, + "version": null, + "template": null, + "valid_from": "2024-01-01 00:00:00.000000 +00:00", + "expiry": "2999-12-31 23:59:59.000000 +00:00" + } +] diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.json new file mode 100644 index 00000000..ee5ccd40 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_external_type_detail_versions.json @@ -0,0 +1,26 @@ +[ + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d560", + "verified_credential_external_type_id": 1, + "version": "1.0.0", + "template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_Traceability.pdf", + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-09-30 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d561", + "verified_credential_external_type_id": 2, + "version": "1.0.0", + "template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_PCF.pdf", + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-09-30 00:00:00.000000 +00:00" + }, + { + "id": "37aa6259-b452-4d50-b09e-827929dcfa15", + "verified_credential_external_type_id": 6, + "version": "1.0.0", + "template": "https://catena-x.net/fileadmin/user_upload/04_Einfuehren_und_umsetzen/Governance_Framework/231016_Catena-X_Use_Case_Framework_PCF.pdf", + "valid_from": "2023-10-16 00:00:00.000000 +00:00", + "expiry": "2023-10-16 00:00:00.000000 +00:00" + } +] diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_external_types.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_external_types.json new file mode 100644 index 00000000..c07dc64e --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_external_types.json @@ -0,0 +1,26 @@ +[ + { + "verified_credential_external_type_id": 1, + "verified_credential_type_id": 1 + }, + { + "verified_credential_external_type_id": 2, + "verified_credential_type_id": 2 + }, + { + "verified_credential_external_type_id": 3, + "verified_credential_type_id": 3 + }, + { + "verified_credential_external_type_id": 4, + "verified_credential_type_id": 4 + }, + { + "verified_credential_external_type_id": 5, + "verified_credential_type_id": 5 + }, + { + "verified_credential_external_type_id": 6, + "verified_credential_type_id": 6 + } +] diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_kinds.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_kinds.json new file mode 100644 index 00000000..365789bd --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_kinds.json @@ -0,0 +1,30 @@ +[ + { + "verified_credential_type_id": 1, + "verified_credential_type_kind_id": 1 + }, + { + "verified_credential_type_id": 2, + "verified_credential_type_kind_id": 1 + }, + { + "verified_credential_type_id": 3, + "verified_credential_type_kind_id": 1 + }, + { + "verified_credential_type_id": 4, + "verified_credential_type_kind_id": 2 + }, + { + "verified_credential_type_id": 5, + "verified_credential_type_kind_id": 1 + }, + { + "verified_credential_type_id": 6, + "verified_credential_type_kind_id": 1 + }, + { + "verified_credential_type_id": 7, + "verified_credential_type_kind_id": 3 + } +] diff --git a/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_use_cases.json b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_use_cases.json new file mode 100644 index 00000000..92d3a49f --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/Seeder/Data/verified_credential_type_assigned_use_cases.json @@ -0,0 +1,22 @@ +[ + { + "verified_credential_type_id": 1, + "use_case_id": "06b243a4-ba51-4bf3-bc40-5d79a2231b86" + }, + { + "verified_credential_type_id": 2, + "use_case_id": "06b243a4-ba51-4bf3-bc40-5d79a2231b87" + }, + { + "verified_credential_type_id": 3, + "use_case_id": "b3948771-3372-4568-9e0e-acca4e674098" + }, + { + "verified_credential_type_id": 5, + "use_case_id": "1aacde78-35ec-4df3-ba1e-f988cddcbbd8" + }, + { + "verified_credential_type_id": 6, + "use_case_id": "c065a349-f649-47f8-94d5-1a504a855419" + } +] diff --git a/src/database/SsiCredentialIssuer.Migrations/SsiCredentialIssuer.Migrations.csproj b/src/database/SsiCredentialIssuer.Migrations/SsiCredentialIssuer.Migrations.csproj new file mode 100644 index 00000000..2f7e5b15 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/SsiCredentialIssuer.Migrations.csproj @@ -0,0 +1,81 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations + Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations + net7.0 + enable + enable + 31645f70-6335-4833-b3b4-4826ca778084 + Linux + ..\..\.. + True + Exe + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + Always + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/database/SsiCredentialIssuer.Migrations/appsettings.json b/src/database/SsiCredentialIssuer.Migrations/appsettings.json new file mode 100644 index 00000000..97cfefd1 --- /dev/null +++ b/src/database/SsiCredentialIssuer.Migrations/appsettings.json @@ -0,0 +1,36 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Org.Eclipse.TractusX.SsiCredentialIssuer": "Information", + "Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations": "Warning" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithProcessId", + "WithThreadId" + ], + "Properties": { + "Application": "Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations" + } + }, + "ConnectionStrings": { + "IssuerDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Seeding": { + "DataPaths": [ + "Seeder/Data" + ], + "TestDataEnvironments": [] + } +} diff --git a/src/externalservices/Portal.Service/DependencyInjection/PortalSettings.cs b/src/externalservices/Portal.Service/DependencyInjection/PortalSettings.cs new file mode 100644 index 00000000..feb2a941 --- /dev/null +++ b/src/externalservices/Portal.Service/DependencyInjection/PortalSettings.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 Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; + +[ExcludeFromCodeCoverage] +public class PortalSettings : KeyVaultAuthSettings +{ + [Required(AllowEmptyStrings = false)] + public string BaseAddress { get; set; } = null!; +} diff --git a/src/externalservices/Portal.Service/DependencyInjection/ServiceCollectionExtensions.cs b/src/externalservices/Portal.Service/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8f2c3306 --- /dev/null +++ b/src/externalservices/Portal.Service/DependencyInjection/ServiceCollectionExtensions.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + [ExcludeFromCodeCoverage] + public static IServiceCollection AddPortalService(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions() + .Bind(section) + .ValidateOnStart(); + + services.AddTransient>(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + return services + .AddScoped() + .AddCustomHttpClientWithAuthentication(settings.Value.BaseAddress); + } +} diff --git a/src/externalservices/Portal.Service/Models/MailData.cs b/src/externalservices/Portal.Service/Models/MailData.cs new file mode 100644 index 00000000..e4dab9a4 --- /dev/null +++ b/src/externalservices/Portal.Service/Models/MailData.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.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; + +public record MailData( + [property: JsonPropertyName("bpnl")] string Bpnl, + [property: JsonPropertyName("template")] string Template, + [property: JsonPropertyName("mailParameters")] IDictionary MailParameters +); diff --git a/src/externalservices/Portal.Service/Models/NotificationRequest.cs b/src/externalservices/Portal.Service/Models/NotificationRequest.cs new file mode 100644 index 00000000..606f6b87 --- /dev/null +++ b/src/externalservices/Portal.Service/Models/NotificationRequest.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.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; + +public record NotificationRequest( + [property: JsonPropertyName("bpnl")] string Bpnl, + [property: JsonPropertyName("content")] string Content, + [property: JsonPropertyName("notificationTypeId")] NotificationTypeId NotificationTypeId +); diff --git a/src/externalservices/Portal.Service/Models/NotificationTypeId.cs b/src/externalservices/Portal.Service/Models/NotificationTypeId.cs new file mode 100644 index 00000000..e6d3409d --- /dev/null +++ b/src/externalservices/Portal.Service/Models/NotificationTypeId.cs @@ -0,0 +1,156 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; + +/// +/// Possible types of a notification +/// +public enum NotificationTypeId +{ + /// + /// Notification is just an information for the user + /// + INFO = 1, + + /// + /// Notification requires the user to take some kind of action + /// + ACTION = 2, + + /// + /// Welcome message + /// + WELCOME = 3, + + /// + /// Welcome use case explination + /// + WELCOME_USE_CASES = 4, + + /// + /// Welcome - link to service provider marketplace + /// + WELCOME_SERVICE_PROVIDER = 5, + + /// + /// Welcome - link to register connector + /// + WELCOME_CONNECTOR_REGISTRATION = 6, + + /// + /// Welcome - link to apps + /// + WELCOME_APP_MARKETPLACE = 7, + + /// + /// New App Subscription was requested + /// + APP_SUBSCRIPTION_REQUEST = 8, + + /// + /// New App Subscription was activated + /// + APP_SUBSCRIPTION_ACTIVATION = 9, + + /// + /// Connector was registered + /// + CONNECTOR_REGISTERED = 10, + + /// + /// App Release was requested + /// + APP_RELEASE_REQUEST = 11, + + /// + /// Technical user was created + /// + TECHNICAL_USER_CREATION = 12, + + /// + /// Service request + /// + SERVICE_REQUEST = 13, + + /// + /// Activation of a service + /// + SERVICE_ACTIVATION = 14, + + /// + /// Role Added for Active App + /// + APP_ROLE_ADDED = 15, + + /// + /// Approve App to change status from IN_REVIEW to ACTIVE + /// + APP_RELEASE_APPROVAL = 16, + + /// + /// Service Release was requested + /// + SERVICE_RELEASE_REQUEST = 17, + + /// + /// Approve Service to change status from IN_REVIEW to ACTIVE + /// + SERVICE_RELEASE_APPROVAL = 18, + + /// + /// Notification when a app is rejected + /// + APP_RELEASE_REJECTION = 19, + + /// + /// Notification when a service is rejected + /// + SERVICE_RELEASE_REJECTION = 20, + + /// + /// Notification when the user roles are updated + /// + ROLE_UPDATE_CORE_OFFER = 21, + + /// + /// Notification when the user roles are updated for an offer + /// + ROLE_UPDATE_APP_OFFER = 22, + + /// + /// Notification when the url of a subscription is changed + /// + SUBSCRIPTION_URL_UPDATE = 23, + + /// + /// Notification when a credential got approved + /// + CREDENTIAL_APPROVAL = 24, + + /// + /// Notification when a credential got rejected + /// + CREDENTIAL_REJECTED = 25, + + /// + /// Notification when a credential got rejected + /// + CREDENTIAL_EXPIRY = 26 +} diff --git a/src/externalservices/Portal.Service/Portal.Service.csproj b/src/externalservices/Portal.Service/Portal.Service.csproj new file mode 100644 index 00000000..894c46d8 --- /dev/null +++ b/src/externalservices/Portal.Service/Portal.Service.csproj @@ -0,0 +1,35 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service + Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service + net7.0 + enable + enable + + + + + + + + diff --git a/src/externalservices/Portal.Service/Services/IPortalService.cs b/src/externalservices/Portal.Service/Services/IPortalService.cs new file mode 100644 index 00000000..4866840b --- /dev/null +++ b/src/externalservices/Portal.Service/Services/IPortalService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; + +public interface IPortalService +{ + Task AddNotification(string content, string bpnl, NotificationTypeId notificationTypeId, CancellationToken cancellationToken); + Task TriggerMail(string template, string bpnl, IDictionary mailParameters, CancellationToken cancellationToken); +} diff --git a/src/externalservices/Portal.Service/Services/PortalService.cs b/src/externalservices/Portal.Service/Services/PortalService.cs new file mode 100644 index 00000000..769e021a --- /dev/null +++ b/src/externalservices/Portal.Service/Services/PortalService.cs @@ -0,0 +1,60 @@ +/******************************************************************************** + * 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.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; + +public class PortalService : IPortalService +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly ITokenService _tokenService; + private readonly PortalSettings _settings; + + public PortalService(ITokenService tokenService, IOptions options) + { + _tokenService = tokenService; + _settings = options.Value; + } + + public async Task AddNotification(string content, string bpnl, NotificationTypeId notificationTypeId, CancellationToken cancellationToken) + { + var client = await _tokenService.GetAuthorizedClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new NotificationRequest(bpnl, content, notificationTypeId); + await client.PostAsJsonAsync("api/notifications/notifications", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("notification", HttpAsyncResponseMessageExtension.RecoverOptions.REQUEST_EXCEPTION) + .ConfigureAwait(false); + } + + public async Task TriggerMail(string template, string bpnl, IDictionary mailParameters, CancellationToken cancellationToken) + { + var client = await _tokenService.GetAuthorizedClient(_settings, cancellationToken).ConfigureAwait(false); + var data = new MailData(bpnl, template, mailParameters); + await client.PostAsJsonAsync("api/administration/mail", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("mail", HttpAsyncResponseMessageExtension.RecoverOptions.REQUEST_EXCEPTION) + .ConfigureAwait(false); + } +} diff --git a/src/externalservices/Wallet.Service/BusinessLogic/IWalletBusinessLogic.cs b/src/externalservices/Wallet.Service/BusinessLogic/IWalletBusinessLogic.cs new file mode 100644 index 00000000..e04edc06 --- /dev/null +++ b/src/externalservices/Wallet.Service/BusinessLogic/IWalletBusinessLogic.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 + ********************************************************************************/ + +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; +using EncryptionInformation = Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models.EncryptionInformation; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; + +public interface IWalletBusinessLogic +{ + Task CreateCredential(Guid companySsiDetailId, JsonDocument schema, CancellationToken cancellationToken); + Task SignCredential(Guid companySsiDetailId, Guid credentialId, CancellationToken cancellationToken); + Task CreateCredentialForHolder(Guid companySsiDetailId, string holderWalletUrl, string clientId, EncryptionInformation encryptionInformation, string credential, CancellationToken cancellationToken); + Task GetCredential(Guid credentialId, Guid externalCredentialId, VerifiedCredentialTypeKindId kindId, CancellationToken cancellationToken); +} diff --git a/src/externalservices/Wallet.Service/BusinessLogic/WalletBusinessLogic.cs b/src/externalservices/Wallet.Service/BusinessLogic/WalletBusinessLogic.cs new file mode 100644 index 00000000..58b68020 --- /dev/null +++ b/src/externalservices/Wallet.Service/BusinessLogic/WalletBusinessLogic.cs @@ -0,0 +1,118 @@ +/******************************************************************************** + * 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 Json.Schema; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Encryption; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Reflection; +using System.Security.Cryptography; +using System.Text.Json; +using EncryptionInformation = Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models.EncryptionInformation; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; + +public class WalletBusinessLogic : IWalletBusinessLogic +{ + private readonly IWalletService _walletService; + private readonly IIssuerRepositories _repositories; + private readonly WalletSettings _settings; + + public WalletBusinessLogic(IWalletService walletService, IIssuerRepositories repositories, IOptions options) + { + _walletService = walletService; + _repositories = repositories; + _settings = options.Value; + } + + public async Task CreateCredential(Guid companySsiDetailId, JsonDocument schema, CancellationToken cancellationToken) + { + var credentialId = await _walletService.CreateCredential(schema, cancellationToken).ConfigureAwait(false); + _repositories.GetInstance().AttachAndModifyCompanySsiDetails(companySsiDetailId, c => c.ExternalCredentialId = null, c => c.ExternalCredentialId = credentialId); + } + + public async Task SignCredential(Guid companySsiDetailId, Guid credentialId, CancellationToken cancellationToken) + { + var credential = await _walletService.SignCredential(credentialId, cancellationToken).ConfigureAwait(false); + _repositories.GetInstance().AttachAndModifyCompanySsiDetails(companySsiDetailId, c => c.Credential = null, c => c.Credential = credential); + } + + public async Task GetCredential(Guid credentialId, Guid externalCredentialId, VerifiedCredentialTypeKindId kindId, CancellationToken cancellationToken) + { + var credential = await _walletService.GetCredential(externalCredentialId, cancellationToken).ConfigureAwait(false); + await ValidateSchema(kindId, credential, cancellationToken).ConfigureAwait(false); + + using var stream = new MemoryStream(); + using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }); + credential.WriteTo(writer); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); + var documentContent = stream.ToArray(); + var hash = SHA512.HashData(documentContent); + var documentRepository = _repositories.GetInstance(); + var docId = documentRepository.CreateDocument("signed-credential.json", documentContent, hash, MediaTypeId.JSON, DocumentTypeId.VERIFIED_CREDENTIAL, null).Id; + documentRepository.AssignDocumentToCompanySsiDetails(docId, credentialId); + } + + private static async Task ValidateSchema(VerifiedCredentialTypeKindId kindId, JsonDocument content, CancellationToken cancellationToken) + { + var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (location == null) + { + throw new UnexpectedConditionException("Assembly location must be set"); + } + + var path = Path.Combine(location, "Schemas", $"{kindId}Credential.schema.json"); + var schemaJson = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false); + + var schema = JsonSchema.FromText(schemaJson); + SchemaRegistry.Global.Register(schema); + var results = schema.Evaluate(content); + if (!results.IsValid) + { + throw new ServiceException($"Invalid schema for type {kindId}"); + } + } + + public async Task CreateCredentialForHolder(Guid companySsiDetailId, string holderWalletUrl, string clientId, EncryptionInformation encryptionInformation, string credential, CancellationToken cancellationToken) + { + var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == encryptionInformation.EncryptionMode) ?? throw new ConfigurationException($"EncryptionModeIndex {encryptionInformation.EncryptionMode} is not configured"); + var secret = CryptoHelper.Decrypt(encryptionInformation.Secret, encryptionInformation.InitializationVector, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode); + + await _walletService + .CreateCredentialForHolder(holderWalletUrl, clientId, secret, credential, cancellationToken) + .ConfigureAwait(false); + + _repositories.GetInstance().AttachAndModifyProcessData(companySsiDetailId, + c => + { + c.ClientId = clientId; + c.ClientSecret = encryptionInformation.Secret; + }, + c => + { + c.ClientId = null; + c.ClientSecret = null; + }); + } +} diff --git a/src/externalservices/Wallet.Service/DependencyInjection/ServiceCollectionExtensions.cs b/src/externalservices/Wallet.Service/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8b9cd986 --- /dev/null +++ b/src/externalservices/Wallet.Service/DependencyInjection/ServiceCollectionExtensions.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + [ExcludeFromCodeCoverage] + public static IServiceCollection AddWalletService(this IServiceCollection services, IConfiguration config) + { + services.AddOptions() + .Bind(config.GetSection("Wallet")) + .ValidateOnStart(); + + services.AddTransient>(); + + var sp = services.BuildServiceProvider(); + var settings = sp.GetRequiredService>(); + return services + .AddScoped() + .AddScoped() + .AddScoped() + .AddCustomHttpClientWithAuthentication(settings.Value.BaseAddress); + } +} diff --git a/src/externalservices/Wallet.Service/DependencyInjection/WalletSettings.cs b/src/externalservices/Wallet.Service/DependencyInjection/WalletSettings.cs new file mode 100644 index 00000000..1360d199 --- /dev/null +++ b/src/externalservices/Wallet.Service/DependencyInjection/WalletSettings.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 Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; + +[ExcludeFromCodeCoverage] +public class WalletSettings : BasicAuthSettings +{ + [Required(AllowEmptyStrings = false)] + public string BaseAddress { get; set; } = null!; + + [Required] + public IEnumerable EncryptionConfigs { get; set; } = null!; + + [Required] + public int EncrptionConfigIndex { get; set; } +} diff --git a/src/externalservices/Wallet.Service/Models/CreateCredentialResponse.cs b/src/externalservices/Wallet.Service/Models/CreateCredentialResponse.cs new file mode 100644 index 00000000..74938f56 --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/CreateCredentialResponse.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record CreateCredentialRequest( + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("payload")] CredentialPayload Payload +); + +public record CredentialPayload( + [property: JsonPropertyName("issue")] JsonDocument Issue +); + +public record CreateCredentialResponse( + [property: JsonPropertyName("id")] Guid Id +); diff --git a/src/externalservices/Wallet.Service/Models/CredentialData.cs b/src/externalservices/Wallet.Service/Models/CredentialData.cs new file mode 100644 index 00000000..817c2d9b --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/CredentialData.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 System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record CredentialData( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("verifiableCredential")] string VerifiableCredential, + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("signing_key_id")] string SigningKeyId +); diff --git a/src/externalservices/Wallet.Service/Models/DeriveCredentialData.cs b/src/externalservices/Wallet.Service/Models/DeriveCredentialData.cs new file mode 100644 index 00000000..5cd9aa53 --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/DeriveCredentialData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record DeriveCredentialData( + [property: JsonPropertyName("application")] string Application, + [property: JsonPropertyName("payload")] DeriveCredentialPayload Payload +); + +public record DeriveCredentialPayload( + [property: JsonPropertyName("derive")] DeriveCredential Dervie +); + +public record DeriveCredential( + [property: JsonPropertyName("verifiableCredential")] string Credential +); diff --git a/src/externalservices/Wallet.Service/Models/EncryptionInformation.cs b/src/externalservices/Wallet.Service/Models/EncryptionInformation.cs new file mode 100644 index 00000000..4565d204 --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/EncryptionInformation.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record EncryptionInformation( + byte[] Secret, + byte[] InitializationVector, + int EncryptionMode +); diff --git a/src/externalservices/Wallet.Service/Models/GetCredentialResponse.cs b/src/externalservices/Wallet.Service/Models/GetCredentialResponse.cs new file mode 100644 index 00000000..19a6902c --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/GetCredentialResponse.cs @@ -0,0 +1,10 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record GetCredentialResponse( + [property: JsonPropertyName("verifiableCredential")] string VerifiableCredential, + [property: JsonPropertyName("credential")] JsonDocument Credential, + [property: JsonPropertyName("signing_key_id")] string SigningKeyId +); diff --git a/src/externalservices/Wallet.Service/Models/SignCredentialRequest.cs b/src/externalservices/Wallet.Service/Models/SignCredentialRequest.cs new file mode 100644 index 00000000..f3dc0442 --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/SignCredentialRequest.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 System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record SignCredentialRequest( + [property: JsonPropertyName("payload")] SignPayload Payload +); + +public record SignPayload( + [property: JsonPropertyName("sign")] SignUpdate Sign +); + +public record SignUpdate( + [property: JsonPropertyName("proofMechanism")] string ProofMechanism, + [property: JsonPropertyName("proofType")] string ProofType +); + +public record SignCredentialResponse( + [property: JsonPropertyName("jwt")] string Jwt +); diff --git a/src/externalservices/Wallet.Service/Schemas/BPNCredential.schema.json b/src/externalservices/Wallet.Service/Schemas/BPNCredential.schema.json new file mode 100644 index 00000000..45918c2a --- /dev/null +++ b/src/externalservices/Wallet.Service/Schemas/BPNCredential.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://eclipse-tractusx.github.io/BpnCredential.schema.json", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "const": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/catenax/credentials/v1.0.0" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VerifiableCredential", + "BpnCredential" + ] + } + }, + "issuanceDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "expirationDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "issuer": { + "type": "string" + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "holderIdentifier": { + "type": "string" + }, + "bpn": { + "type": "string" + } + }, + "required": ["id", "holderIdentifier", "bpn"], + "additionalProperties": false + } + }, + "required": ["id", "@context", "type", "issuanceDate", "expirationDate", "issuer", "credentialSubject"], + "additionalProperties": false +} diff --git a/src/externalservices/Wallet.Service/Schemas/FRAMEWORKCredential.schema.json b/src/externalservices/Wallet.Service/Schemas/FRAMEWORKCredential.schema.json new file mode 100644 index 00000000..182a1ea3 --- /dev/null +++ b/src/externalservices/Wallet.Service/Schemas/FRAMEWORKCredential.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://eclipse-tractusx.github.io/MembershipCredential.schema.json", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "const": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/catenax/credentials/v1.0.0" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string" + } + }, + "issuanceDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "expirationDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "issuer": { + "type": "string" + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "holderIdentifier": { + "type": "string" + }, + "group": { + "type": "string", + "const": "UseCaseFramework" + }, + "useCase": { + "type": "string" + }, + "contractTemplate": { + "type": "string" + }, + "contractVersion": { + "type": "string" + } + }, + "required": ["id", "holderIdentifier", "group", "useCase", "contractTemplate", "contractVersion"], + "additionalProperties": false + } + }, + "required": ["id", "@context", "type", "issuanceDate", "expirationDate", "issuer", "credentialSubject"], + "additionalProperties": false +} diff --git a/src/externalservices/Wallet.Service/Schemas/MEMBERSHIPCredential.schema.json b/src/externalservices/Wallet.Service/Schemas/MEMBERSHIPCredential.schema.json new file mode 100644 index 00000000..1b796039 --- /dev/null +++ b/src/externalservices/Wallet.Service/Schemas/MEMBERSHIPCredential.schema.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://eclipse-tractusx.github.io/MembershipCredential.schema.json", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "@context": { + "type": "array", + "items": { + "type": "string" + }, + "const": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/catenax/credentials/v1.0.0" + ] + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VerifiableCredential", + "MembershipCredential" + ] + } + }, + "issuanceDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "expirationDate": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|\\+\\d{2}:\\d{2})?$" + }, + "issuer": { + "type": "string" + }, + "credentialSubject": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "holderIdentifier": { + "type": "string" + }, + "memberOf": { + "type": "string" + } + }, + "required": ["id", "holderIdentifier", "memberOf"], + "additionalProperties": false + } + }, + "required": ["id", "@context", "type", "issuanceDate", "expirationDate", "issuer", "credentialSubject"], + "additionalProperties": false +} diff --git a/src/externalservices/Wallet.Service/Services/BasicAuthSettings.cs b/src/externalservices/Wallet.Service/Services/BasicAuthSettings.cs new file mode 100644 index 00000000..3589d2c0 --- /dev/null +++ b/src/externalservices/Wallet.Service/Services/BasicAuthSettings.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.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +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!; +} + +public record GetBasicTokenSettings(string HttpClientName, string ClientId, string ClientSecret, string TokenAddress); diff --git a/src/externalservices/Wallet.Service/Services/BasicAuthTokenService.cs b/src/externalservices/Wallet.Service/Services/BasicAuthTokenService.cs new file mode 100644 index 00000000..fae20579 --- /dev/null +++ b/src/externalservices/Wallet.Service/Services/BasicAuthTokenService.cs @@ -0,0 +1,72 @@ +/******************************************************************************** + * 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.HttpClientExtensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +public class BasicAuthTokenService : IBasicAuthTokenService +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private readonly IHttpClientFactory _httpClientFactory; + + public BasicAuthTokenService(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + + public async Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken) + { + var tokenParameters = new GetBasicTokenSettings( + $"{typeof(T).Name}Auth", + settings.ClientId, + settings.ClientSecret, + settings.TokenAddress); + + 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; + } + + private async Task GetBasicTokenAsync(GetBasicTokenSettings settings, CancellationToken cancellationToken) + { + var formParameters = new Dictionary + { + { "grant_type", "client_credentials" } + }; + using var content = new FormUrlEncodedContent(formParameters); + var authClient = _httpClientFactory.CreateClient(settings.HttpClientName); + var authenticationString = $"{settings.ClientId}:{settings.ClientSecret}"; + var base64String = Convert.ToBase64String(System.Text.Encoding.ASCII.GetBytes(authenticationString)); + + authClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64String); + + var response = await authClient.PostAsync(settings.TokenAddress, content, cancellationToken) + .CatchingIntoServiceExceptionFor("token-post", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE).ConfigureAwait(false); + + var responseObject = await response.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + return responseObject?.AccessToken; + } +} diff --git a/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs b/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs new file mode 100644 index 00000000..e379cb34 --- /dev/null +++ b/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +public interface IBasicAuthTokenService +{ + Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken); +} diff --git a/src/externalservices/Wallet.Service/Services/IWalletService.cs b/src/externalservices/Wallet.Service/Services/IWalletService.cs new file mode 100644 index 00000000..27365f47 --- /dev/null +++ b/src/externalservices/Wallet.Service/Services/IWalletService.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; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +public interface IWalletService +{ + Task CreateCredential(JsonDocument payload, CancellationToken cancellationToken); + Task SignCredential(Guid credentialId, CancellationToken cancellationToken); + Task CreateCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, string credential, CancellationToken cancellationToken); + Task GetCredential(Guid externalCredentialId, CancellationToken cancellationToken); +} diff --git a/src/externalservices/Wallet.Service/Services/WalletService.cs b/src/externalservices/Wallet.Service/Services/WalletService.cs new file mode 100644 index 00000000..e5fb59ff --- /dev/null +++ b/src/externalservices/Wallet.Service/Services/WalletService.cs @@ -0,0 +1,116 @@ +/******************************************************************************** + * 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.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; +using System.Net.Http.Json; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +public class WalletService : IWalletService +{ + private const string NoIdErrorMessage = "Response must contain a valid id"; + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + + private readonly IBasicAuthTokenService _basicAuthTokenService; + private readonly WalletSettings _settings; + + public WalletService(IBasicAuthTokenService basicAuthTokenService, IOptions options) + { + _basicAuthTokenService = basicAuthTokenService; + _settings = options.Value; + } + + public async Task CreateCredential(JsonDocument payload, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken); + var data = new CreateCredentialRequest("catena-x-portal", new CredentialPayload(payload)); + var result = await client.PostAsJsonAsync("api/v2.0.0/credentials", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + var response = await result.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + if (response == null) + { + throw new ConflictException(NoIdErrorMessage); + } + + return response.Id; + } + + public async Task SignCredential(Guid credentialId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken); + var data = new SignCredentialRequest(new SignPayload(new SignUpdate("external", "jwt"))); + var result = await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{credentialId}", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("sign-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + var response = await result.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + if (response is null) + { + throw new ServiceException(NoIdErrorMessage, true); + } + + return response.Jwt; + } + + public async Task GetCredential(Guid externalCredentialId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken); + var result = await client.GetAsync($"/api/v2.0.0/credentials/{externalCredentialId}", cancellationToken) + .CatchingIntoServiceExceptionFor("get-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + var response = await result.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + if (response is null) + { + throw new ServiceException(NoIdErrorMessage, true); + } + + return response.Credential; + } + + public async Task CreateCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, string credential, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + ClientId = clientId, + ClientSecret = clientSecret, + TokenAddress = $"{holderWalletUrl}/oauth/token" + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken); + var data = new DeriveCredentialData("catena-x-portal", new DeriveCredentialPayload(new DeriveCredential(credential))); + var result = await client.PostAsJsonAsync("/api/v2.0.0/credentials", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("create-holder-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + var response = await result.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + if (response is null) + { + throw new ServiceException(NoIdErrorMessage, true); + } + + return response.Id; + } +} diff --git a/src/externalservices/Wallet.Service/Wallet.Service.csproj b/src/externalservices/Wallet.Service/Wallet.Service.csproj new file mode 100644 index 00000000..c37e3a95 --- /dev/null +++ b/src/externalservices/Wallet.Service/Wallet.Service.csproj @@ -0,0 +1,67 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service + Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service + net7.0 + enable + enable + + + + + + + + + + + + + + true + Always + PreserveNewest + + + true + Always + PreserveNewest + + + true + Always + PreserveNewest + + + + Always + + + + Always + + + + Always + + + diff --git a/src/issuer/SsiCredentialIssuer.Service/Authentication/CustomClaimTypes.cs b/src/issuer/SsiCredentialIssuer.Service/Authentication/CustomClaimTypes.cs new file mode 100644 index 00000000..688d6877 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Authentication/CustomClaimTypes.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Authentication; + +public static class CustomClaimTypes +{ + public const string Sub = "sub"; + public const string ClientId = "clientId"; + public const string PreferredUserName = "preferred_username"; + public const string ResourceAccess = "resource_access"; +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Authentication/KeycloakClaimsTransformation.cs b/src/issuer/SsiCredentialIssuer.Service/Authentication/KeycloakClaimsTransformation.cs new file mode 100644 index 00000000..321a1c7b --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Authentication/KeycloakClaimsTransformation.cs @@ -0,0 +1,73 @@ +/******************************************************************************** + * 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.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using System.Json; +using System.Security.Claims; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Authentication +{ + public class KeycloakClaimsTransformation : IClaimsTransformation + { + private readonly JwtBearerOptions _options; + + public KeycloakClaimsTransformation(IOptions options) + { + _options = options.Value; + } + + public Task TransformAsync(ClaimsPrincipal principal) + { + var claimsIdentity = new ClaimsIdentity(); + if (AddRoles(principal, claimsIdentity)) + { + principal.AddIdentity(claimsIdentity); + } + + return Task.FromResult(principal); + } + + private bool AddRoles(ClaimsPrincipal principal, ClaimsIdentity claimsIdentity) => + principal.Claims + .Where(claim => + claim.Type == CustomClaimTypes.ResourceAccess && + claim.ValueType == "JSON") + .SelectMany(claim => + JsonValue.Parse(claim.Value) is JsonObject jsonObject && + jsonObject.TryGetValue( + _options.TokenValidationParameters.ValidAudience, + out var audience) && + audience is JsonObject client && + client.TryGetValue("roles", out var jsonRoles) && + jsonRoles is JsonArray roles + ? roles.Where(x => x.JsonType == JsonType.String) + .Select(role => new Claim(ClaimTypes.Role, role)) + : Enumerable.Empty()) + .IfAny(claims => + { + foreach (var claim in claims) + { + claimsIdentity.AddClaim(claim); + } + }); + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs new file mode 100644 index 00000000..651fd4d6 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialBusinessLogic.cs @@ -0,0 +1,455 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.HttpClientExtensions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Encryption; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; +using System.Globalization; +using System.Security.Cryptography; +using System.Text.Json; +using ErrorParameter = Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.ErrorParameter; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; + +public class CredentialBusinessLogic : ICredentialBusinessLogic +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private static readonly IEnumerable Context = new[] { "https://www.w3.org/2018/credentials/v1", "https://w3id.org/catenax/credentials/v1.0.0" }; + + private readonly IIssuerRepositories _repositories; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IHttpClientFactory _clientFactory; + private readonly IPortalService _portalService; + private readonly CredentialSettings _settings; + private readonly IIdentityData _identity; + + /// + /// Constructor + /// + /// + /// + /// + /// + /// + /// + public CredentialBusinessLogic( + IIssuerRepositories repositories, + IIdentityService identityService, + IDateTimeProvider dateTimeProvider, + IHttpClientFactory clientFactory, + IPortalService portalService, + IOptions options) + { + _repositories = repositories; + _identity = identityService.IdentityData; + _dateTimeProvider = dateTimeProvider; + _clientFactory = clientFactory; + _portalService = portalService; + _settings = options.Value; + } + + /// + public async Task> GetUseCaseParticipationAsync() => + await _repositories + .GetInstance() + .GetUseCaseParticipationForCompany(_identity.Bpnl, _dateTimeProvider.OffsetNow) + .Select(x => new UseCaseParticipationData( + x.UseCase, + x.Description, + x.CredentialType, + x.VerifiedCredentials + .Select(y => + new CompanySsiExternalTypeDetailData( + y.ExternalDetailData, + y.SsiDetailData.CatchingInto( + data => data + .Select(d => new CompanySsiDetailData( + d.CredentialId, + d.ParticipationStatus, + d.ExpiryDate, + d.Documents)) + .SingleOrDefault(), + (InvalidOperationException _) => throw ConflictException.Create(CompanyDataErrors.MULTIPLE_SSI_DETAIL)))) + .ToList())) + .ToListAsync() + .ConfigureAwait(false); + + /// + public async Task> GetSsiCertificatesAsync() => + await _repositories + .GetInstance() + .GetSsiCertificates(_identity.Bpnl, _dateTimeProvider.OffsetNow) + .Select(x => new CertificateParticipationData( + x.CredentialType, + x.Credentials + .Select(y => + new CompanySsiExternalTypeDetailData( + y.ExternalDetailData, + y.SsiDetailData.CatchingInto( + data => data + .Select(d => new CompanySsiDetailData( + d.CredentialId, + d.ParticipationStatus, + d.ExpiryDate, + d.Documents)) + .SingleOrDefault(), + (InvalidOperationException _) => throw ConflictException.Create(CompanyDataErrors.MULTIPLE_SSI_DETAIL)))) + .ToList())) + .ToListAsync() + .ConfigureAwait(false); + + /// + public Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting) + { + var query = _repositories + .GetInstance() + .GetAllCredentialDetails(companySsiDetailStatusId, credentialTypeId); + var sortedQuery = sorting switch + { + CompanySsiDetailSorting.BpnlAsc or null => query.OrderBy(c => c.Bpnl), + CompanySsiDetailSorting.BpnlDesc => query.OrderByDescending(c => c.Bpnl), + _ => query + }; + + return Pagination.CreateResponseAsync(page, size, _settings.MaxPageSize, (skip, take) => + new Pagination.AsyncSource + ( + query.CountAsync(), + sortedQuery + .Skip(skip) + .Take(take) + .Select(c => new CredentialDetailData( + c.Id, + c.Bpnl, + c.VerifiedCredentialTypeId, + c.VerifiedCredentialType!.VerifiedCredentialTypeAssignedUseCase!.UseCase!.Name, + c.CompanySsiDetailStatusId, + c.ExpiryDate, + c.Documents.Select(d => new DocumentData(d.Id, d.DocumentName, d.DocumentTypeId)), + c.VerifiedCredentialExternalTypeDetailVersion == null + ? null + : new ExternalTypeDetailData( + c.VerifiedCredentialExternalTypeDetailVersion.Id, + c.VerifiedCredentialExternalTypeDetailVersion.VerifiedCredentialExternalTypeId, + c.VerifiedCredentialExternalTypeDetailVersion.Version, + c.VerifiedCredentialExternalTypeDetailVersion.Template, + c.VerifiedCredentialExternalTypeDetailVersion.ValidFrom, + c.VerifiedCredentialExternalTypeDetailVersion.Expiry)) + ).AsAsyncEnumerable() + )); + } + + /// + public async Task ApproveCredential(Guid credentialId, CancellationToken cancellationToken) + { + var companySsiRepository = _repositories.GetInstance(); + var (exists, data) = await companySsiRepository.GetSsiApprovalData(credentialId).ConfigureAwait(false); + ValidateApprovalData(credentialId, exists, data); + + var processStepRepository = _repositories.GetInstance(); + var processId = processStepRepository.CreateProcess(ProcessTypeId.CREATE_CREDENTIAL).Id; + processStepRepository.CreateProcessStep(ProcessStepTypeId.CREATE_CREDENTIAL, ProcessStepStatusId.TODO, processId); + + var expiryDate = GetExpiryDate(data.DetailData?.ExpiryDate); + companySsiRepository.AttachAndModifyCompanySsiDetails(credentialId, c => + { + c.CompanySsiDetailStatusId = data.Status; + c.ExpiryDate = DateTimeOffset.MinValue; + c.ProcessId = null; + }, + c => + { + c.CompanySsiDetailStatusId = CompanySsiDetailStatusId.ACTIVE; + c.DateLastChanged = _dateTimeProvider.OffsetNow; + c.ExpiryDate = expiryDate; + c.ProcessId = processId; + }); + + var typeValue = data.Type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CompanyDataErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", data.Type.ToString()) }); + var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options); + await _portalService.AddNotification(content, _identity.Bpnl, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(false); + var mailParameters = new Dictionary + { + { "requestName", typeValue }, + { "credentialType", typeValue }, + { "expiryDate", expiryDate.ToString("o", CultureInfo.InvariantCulture) } + }; + await _portalService.TriggerMail("CredentialApproval", _identity.Bpnl, mailParameters, cancellationToken).ConfigureAwait(false); + await _repositories.SaveAsync().ConfigureAwait(false); + } + + private static void ValidateApprovalData(Guid credentialId, bool exists, SsiApprovalData data) + { + if (!exists) + { + throw NotFoundException.Create(CompanyDataErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) }); + } + + if (data.Status != CompanySsiDetailStatusId.PENDING) + { + throw ConflictException.Create(CompanyDataErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) }); + } + + if (string.IsNullOrWhiteSpace(data.Bpn)) + { + throw UnexpectedConditionException.Create(CompanyDataErrors.BPN_NOT_SET); + } + + if (data.DetailData == null && data.Kind == VerifiedCredentialTypeKindId.FRAMEWORK) + { + throw ConflictException.Create(CompanyDataErrors.EXTERNAL_TYPE_DETAIL_ID_NOT_SET); + } + + if (data.Kind != VerifiedCredentialTypeKindId.FRAMEWORK && data.Kind != VerifiedCredentialTypeKindId.MEMBERSHIP && data.Kind != VerifiedCredentialTypeKindId.BPN) + { + throw ConflictException.Create(CompanyDataErrors.KIND_NOT_SUPPORTED, new ErrorParameter[] { new("kind", data.Kind != null ? data.Kind.Value.ToString() : "empty kind") }); + } + + if (data.Kind == VerifiedCredentialTypeKindId.FRAMEWORK && string.IsNullOrWhiteSpace(data.DetailData!.Version)) + { + throw ConflictException.Create(CompanyDataErrors.EMPTY_VERSION); + } + } + + private DateTimeOffset GetExpiryDate(DateTimeOffset? expiryDate) + { + var now = _dateTimeProvider.OffsetNow; + var future = now.AddMonths(12); + var expiry = expiryDate ?? future; + + if (expiry < now) + { + throw ConflictException.Create(CompanyDataErrors.EXPIRY_DATE_IN_PAST); + } + + return expiry > future ? future : expiry; + } + + /// + public async Task RejectCredential(Guid credentialId, CancellationToken cancellationToken) + { + var companySsiRepository = _repositories.GetInstance(); + var (exists, status, type) = await companySsiRepository.GetSsiRejectionData(credentialId).ConfigureAwait(false); + if (!exists) + { + throw NotFoundException.Create(CompanyDataErrors.SSI_DETAILS_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) }); + } + + if (status != CompanySsiDetailStatusId.PENDING) + { + throw ConflictException.Create(CompanyDataErrors.CREDENTIAL_NOT_PENDING, new ErrorParameter[] { new("credentialId", credentialId.ToString()), new("status", CompanySsiDetailStatusId.PENDING.ToString()) }); + } + + var typeValue = type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CompanyDataErrors.CREDENTIAL_TYPE_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialType", type.ToString()) }); + var content = JsonSerializer.Serialize(new { Type = type, CredentialId = credentialId }, Options); + await _portalService.AddNotification(content, _identity.Bpnl, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken).ConfigureAwait(false); + + companySsiRepository.AttachAndModifyCompanySsiDetails(credentialId, c => + { + c.CompanySsiDetailStatusId = status; + }, + c => + { + c.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + c.DateLastChanged = _dateTimeProvider.OffsetNow; + }); + + await _repositories.SaveAsync().ConfigureAwait(false); + + var mailParameters = new Dictionary + { + { "requestName", typeValue }, + { "reason", "Declined by the Operator" } + }; + + await _portalService.TriggerMail("CredentialRejected", _identity.Bpnl, mailParameters, cancellationToken).ConfigureAwait(false); + } + + /// + public IAsyncEnumerable GetCertificateTypes() => + _repositories.GetInstance().GetCertificateTypes(_identity.Bpnl); + + public async Task CreateBpnCredential(CreateBpnCredentialRequest requestData, CancellationToken cancellationToken) + { + var companyCredentialDetailsRepository = _repositories.GetInstance(); + var holderDid = await GetHolderInformation(requestData.Holder, cancellationToken).ConfigureAwait(false); + var schemaData = new BpnCredential( + Guid.NewGuid(), + Context, + new[] { "VerifiableCredential", "BpnCredential" }, + "BpnCredential", + "Bpn Credential", + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddMonths(12), + _settings.IssuerDid, + new BpnCredentialSubject( + holderDid, + requestData.BusinessPartnerNumber, + requestData.BusinessPartnerNumber + ) + ); + var schema = JsonSerializer.Serialize(schemaData, Options); + return await HandleCredentialProcessCreation(VerifiedCredentialTypeKindId.BPN, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, schema, requestData.TechnicalUserDetails, null, companyCredentialDetailsRepository); + } + + public async Task CreateMembershipCredential(CreateMembershipCredentialRequest requestData, CancellationToken cancellationToken) + { + var companyCredentialDetailsRepository = _repositories.GetInstance(); + + var holderDid = await GetHolderInformation(requestData.Holder, cancellationToken).ConfigureAwait(false); + var schemaData = new MembershipCredential( + Guid.NewGuid(), + Context, + new[] { "VerifiableCredential", "MembershipCredential" }, + "MembershipCredential", + "Membership Credential", + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow.AddMonths(12), + _settings.IssuerDid, + new MembershipCredentialSubject( + holderDid, + requestData.HolderBpn, + requestData.MemberOf + ) + ); + var schema = JsonSerializer.Serialize(schemaData, Options); + return await HandleCredentialProcessCreation(VerifiedCredentialTypeKindId.MEMBERSHIP, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, schema, requestData.TechnicalUserDetails, null, companyCredentialDetailsRepository); + } + + public async Task CreateFrameworkCredential(CreateFrameworkCredentialRequest requestData, CancellationToken cancellationToken) + { + var companyCredentialDetailsRepository = _repositories.GetInstance(); + var result = await companyCredentialDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(requestData.UseCaseFrameworkVersionId, requestData.UseCaseFrameworkId).ConfigureAwait(false); + if (!result.Exists) + { + throw ControllerArgumentException.Create(CompanyDataErrors.EXTERNAL_TYPE_DETAIL_NOT_FOUND, new ErrorParameter[] { new("verifiedCredentialExternalTypeDetailId", requestData.UseCaseFrameworkId.ToString()) }); + } + + if (result.Expiry < _dateTimeProvider.OffsetNow) + { + throw ControllerArgumentException.Create(CompanyDataErrors.EXPIRY_DATE_IN_PAST); + } + + if (string.IsNullOrWhiteSpace(result.Version)) + { + throw ControllerArgumentException.Create(CompanyDataErrors.EMPTY_VERSION); + } + + if (string.IsNullOrWhiteSpace(result.Template)) + { + throw ControllerArgumentException.Create(CompanyDataErrors.EMPTY_TEMPLATE); + } + + if (result.UseCase.Count() != 1) + { + throw ControllerArgumentException.Create(CompanyDataErrors.MULTIPLE_USE_CASES); + } + + var useCase = result.UseCase.Single(); + var holderDid = await GetHolderInformation(requestData.Holder, cancellationToken).ConfigureAwait(false); + var schemaData = new FrameworkCredential( + Guid.NewGuid(), + Context, + new[] { "VerifiableCredential", $"{useCase}Credential" }, + $"{useCase}Credential", + $"Framework Credential for UseCase {useCase}", + DateTimeOffset.UtcNow, + result.Expiry, + _settings.IssuerDid, + new FrameworkCredentialSubject( + holderDid, + requestData.HolderBpn, + "UseCaseFramework", + useCase, + result.Template!, + result.Version! + ) + ); + var schema = JsonSerializer.Serialize(schemaData, Options); + return await HandleCredentialProcessCreation(VerifiedCredentialTypeKindId.FRAMEWORK, requestData.UseCaseFrameworkId, schema, requestData.TechnicalUserDetails, requestData.UseCaseFrameworkVersionId, companyCredentialDetailsRepository); + } + + private async Task GetHolderInformation(string didDocumentLocation, CancellationToken cancellationToken) + { + var client = _clientFactory.CreateClient("didDocumentDownload"); + var result = await client.GetAsync(didDocumentLocation, cancellationToken) + .CatchingIntoServiceExceptionFor("get-did-document").ConfigureAwait(false); + var did = await result.Content.ReadFromJsonAsync(Options, cancellationToken).ConfigureAwait(false); + if (did == null) + { + throw ConflictException.Create(CompanyDataErrors.DID_NOT_SET); + } + + return did.Id; + } + + private async Task HandleCredentialProcessCreation(VerifiedCredentialTypeKindId kindId, VerifiedCredentialTypeId typeId, string schema, TechnicalUserDetails? technicalUserDetails, Guid? detailVersionId, ICompanySsiDetailsRepository companyCredentialDetailsRepository) + { + var documentContent = System.Text.Encoding.UTF8.GetBytes(schema); + var hash = SHA512.HashData(documentContent); + var documentRepository = _repositories.GetInstance(); + var docId = documentRepository.CreateDocument("schema.json", documentContent, + hash, MediaTypeId.JSON, DocumentTypeId.PRESENTATION, x => + { + x.CompanyUserId = _identity.IdentityId; + x.DocumentStatusId = DocumentStatusId.ACTIVE; + }).Id; + + var ssiDetailId = companyCredentialDetailsRepository.CreateSsiDetails( + _identity.Bpnl, + typeId, docId, CompanySsiDetailStatusId.PENDING, + _identity.IdentityId, + c => c.VerifiedCredentialExternalTypeDetailVersionId = detailVersionId).Id; + documentRepository.AssignDocumentToCompanySsiDetails(docId, ssiDetailId); + + companyCredentialDetailsRepository.CreateProcessData(ssiDetailId, JsonDocument.Parse(schema), kindId, + c => + { + if (technicalUserDetails == null) + { + return; + } + + var cryptoConfig = _settings.EncryptionConfigs.SingleOrDefault(x => x.Index == _settings.EncrptionConfigIndex) ?? throw new ConfigurationException($"EncryptionModeIndex {_settings.EncrptionConfigIndex} is not configured"); + var (secret, initializationVector) = CryptoHelper.Encrypt(technicalUserDetails.ClientSecret, Convert.FromHexString(cryptoConfig.EncryptionKey), cryptoConfig.CipherMode, cryptoConfig.PaddingMode); + + c.ClientId = technicalUserDetails.ClientId; + c.ClientSecret = secret; + c.InitializationVector = initializationVector; + c.HolderWalletUrl = technicalUserDetails.WalletUrl; + }); + + await _repositories.SaveAsync().ConfigureAwait(false); + return ssiDetailId; + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialSettings.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialSettings.cs new file mode 100644 index 00000000..6422790a --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/CredentialSettings.cs @@ -0,0 +1,62 @@ +/******************************************************************************** + * 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.Models.Configuration; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Validation; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; + +public class CredentialSettings +{ + /// + /// The Did of the issuer + /// + public string IssuerDid { get; set; } = null!; + + /// + /// The maximum amount of elements for a page + /// + public int MaxPageSize { get; set; } + + [Required] + public IEnumerable EncryptionConfigs { get; set; } = null!; + + [Required] + public int EncrptionConfigIndex { get; set; } +} + +public static class CompanyDataSettingsExtensions +{ + [ExcludeFromCodeCoverage] + public static IServiceCollection ConfigureCredentialSettings( + this IServiceCollection services, + IConfigurationSection section + ) + { + services.AddOptions() + .Bind(section) + .ValidateDistinctValues(section) + .ValidateEnumEnumeration(section) + .ValidateOnStart(); + return services; + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.cs new file mode 100644 index 00000000..730d9072 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/ICredentialBusinessLogic.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 Org.Eclipse.TractusX.Portal.Backend.Framework.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; + +public interface ICredentialBusinessLogic +{ + Task> GetUseCaseParticipationAsync(); + + Task> GetSsiCertificatesAsync(); + + Task> GetCredentials(int page, int size, CompanySsiDetailStatusId? companySsiDetailStatusId, VerifiedCredentialTypeId? credentialTypeId, CompanySsiDetailSorting? sorting); + + Task ApproveCredential(Guid credentialId, CancellationToken cancellationToken); + + Task RejectCredential(Guid credentialId, CancellationToken cancellationToken); + + IAsyncEnumerable GetCertificateTypes(); + Task CreateBpnCredential(CreateBpnCredentialRequest requestData, CancellationToken cancellationToken); + Task CreateMembershipCredential(CreateMembershipCredentialRequest requestData, CancellationToken cancellationToken); + Task CreateFrameworkCredential(CreateFrameworkCredentialRequest requestData, CancellationToken cancellationToken); +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs b/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs new file mode 100644 index 00000000..5ddeb2e7 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Controllers/CredentialController.cs @@ -0,0 +1,178 @@ +/******************************************************************************** + * 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.AspNetCore.Mvc; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Extensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; +using System.Diagnostics.CodeAnalysis; +using Constants = Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models.Constants; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Controllers; + +/// +/// Creates a new instance of +/// +public static class CredentialController +{ + private const string RequestSsiRole = "request_ssicredential"; + private const string DecisionSsiRole = "decision_ssicredential"; + + [ExcludeFromCodeCoverage] + public static RouteGroupBuilder MapCompanyDataApi(this RouteGroupBuilder group) + { + var issuer = group.MapGroup("/issuer"); + + issuer.MapGet("useCaseParticipation", (ICredentialBusinessLogic logic) => logic.GetUseCaseParticipationAsync()) + .WithSwaggerDescription("Gets all use case frameworks and the participation status of the acting company", + "Example: GET: api/issuer/useCaseParticipation") + .RequireAuthorization(r => + { + r.RequireRole("view_use_case_participation"); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(IEnumerable), Constants.JsonContentType) + .Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType); + + issuer.MapGet("certificates", (ICredentialBusinessLogic logic) => logic.GetSsiCertificatesAsync()) + .WithSwaggerDescription("Gets all company certificate requests and their status", + "Example: GET: api/issuer/certificates") + .RequireAuthorization(r => + { + r.RequireRole("view_certificates"); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(IEnumerable), Constants.JsonContentType); + + issuer.MapGet("certificateTypes", (ICredentialBusinessLogic logic) => logic.GetCertificateTypes()) + .WithSwaggerDescription("Gets the certificate types for which the company can apply for", + "Example: GET: api/issuer/certificateTypes") + .RequireAuthorization(r => + { + r.RequireRole(RequestSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(IEnumerable), Constants.JsonContentType); + + issuer.MapGet(string.Empty, (ICredentialBusinessLogic logic, [FromQuery] int? page, + [FromQuery] int? size, + [FromQuery] CompanySsiDetailStatusId? companySsiDetailStatusId, + [FromQuery] VerifiedCredentialTypeId? credentialTypeId, + [FromQuery] CompanySsiDetailSorting? sorting) => logic.GetCredentials(page ?? 0, size ?? 15, + companySsiDetailStatusId, credentialTypeId, sorting)) + .WithSwaggerDescription("Gets all outstanding, existing and inactive credentials", + "Example: GET: /api/issuer", + "The page to get", + "Amount of entries", + "OPTIONAL: Filter for the status", + "OPTIONAL: The type of the credential that should be returned", + "OPTIONAL: Search string for the company name", + "Defines the sorting of the list") + .RequireAuthorization(r => r.RequireRole(DecisionSsiRole)) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(IEnumerable), Constants.JsonContentType); + + issuer.MapPost("bpn", ([FromBody] CreateBpnCredentialRequest requestData, CancellationToken cancellationToken, ICredentialBusinessLogic logic) => logic.CreateBpnCredential(requestData, cancellationToken)) + .WithSwaggerDescription("Creates a bpn credential for the given data", + "POST: api/issuer/bpn", + "The request data containing information over the credential that should be created") + .RequireAuthorization(r => + { + r.RequireRole(RequestSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(Guid), contentType: Constants.JsonContentType); + + issuer.MapPost("membership", ([FromBody] CreateMembershipCredentialRequest requestData, CancellationToken cancellationToken, ICredentialBusinessLogic logic) => logic.CreateMembershipCredential(requestData, cancellationToken)) + .WithSwaggerDescription("Creates a membership credential for the given data", + "POST: api/issuer/membership", + "The request data containing information over the credential that should be created") + .RequireAuthorization(r => + { + r.RequireRole(RequestSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(Guid), contentType: Constants.JsonContentType); + + issuer.MapPost("framework", ([FromBody] CreateFrameworkCredentialRequest requestData, CancellationToken cancellationToken, ICredentialBusinessLogic logic) => logic.CreateFrameworkCredential(requestData, cancellationToken)) + .WithSwaggerDescription("Creates a framework credential for the given data", + "POST: api/issuer/framework", + "The request data containing information over the credential that should be created") + .RequireAuthorization(r => + { + r.RequireRole(RequestSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(Guid), contentType: Constants.JsonContentType); + + issuer.MapPut("{credentialId}/approval", async ([FromRoute] Guid credentialId, CancellationToken cancellationToken, ICredentialBusinessLogic logic) => + { + await logic.ApproveCredential(credentialId, cancellationToken).ConfigureAwait(false); + return Results.NoContent(); + }) + .WithSwaggerDescription("Approves the given credential and triggers the verified credential creation", + "PUT: api/issuer/{credentialId}/approval", + "Id of the entry that should be approved", + "Cancellation Token") + .RequireAuthorization(r => + { + r.RequireRole(DecisionSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status204NoContent, contentType: Constants.JsonContentType) + .Produces(StatusCodes.Status404NotFound, typeof(ErrorResponse), Constants.JsonContentType) + .Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType); + + issuer.MapPut("{credentialId}/reject", async ([FromRoute] Guid credentialId, CancellationToken cancellationToken, ICredentialBusinessLogic logic) => + { + await logic.RejectCredential(credentialId, cancellationToken).ConfigureAwait(false); + return Results.NoContent(); + }) + .WithSwaggerDescription("Rejects the given credential", + "PUT: api/issuer/{credentialId}/reject", + "Id of the entry that should be rejected") + .RequireAuthorization(r => + { + r.RequireRole(DecisionSsiRole); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status204NoContent, contentType: Constants.JsonContentType) + .Produces(StatusCodes.Status404NotFound, typeof(ErrorResponse), Constants.JsonContentType) + .Produces(StatusCodes.Status409Conflict, typeof(ErrorResponse), Constants.JsonContentType); + + return group; + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/CredentialServiceCollectionExtensions.cs b/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/CredentialServiceCollectionExtensions.cs new file mode 100644 index 00000000..ea4e3804 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/CredentialServiceCollectionExtensions.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 + ********************************************************************************/ + +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.DependencyInjection; + +public static class CredentialServiceCollectionExtensions +{ + [ExcludeFromCodeCoverage] + public static IServiceCollection AddCredentialService(this IServiceCollection services, IConfigurationSection section) => + services + .ConfigureCredentialSettings(section) + .AddTransient(); +} diff --git a/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CompanyDataErrorMessageContainer.cs b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CompanyDataErrorMessageContainer.cs new file mode 100644 index 00000000..15fa4f87 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/CompanyDataErrorMessageContainer.cs @@ -0,0 +1,88 @@ +/******************************************************************************** + * 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.ErrorHandling.Service; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; + +[ExcludeFromCodeCoverage] +public class CompanyDataErrorMessageContainer : IErrorMessageContainer +{ + private static readonly IReadOnlyDictionary _messageContainer = new Dictionary { + { CompanyDataErrors.INVALID_COMPANY, "company {companyId} is not a valid company" }, + { CompanyDataErrors.INVALID_COMPANY_STATUS, "Company Status is Incorrect" }, + { CompanyDataErrors.USE_CASE_NOT_FOUND, "UseCaseId {useCaseId} is not available" }, + { CompanyDataErrors.INVALID_LANGUAGECODE, "language {languageShortName} is not a valid languagecode" }, + { CompanyDataErrors.COMPANY_NOT_FOUND, "company {companyId} does not exist" }, + { CompanyDataErrors.COMPANY_ROLE_IDS_CONSENT_STATUS_NULL, "neither CompanyRoleIds nor ConsentStatusDetails should ever be null here" }, + { CompanyDataErrors.MISSING_AGREEMENTS, "All agreements need to get signed as Active or InActive. Missing consents: [{missingConsents}]" }, + { CompanyDataErrors.UNASSIGN_ALL_ROLES, "Company can't unassign from all roles, Atleast one Company role need to signed as active" }, + { CompanyDataErrors.AGREEMENTS_NOT_ASSIGNED_WITH_ROLES, "Agreements not associated with requested companyRoles: [{companyRoles}]" }, + { CompanyDataErrors.MULTIPLE_SSI_DETAIL, "There should only be one pending or active ssi detail be assigne" }, + { CompanyDataErrors.EXTERNAL_TYPE_DETAIL_NOT_FOUND, "VerifiedCredentialExternalTypeDetail {verifiedCredentialExternalTypeDetailId} does not exist" }, + { CompanyDataErrors.EXPIRY_DATE_IN_PAST, "The expiry date must not be in the past" }, + { CompanyDataErrors.CREDENTIAL_NO_CERTIFICATE, "{credentialTypeId} is not assigned to a certificate" }, + { CompanyDataErrors.EXTERNAL_TYPE_DETAIL_ID_NOT_SET, "The VerifiedCredentialExternalTypeDetailId must be set" }, + { CompanyDataErrors.CREDENTIAL_ALREADY_EXISTING, "Credential request already existing" }, + { CompanyDataErrors.CREDENTIAL_TYPE_NOT_FOUND, "VerifiedCredentialType {verifiedCredentialType} does not exists" }, + { CompanyDataErrors.SSI_DETAILS_NOT_FOUND, "CompanySsiDetail {credentialId} does not exists" }, + { CompanyDataErrors.CREDENTIAL_NOT_PENDING, "Credential {credentialId} must be {status}" }, + { CompanyDataErrors.BPN_NOT_SET, "Bpn should be set for company" }, + { CompanyDataErrors.EXPIRY_DATE_NOT_SET, "Expiry date must always be set for use cases" }, + { CompanyDataErrors.EMPTY_VERSION, "External Detail Version must not be null" }, + { CompanyDataErrors.EMPTY_TEMPLATE, "Template must not be null" }, + { CompanyDataErrors.KIND_NOT_SUPPORTED, "{kind} is currently not supported" }, + { CompanyDataErrors.MULTIPLE_USE_CASES, "There must only be one use case" }, + { CompanyDataErrors.DID_NOT_SET, "Did must not be null" } + }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); + + public Type Type { get => typeof(CompanyDataErrors); } + public IReadOnlyDictionary MessageContainer { get => _messageContainer; } +} + +public enum CompanyDataErrors +{ + INVALID_COMPANY, + INVALID_COMPANY_STATUS, + USE_CASE_NOT_FOUND, + INVALID_LANGUAGECODE, + COMPANY_NOT_FOUND, + COMPANY_ROLE_IDS_CONSENT_STATUS_NULL, + MISSING_AGREEMENTS, + UNASSIGN_ALL_ROLES, + AGREEMENTS_NOT_ASSIGNED_WITH_ROLES, + MULTIPLE_SSI_DETAIL, + EXTERNAL_TYPE_DETAIL_NOT_FOUND, + EXPIRY_DATE_IN_PAST, + CREDENTIAL_NO_CERTIFICATE, + EXTERNAL_TYPE_DETAIL_ID_NOT_SET, + CREDENTIAL_ALREADY_EXISTING, + CREDENTIAL_TYPE_NOT_FOUND, + SSI_DETAILS_NOT_FOUND, + CREDENTIAL_NOT_PENDING, + BPN_NOT_SET, + EXPIRY_DATE_NOT_SET, + EMPTY_VERSION, + EMPTY_TEMPLATE, + KIND_NOT_SUPPORTED, + MULTIPLE_USE_CASES, + DID_NOT_SET +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Extensions/RouteHandlerBuilderExtensions.cs b/src/issuer/SsiCredentialIssuer.Service/Extensions/RouteHandlerBuilderExtensions.cs new file mode 100644 index 00000000..e3cdb32d --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Extensions/RouteHandlerBuilderExtensions.cs @@ -0,0 +1,48 @@ +/******************************************************************************** + * 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.ErrorHandling.Web; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Extensions; + +[ExcludeFromCodeCoverage] +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; + }); + + public static RouteHandlerBuilder WithDefaultResponses(this RouteHandlerBuilder builder) => + builder + .Produces(StatusCodes.Status401Unauthorized, typeof(ErrorResponse), contentType: "application/json") + .Produces(StatusCodes.Status500InternalServerError, typeof(ErrorResponse), "application/json"); +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimTypes.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimTypes.cs new file mode 100644 index 00000000..1dc65bea --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimTypes.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public static class ClaimTypes +{ + public const string PreferredUserName = "preferred_username"; + public const string Bpn = "bpn"; +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityDataBuilder.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityDataBuilder.cs new file mode 100644 index 00000000..8924ed10 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityDataBuilder.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 Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public class ClaimsIdentityDataBuilder : IClaimsIdentityDataBuilder +{ + private Guid? _identityId; + private string? _bpnl; + + public Guid IdentityId { get => _identityId ?? throw new UnexpectedConditionException("userId should never be null here (endpoint must be annotated with an identity policy)"); } + public string Bpnl { get => _bpnl ?? throw new UnexpectedConditionException("bpnl should never be null here (endpoint must be annotated with an identity policy)"); } + + public void AddIdentityId(Guid identityId) + { + _identityId = identityId; + } + + public void AddBpnl(string bpnl) + { + _bpnl = bpnl; + } + + public IClaimsIdentityDataBuilderStatus Status { get; set; } = IClaimsIdentityDataBuilderStatus.Initial; +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityService.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityService.cs new file mode 100644 index 00000000..615f4d01 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public class ClaimsIdentityService : IIdentityService, IIdentityIdService +{ + private readonly IIdentityData _identityData; + + public ClaimsIdentityService(IClaimsIdentityDataBuilder claimsIdentityDataBuilder) + { + _identityData = claimsIdentityDataBuilder; + } + + public IIdentityData IdentityData => _identityData; + + public Guid IdentityId => _identityData.IdentityId; +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityServiceCollectionExtensions.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityServiceCollectionExtensions.cs new file mode 100644 index 00000000..8ac59308 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/ClaimsIdentityServiceCollectionExtensions.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +[ExcludeFromCodeCoverage] +public static class ClaimsIdentityServiceCollectionExtensions +{ + public static IServiceCollection AddClaimsIdentityService(this IServiceCollection services) + { + return services + .AddScoped() + .AddTransient() + .AddTransient(); + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/IClaimsIdentityDataBuilder.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/IClaimsIdentityDataBuilder.cs new file mode 100644 index 00000000..fa683915 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/IClaimsIdentityDataBuilder.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public interface IClaimsIdentityDataBuilder : IIdentityData +{ + void AddIdentityId(Guid identityId); + void AddBpnl(string bpnl); + IClaimsIdentityDataBuilderStatus Status { get; set; } +} + +public enum IClaimsIdentityDataBuilderStatus +{ + Initial, + Initialized, + Complete, + Empty +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityData.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityData.cs new file mode 100644 index 00000000..70184a6c --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public interface IIdentityData +{ + Guid IdentityId { get; } + string Bpnl { get; } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityService.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityService.cs new file mode 100644 index 00000000..6ff8f0cd --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/IIdentityService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public interface IIdentityService +{ + IIdentityData IdentityData { get; } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/IdentityIdService.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/IdentityIdService.cs new file mode 100644 index 00000000..ebfa998e --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/IdentityIdService.cs @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public class IdentityIdService : IIdentityIdService +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public IdentityIdService(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Guid IdentityId => GetIdentityId(); + + private Guid GetIdentityId() + { + var preferredUserName = _httpContextAccessor?.HttpContext?.User.Claims.SingleOrDefault(x => x.Type == ClaimTypes.PreferredUserName)?.Value ?? + throw new UnexpectedConditionException("Username must be set here"); + if (Guid.TryParse(preferredUserName, out var identityId)) + { + return identityId; + } + + throw new UnexpectedConditionException("Username must be a uuid"); + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs new file mode 100644 index 00000000..8d8ccca6 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/MandatoryIdentityClaimHandler.cs @@ -0,0 +1,99 @@ +/******************************************************************************** + * 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.AspNetCore.Authorization; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using System.Security.Claims; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public class MandatoryIdentityClaimRequirement : IAuthorizationRequirement +{ + public MandatoryIdentityClaimRequirement(PolicyTypeId policyTypeId) + { + PolicyTypeId = policyTypeId; + } + + public PolicyTypeId PolicyTypeId { get; } +} + +public class MandatoryIdentityClaimHandler : AuthorizationHandler +{ + private readonly IClaimsIdentityDataBuilder _identityDataBuilder; + private readonly ILogger _logger; + + public MandatoryIdentityClaimHandler(IClaimsIdentityDataBuilder claimsIdentityDataBuilder, ILogger logger) + { + _identityDataBuilder = claimsIdentityDataBuilder; + _logger = logger; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MandatoryIdentityClaimRequirement requirement) + { + if (_identityDataBuilder.Status == IClaimsIdentityDataBuilderStatus.Initial) + { + InitializeClaims(context.User); + } + + if (_identityDataBuilder.Status == IClaimsIdentityDataBuilderStatus.Empty) + { + context.Fail(); + return Task.CompletedTask; + } + + if (requirement.PolicyTypeId switch + { + PolicyTypeId.ValidIdentity => _identityDataBuilder.IdentityId != Guid.Empty, + PolicyTypeId.ValidBpn => !string.IsNullOrWhiteSpace(_identityDataBuilder.Bpnl), + _ => throw new UnexpectedConditionException($"unexpected PolicyTypeId {requirement.PolicyTypeId}") + }) + { + context.Succeed(requirement); + } + else + { + context.Fail(); + } + + return Task.CompletedTask; + } + + private void InitializeClaims(ClaimsPrincipal principal) + { + var preferredUserName = principal.Claims.SingleOrDefault(x => x.Type == ClaimTypes.PreferredUserName)?.Value; + if (!Guid.TryParse(preferredUserName, out var identityId)) + { + _logger.LogInformation("Preferred user name {PreferredUserName} couldn't be parsed to uuid", preferredUserName); + _identityDataBuilder.Status = IClaimsIdentityDataBuilderStatus.Empty; + return; + } + + var bpnl = principal.Claims.SingleOrDefault(x => x.Type == ClaimTypes.Bpn)?.Value; + if (string.IsNullOrWhiteSpace(bpnl)) + { + _logger.LogInformation("Bpn must be set for user {PreferredUserName}", preferredUserName); + _identityDataBuilder.Status = IClaimsIdentityDataBuilderStatus.Empty; + return; + } + + _identityDataBuilder.AddIdentityId(identityId); + _identityDataBuilder.AddBpnl(bpnl); + _identityDataBuilder.Status = IClaimsIdentityDataBuilderStatus.Complete; + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Identity/PolicyTypes.cs b/src/issuer/SsiCredentialIssuer.Service/Identity/PolicyTypes.cs new file mode 100644 index 00000000..14d7dcad --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Identity/PolicyTypes.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +public static class PolicyTypes +{ + public const string ValidIdentity = "ValidIdentity"; + public const string ValidBpn = "ValidBpn"; +} + +public enum PolicyTypeId +{ + ValidIdentity, + ValidBpn +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/Constants.cs b/src/issuer/SsiCredentialIssuer.Service/Models/Constants.cs new file mode 100644 index 00000000..8779280b --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public static class Constants +{ + public const string JsonContentType = "application/json"; +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/CreateBpnCredentialRequest.cs b/src/issuer/SsiCredentialIssuer.Service/Models/CreateBpnCredentialRequest.cs new file mode 100644 index 00000000..63b625a6 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Models/CreateBpnCredentialRequest.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 Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public record CreateBpnCredentialRequest( + [property: JsonPropertyName("holder")] string Holder, + [property: JsonPropertyName("businessPartnerNumber")] string BusinessPartnerNumber, + [property: JsonPropertyName("technicalUserDetails")] TechnicalUserDetails? TechnicalUserDetails +); + +public record CreateMembershipCredentialRequest( + [property: JsonPropertyName("holder")] string Holder, + [property: JsonPropertyName("businessPartnerNumber")] string HolderBpn, + [property: JsonPropertyName("memberOf")] string MemberOf, + [property: JsonPropertyName("technicalUserDetails")] TechnicalUserDetails? TechnicalUserDetails +); + +public record CreateFrameworkCredentialRequest( + [property: JsonPropertyName("holder")] string Holder, + [property: JsonPropertyName("businessPartnerNumber")] string HolderBpn, + [property: JsonPropertyName("useCaseFrameworkId")] VerifiedCredentialTypeId UseCaseFrameworkId, + [property: JsonPropertyName("useCaseFrameworkVersionId")] Guid UseCaseFrameworkVersionId, + [property: JsonPropertyName("technicalUserDetails")] TechnicalUserDetails? TechnicalUserDetails +); diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/CredentialData.cs b/src/issuer/SsiCredentialIssuer.Service/Models/CredentialData.cs new file mode 100644 index 00000000..fba1f917 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Models/CredentialData.cs @@ -0,0 +1,76 @@ +/******************************************************************************** + * 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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public record FrameworkCredential( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("@context")] IEnumerable Context, + [property: JsonPropertyName("type")] IEnumerable Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("issuanceDate")] DateTimeOffset IssuanceDate, + [property: JsonPropertyName("expirationDate")] DateTimeOffset ExpirationDate, + [property: JsonPropertyName("issuer")] string Issuer, + [property: JsonPropertyName("credentialSubject")] FrameworkCredentialSubject CredentialSubject); + +public record FrameworkCredentialSubject( + [property: JsonPropertyName("id")] string Did, + [property: JsonPropertyName("holderIndentifier")] string HolderIndentifier, + [property: JsonPropertyName("group")] string Group, + [property: JsonPropertyName("useCase")] string UseCase, + [property: JsonPropertyName("contractTemplate")] string ContractTemplate, + [property: JsonPropertyName("contractVersion")] string ContractVersion +); + +public record MembershipCredential( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("@context")] IEnumerable Context, + [property: JsonPropertyName("type")] IEnumerable Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("issuanceDate")] DateTimeOffset IssuanceDate, + [property: JsonPropertyName("expirationDate")] DateTimeOffset ExpirationDate, + [property: JsonPropertyName("issuer")] string Issuer, + [property: JsonPropertyName("credentialSubject")] MembershipCredentialSubject CredentialSubject); + +public record MembershipCredentialSubject( + [property: JsonPropertyName("id")] string Did, + [property: JsonPropertyName("holderIndentifier")] string HolderIndentifier, + [property: JsonPropertyName("memberOf")] string MemberOf +); + +public record BpnCredential( + [property: JsonPropertyName("id")] Guid Id, + [property: JsonPropertyName("@context")] IEnumerable Context, + [property: JsonPropertyName("type")] IEnumerable Type, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("issuanceDate")] DateTimeOffset IssuanceDate, + [property: JsonPropertyName("expirationDate")] DateTimeOffset ExpirationDate, + [property: JsonPropertyName("issuer")] string Issuer, + [property: JsonPropertyName("credentialSubject")] BpnCredentialSubject CredentialSubject); + +public record BpnCredentialSubject( + [property: JsonPropertyName("id")] string Did, + [property: JsonPropertyName("holderIndentifier")] string HolderIndentifier, + [property: JsonPropertyName("bpn")] string Bpn +); diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/DidDocument.cs b/src/issuer/SsiCredentialIssuer.Service/Models/DidDocument.cs new file mode 100644 index 00000000..de60516a --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Models/DidDocument.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 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public record DidDocument( + [property: JsonPropertyName("id")] string Id +); diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationCreationData.cs b/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationCreationData.cs new file mode 100644 index 00000000..085902b6 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationCreationData.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public record UseCaseParticipationCreationData +( + Guid VerifiedCredentialExternalTypeDetailId, + VerifiedCredentialTypeId CredentialType, + IFormFile Document +); + +public record SsiCertificateCreationData +( + Guid? VerifiedCredentialExternalTypeDetailId, + VerifiedCredentialTypeId CredentialType, + IFormFile Document +); diff --git a/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationData.cs b/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationData.cs new file mode 100644 index 00000000..e50c222a --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Models/UseCaseParticipationData.cs @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; + +public record UseCaseParticipationData +( + string? UseCase, + string? Description, + VerifiedCredentialTypeId CredentialType, + IEnumerable VerifiedCredentials +); + +public record CertificateParticipationData +( + VerifiedCredentialTypeId CredentialType, + IEnumerable VerifiedCredentials +); + +public record CompanySsiExternalTypeDetailData +( + ExternalTypeDetailData ExternalDetailData, + CompanySsiDetailData? SsiDetailData +); + +public record CompanySsiDetailData +( + Guid CredentialId, + CompanySsiDetailStatusId ParticipationStatus, + DateTimeOffset? ExpiryDate, + IEnumerable Documents +); diff --git a/src/issuer/SsiCredentialIssuer.Service/Program.cs b/src/issuer/SsiCredentialIssuer.Service/Program.cs new file mode 100644 index 00000000..3b2e3380 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Program.cs @@ -0,0 +1,64 @@ +/******************************************************************************** + * 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.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Service; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Authentication; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Controllers; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using System.Text.Json.Serialization; + +const string VERSION = "v1"; + +WebApplicationBuildRunner + .BuildAndRunWebApplication(args, "issuer", VERSION, ".Issuer", builder => + { + builder.Services + .AddTransient() + .AddTransient() + .AddTransient() + .AddClaimsIdentityService() + .AddEndpointsApiExplorer() + .AddIssuerRepositories(builder.Configuration) + .ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }) + .Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }) + .AddCredentialService(builder.Configuration.GetSection("Credential")) + .AddPortalService(builder.Configuration.GetSection("Portal")) + .AddSingleton() + .AddSingleton(); + }, + (app, _) => + { + app.MapGroup("/api") + .WithOpenApi() + .MapCompanyDataApi(); + }); diff --git a/src/issuer/SsiCredentialIssuer.Service/Properties/launchSettings.json b/src/issuer/SsiCredentialIssuer.Service/Properties/launchSettings.json new file mode 100644 index 00000000..6d4252b8 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/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/issuer/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000" + } + }, + "SsiCredentialIssuer.Service": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "api/issuer/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "Cors__AllowedOrigins__0": "http://localhost:3000", + "Cors__AllowedOrigins__1": "https://portal.example.org" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + } + } +} \ No newline at end of file diff --git a/src/issuer/SsiCredentialIssuer.Service/SsiCredentialIssuer.Service.csproj b/src/issuer/SsiCredentialIssuer.Service/SsiCredentialIssuer.Service.csproj new file mode 100644 index 00000000..10f89f32 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/SsiCredentialIssuer.Service.csproj @@ -0,0 +1,73 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Service + Org.Eclipse.TractusX.SsiCredentialIssuer.Service + net7.0 + enable + enable + 1c25fec6-9663-495e-9c65-3212002d71ab + Linux + ..\..\.. + True + CS1591 + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + Program.cs + + + + + + + + + + + diff --git a/src/issuer/SsiCredentialIssuer.Service/appsettings.json b/src/issuer/SsiCredentialIssuer.Service/appsettings.json new file mode 100644 index 00000000..477b3766 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/appsettings.json @@ -0,0 +1,69 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Information", + "Org.Eclipse.TractusX.SsiCredentialIssuer.Service": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "WithCorrelationId" + ], + "Properties": { + "Application": "SsiCredentialIssuer" + } + }, + "SwaggerEnabled": "", + "HealthChecks": [], + "Cors": { + "AllowedOrigins": [] + }, + "ConnectionStrings": { + "IssuerDb": "" + }, + "JwtBearerOptions": { + "RequireHttpsMetadata": true, + "MetadataAddress": "", + "SaveToken": true, + "TokenValidationParameters": { + "ValidateIssuer": true, + "ValidIssuer": "", + "ValidateIssuerSigningKey": true, + "ValidAudience": "", + "ValidateAudience": true, + "ValidateLifetime": true, + "ClockSkew": 600000 + } + }, + "Portal": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "KeycloakTokenAddress": "", + "BaseAddress": "" + }, + "Credential": { + "IssuerDid": "", + "MaxPageSize": 15, + "EncrptionConfigIndex": 0, + "EncryptionConfigs": [ + { + "Index": 0, + "EncryptionKey": "", + "CipherMode": "", + "PaddingMode": "" + } + ] + } +} diff --git a/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj b/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj new file mode 100644 index 00000000..2be4743c --- /dev/null +++ b/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj @@ -0,0 +1,35 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library + net7.0 + enable + enable + + + + + + + + diff --git a/src/processes/CredentialProcess.Library/CredentialProcessHandler.cs b/src/processes/CredentialProcess.Library/CredentialProcessHandler.cs new file mode 100644 index 00000000..528b70e8 --- /dev/null +++ b/src/processes/CredentialProcess.Library/CredentialProcessHandler.cs @@ -0,0 +1,116 @@ +/******************************************************************************** + * 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.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; + +public class CredentialProcessHandler : ICredentialProcessHandler +{ + private readonly IIssuerRepositories _issuerRepositories; + private readonly IWalletBusinessLogic _walletBusinessLogic; + + public CredentialProcessHandler(IIssuerRepositories issuerRepositories, IWalletBusinessLogic walletBusinessLogic) + { + _issuerRepositories = issuerRepositories; + _walletBusinessLogic = walletBusinessLogic; + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCredential(Guid credentialId, CancellationToken cancellationToken) + { + var data = await _issuerRepositories.GetInstance().GetCredentialStorageInformationById(credentialId).ConfigureAwait(false); + await _walletBusinessLogic.CreateCredential(credentialId, data.Schema, cancellationToken).ConfigureAwait(false); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SIGN_CREDENTIAL, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SignCredential(Guid credentialId, CancellationToken cancellationToken) + { + var externalCredentialId = await _issuerRepositories.GetInstance().GetWalletCredentialId(credentialId).ConfigureAwait(false); + if (externalCredentialId is null) + { + throw new ConflictException("ExternalCredentialId must be set here"); + } + + await _walletBusinessLogic.SignCredential(credentialId, externalCredentialId!.Value, cancellationToken).ConfigureAwait(false); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SaveCredentialDocument(Guid credentialId, CancellationToken cancellationToken) + { + var (externalCredentialId, kindId) = await _issuerRepositories.GetInstance().GetExternalCredentialAndKindId(credentialId).ConfigureAwait(false); + if (externalCredentialId == null) + { + throw new ConflictException("ExternalCredentialId must be set here"); + } + + await _walletBusinessLogic.GetCredential(credentialId, externalCredentialId.Value, kindId, cancellationToken).ConfigureAwait(false); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCredentialForHolder(Guid credentialId, CancellationToken cancellationToken) + { + var (holderWalletData, credential, encryptionInformation) = await _issuerRepositories.GetInstance().GetCredentialData(credentialId).ConfigureAwait(false); + if (credential is null) + { + throw new ConflictException("Credential must be set here"); + } + + if (holderWalletData.ClientId == null || holderWalletData.WalletUrl == null) + { + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.SKIPPED, + false, + null); + } + + if (encryptionInformation.Secret == null || encryptionInformation.InitializationVector == null || encryptionInformation.EncryptionMode == null) + { + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.SKIPPED, + false, + null); + } + + await _walletBusinessLogic.CreateCredentialForHolder(credentialId, holderWalletData.WalletUrl, holderWalletData.ClientId, new EncryptionInformation(encryptionInformation.Secret!, encryptionInformation.InitializationVector, encryptionInformation.EncryptionMode.Value), credential, cancellationToken).ConfigureAwait(false); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.DONE, + false, + null); + } +} diff --git a/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs b/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs new file mode 100644 index 00000000..d4ba5268 --- /dev/null +++ b/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.DependencyInjection; + +[ExcludeFromCodeCoverage] +public static class CredentialHandlerExtensions +{ + public static IServiceCollection AddCredentialProcessHandler(this IServiceCollection services, IConfiguration config) + { + services + .AddTransient() + .AddWalletService(config); + + return services; + } +} diff --git a/src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs b/src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs new file mode 100644 index 00000000..ba239d92 --- /dev/null +++ b/src/processes/CredentialProcess.Library/ICredentialProcessHandler.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; + +public interface ICredentialProcessHandler +{ + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCredential(Guid credentialId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SignCredential(Guid credentialId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> SaveCredentialDocument(Guid credentialId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> CreateCredentialForHolder(Guid credentialId, CancellationToken cancellationToken); +} diff --git a/src/processes/CredentialProcess.Worker/CredentialProcess.Worker.csproj b/src/processes/CredentialProcess.Worker/CredentialProcess.Worker.csproj new file mode 100644 index 00000000..a45ba693 --- /dev/null +++ b/src/processes/CredentialProcess.Worker/CredentialProcess.Worker.csproj @@ -0,0 +1,35 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker + net7.0 + enable + enable + + + + + + + + diff --git a/src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs b/src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs new file mode 100644 index 00000000..44a1598e --- /dev/null +++ b/src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs @@ -0,0 +1,112 @@ +/******************************************************************************** + * 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.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker; + +public class CredentialProcessTypeExecutor : IProcessTypeExecutor +{ + private readonly IIssuerRepositories _issuerRepositories; + private readonly ICredentialProcessHandler _credentialProcessHandler; + + private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( + ProcessStepTypeId.CREATE_CREDENTIAL, + ProcessStepTypeId.SIGN_CREDENTIAL, + ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT, + ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER); + + private Guid _credentialId; + + public CredentialProcessTypeExecutor( + IIssuerRepositories issuerRepositories, + ICredentialProcessHandler credentialProcessHandler) + { + _issuerRepositories = issuerRepositories; + _credentialProcessHandler = credentialProcessHandler; + } + + public ProcessTypeId GetProcessTypeId() => ProcessTypeId.CREATE_CREDENTIAL; + 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, credentialId) = await _issuerRepositories.GetInstance().GetDataForProcessId(processId).ConfigureAwait(false); + if (!exists) + { + throw new NotFoundException($"process {processId} does not exist or is not associated with an credential"); + } + + _credentialId = credentialId; + return new IProcessTypeExecutor.InitializationResult(false, null); + } + + public async ValueTask ExecuteProcessStep(ProcessStepTypeId processStepTypeId, IEnumerable processStepTypeIds, CancellationToken cancellationToken) + { + if (_credentialId == Guid.Empty) + { + throw new UnexpectedConditionException("credentialId should never be empty here"); + } + + IEnumerable? nextStepTypeIds; + ProcessStepStatusId stepStatusId; + bool modified; + string? processMessage; + + try + { + (nextStepTypeIds, stepStatusId, modified, processMessage) = processStepTypeId switch + { + ProcessStepTypeId.CREATE_CREDENTIAL => await _credentialProcessHandler.CreateCredential(_credentialId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SIGN_CREDENTIAL => await _credentialProcessHandler.SignCredential(_credentialId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT => await _credentialProcessHandler.SaveCredentialDocument(_credentialId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER => await _credentialProcessHandler.CreateCredentialForHolder(_credentialId, cancellationToken) + .ConfigureAwait(false), + _ => (null, ProcessStepStatusId.TODO, false, null) + }; + } + catch (Exception ex) when (ex is not SystemException) + { + (stepStatusId, processMessage, nextStepTypeIds) = ProcessError(ex); + modified = true; + } + + return new IProcessTypeExecutor.StepExecutionResult(modified, stepStatusId, nextStepTypeIds, null, processMessage); + } + + private static (ProcessStepStatusId StatusId, string? ProcessMessage, IEnumerable? nextSteps) ProcessError(Exception ex) + { + return ex switch + { + ServiceException { IsRecoverable: true } => (ProcessStepStatusId.TODO, ex.Message, null), + _ => (ProcessStepStatusId.FAILED, ex.Message, null) + }; + } +} diff --git a/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs b/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs new file mode 100644 index 00000000..6063efa1 --- /dev/null +++ b/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; +using System.Diagnostics.CodeAnalysis; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.DependencyInjection; + +[ExcludeFromCodeCoverage] +public static class CredentialProcessCollectionExtensions +{ + public static IServiceCollection AddCredentialProcessExecutor(this IServiceCollection services, IConfiguration config) => + services + .AddTransient() + .AddCredentialProcessHandler(config); +} diff --git a/src/processes/Processes.Library/ManualProcessStepData.cs b/src/processes/Processes.Library/ManualProcessStepData.cs new file mode 100644 index 00000000..dccd234e --- /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 Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Library; + +public record ManualProcessStepData( + ProcessStepTypeId ProcessStepTypeId, + Process Process, + IEnumerable ProcessSteps, + IIssuerRepositories PortalRepositories +); diff --git a/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs new file mode 100644 index 00000000..02e347e9 --- /dev/null +++ b/src/processes/Processes.Library/ManualProcessStepDataExtensions.cs @@ -0,0 +1,131 @@ +/******************************************************************************** + * 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.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Library; + +public static class VerifyProcessDataExtensions +{ + public static ManualProcessStepData CreateManualProcessData( + this VerifyProcessData? processData, + ProcessStepTypeId processStepTypeId, + IIssuerRepositories 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 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 00000000..56ca7374 --- /dev/null +++ b/src/processes/Processes.Library/Processes.Library.csproj @@ -0,0 +1,40 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Library + Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..83f40a52 --- /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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..cb0f5240 --- /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 Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..a5edd1f0 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionService.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 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; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using System.Runtime.CompilerServices; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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 TimeSpan _lockExpiryTime; + 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; + _lockExpiryTime = new TimeSpan(options.Value.LockExpirySeconds * 10000000L); + _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 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); + await foreach (var hasChanged in ExecuteProcess(processExecutor, process, stoppingToken).ConfigureAwait(false)) + { + if (hasChanged) + { + 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); + } + } + + private async IAsyncEnumerable ExecuteProcess(IProcessExecutor processExecutor, Process process, [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var executionResult in processExecutor.ExecuteProcess(process.Id, process.ProcessTypeId, cancellationToken).ConfigureAwait(false)) + { + yield return executionResult switch + { + IProcessExecutor.ProcessExecutionResult.LockRequested => EnsureLock(process), + IProcessExecutor.ProcessExecutionResult.SaveRequested => UpdateVersion(process), + _ => false + }; + } + } + + private bool EnsureLock(ILockableEntity entity) + { + if (entity.IsLocked()) + { + return false; + } + + if (!entity.TryLock(_dateTimeProvider.OffsetNow.Add(_lockExpiryTime))) + { + throw new UnexpectedConditionException("process TryLock should never fail here"); + } + + return true; + } + + private static bool UpdateVersion(ILockableEntity entity) + { + if (!entity.IsLocked()) + { + entity.UpdateVersion(); + } + + return true; + } +} diff --git a/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.cs new file mode 100644 index 00000000..aead73a2 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceExtensions.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 Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; + +public static class ProcessExecutionServiceExtensions +{ + public static IServiceCollection AddProcessExecutionService(this IServiceCollection services, IConfigurationSection section) + { + services.AddOptions().Bind(section); + services + .AddTransient() + .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 00000000..ae00657c --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessExecutionServiceSettings.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 System.ComponentModel.DataAnnotations; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; + +public class ProcessExecutionServiceSettings +{ + [Required] + public int LockExpirySeconds { get; set; } + + [Required] + public Guid IdentityId { 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 00000000..1a45d729 --- /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 Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Async; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; + +public class ProcessExecutor : IProcessExecutor +{ + private readonly ImmutableDictionary _executors; + private readonly IProcessStepRepository _processStepRepository; + private readonly ILogger _logger; + + public ProcessExecutor(IEnumerable executors, IIssuerRepositories 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/ProcessIdentityIdService.cs b/src/processes/Processes.Worker.Library/ProcessIdentityIdService.cs new file mode 100644 index 00000000..f553f834 --- /dev/null +++ b/src/processes/Processes.Worker.Library/ProcessIdentityIdService.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; + +public class ProcessIdentityIdService : IIdentityIdService +{ + private readonly ProcessExecutionServiceSettings _settings; + + public ProcessIdentityIdService(IOptions options) + { + _settings = options.Value; + } + + public Guid IdentityId => _settings.IdentityId; +} 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 00000000..d2867026 --- /dev/null +++ b/src/processes/Processes.Worker.Library/Processes.Worker.Library.csproj @@ -0,0 +1,45 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library + Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..a6d17ff1 --- /dev/null +++ b/src/processes/Processes.Worker/Processes.Worker.csproj @@ -0,0 +1,55 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker + net7.0 + enable + enable + Exe + Linux + ..\..\.. + True + eea9b02b-62ae-457d-91b9-b6717b99dd93 + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs new file mode 100644 index 00000000..35ece519 --- /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 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.SsiCredentialIssuer.CredentialProcess.Worker.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; +using Serilog; + +LoggingExtensions.EnsureInitialized(); +Log.Information("Building worker"); +try +{ + var host = Host + .CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddTransient() + .AddIssuerRepositories(hostContext.Configuration) + .AddProcessExecutionService(hostContext.Configuration.GetSection("Processes")) + .AddPortalService(hostContext.Configuration.GetSection("Portal")) + .AddCredentialProcessExecutor(hostContext.Configuration); + }) + .AddLogging() + .Build(); + Log.Information("Building worker completed"); + + using 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 00000000..5de5fb55 --- /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 00000000..50b53b47 --- /dev/null +++ b/src/processes/Processes.Worker/appsettings.json @@ -0,0 +1,59 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "System": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "Org.Eclipse.TractusX.SsiCredentialIssuer": "Information" + } + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "FromLogContext" + ], + "Properties": { + "Application": "Dim.Process.Worker" + } + }, + "ConnectionStrings": { + "IssuerDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=placeholder;Ssl Mode=Disable;" + }, + "Processes": { + "LockExpirySeconds": 3600, + "IdentityId": "" + }, + "Portal": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "TokenAddress": "", + "BaseAddress": "" + }, + "Wallet": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "TokenAddress": "", + "BaseAddress": "", + "EncrptionConfigIndex": 0, + "EncryptionConfigs": [ + { + "Index": 0, + "EncryptionKey": "", + "CipherMode": "", + "PaddingMode": "" + } + ] + } +} diff --git a/src/settings-coverage.xml b/src/settings-coverage.xml new file mode 100644 index 00000000..48a14895 --- /dev/null +++ b/src/settings-coverage.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Tests.Shared/Extensions/HttpExtensions.cs b/tests/Tests.Shared/Extensions/HttpExtensions.cs new file mode 100644 index 00000000..56a77d38 --- /dev/null +++ b/tests/Tests.Shared/Extensions/HttpExtensions.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.Net.Http.Headers; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared.Extensions; + +public static class HttpExtensions +{ + public static async Task GetResultFromContent(this HttpResponseMessage response) + { + using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var options = new JsonSerializerOptions + { + Converters = { new JsonStringEnumConverter(allowIntegerValues: false) } + }; + return await JsonSerializer.DeserializeAsync(responseStream, options).ConfigureAwait(false) ?? throw new InvalidOperationException(); + } + + public static HttpContent ToJsonContent(this object data, JsonSerializerOptions options, string contentType) + { + var json = JsonSerializer.Serialize(data, options); + HttpContent content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + return content; + } + + public static HttpContent ToFormContent(this string stringContent, string contentType) + { + HttpContent content = new StringContent(stringContent); + content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + return content; + } +} diff --git a/tests/Tests.Shared/FakeIdentity.cs b/tests/Tests.Shared/FakeIdentity.cs new file mode 100644 index 00000000..32df8a14 --- /dev/null +++ b/tests/Tests.Shared/FakeIdentity.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; + +public class FakeIdentity : IIdentityData +{ + public Guid IdentityId => new("ac1cf001-7fbc-1f2f-817f-bce058020001"); + public string Bpnl => "BPNL00000003AYRE"; +} diff --git a/tests/Tests.Shared/FakeIdentityIdService.cs b/tests/Tests.Shared/FakeIdentityIdService.cs new file mode 100644 index 00000000..75a45577 --- /dev/null +++ b/tests/Tests.Shared/FakeIdentityIdService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; + +public class FakeIdentityIdService : IIdentityIdService +{ + private readonly Guid _identityId = new("ac1cf001-7fbc-1f2f-817f-bce058020001"); + + public Guid IdentityId => _identityId; +} diff --git a/tests/Tests.Shared/FakeIdentityService.cs b/tests/Tests.Shared/FakeIdentityService.cs new file mode 100644 index 00000000..cc887a6c --- /dev/null +++ b/tests/Tests.Shared/FakeIdentityService.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 Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; + +public class FakeIdentityService : IIdentityService +{ + public Guid IdentityId => IdentityData.IdentityId; + public IIdentityData IdentityData { get; } = new FakeIdentity(); +} diff --git a/tests/Tests.Shared/HttpMessageHandlerMock.cs b/tests/Tests.Shared/HttpMessageHandlerMock.cs new file mode 100644 index 00000000..d6ab3c78 --- /dev/null +++ b/tests/Tests.Shared/HttpMessageHandlerMock.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 + ********************************************************************************/ + +using System.Net; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; + +public class HttpMessageHandlerMock : HttpMessageHandler +{ + private readonly HttpStatusCode _statusCode; + private readonly Exception? _ex; + private readonly HttpContent? _httpContent; + private readonly bool _isRequestUri; + + public HttpMessageHandlerMock(HttpStatusCode statusCode, HttpContent? httpContent = null, Exception? ex = null, bool IsRequestUri = false) + { + _statusCode = statusCode; + _httpContent = httpContent; + _ex = ex; + _isRequestUri = IsRequestUri; + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + RequestMessage = request; + + if (_ex != null) + { + throw _ex; + } + + var httpResponseMessage = new HttpResponseMessage(_statusCode); + httpResponseMessage.RequestMessage = _isRequestUri ? request : null; + if (_httpContent != null) + { + httpResponseMessage.Content = _httpContent; + } + + return Task.FromResult(httpResponseMessage); + } + + public HttpRequestMessage? RequestMessage { get; private set; } = null; +} diff --git a/tests/Tests.Shared/NoAuditHandler.cs b/tests/Tests.Shared/NoAuditHandler.cs new file mode 100644 index 00000000..eeb3d1a4 --- /dev/null +++ b/tests/Tests.Shared/NoAuditHandler.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 Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; + +public class NoAuditHandler : IAuditHandler +{ +#pragma warning disable CA1822 + public void HandleAuditForChangedEntries(IEnumerable changedEntries, DbContext context) +#pragma warning restore CA1822 + { + } +} diff --git a/tests/Tests.Shared/Tests.Shared.csproj b/tests/Tests.Shared/Tests.Shared.csproj new file mode 100644 index 00000000..33d05345 --- /dev/null +++ b/tests/Tests.Shared/Tests.Shared.csproj @@ -0,0 +1,33 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared + Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared + net7.0 + enable + enable + + + + + + + diff --git a/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs new file mode 100644 index 00000000..705a7298 --- /dev/null +++ b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs @@ -0,0 +1,193 @@ +/******************************************************************************** + * Copyright (c) 2021, 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 FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.Tests; + +public class ExpiryCheckServiceTests +{ + private readonly IFixture _fixture; + private readonly ExpiryCheckService _sut; + + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IIssuerRepositories _issuerRepositories; + private readonly IPortalService _portalService; + private readonly ICompanySsiDetailsRepository _companySsiDetailsRepository; + private readonly ExpiryCheckServiceSettings _settings; + + private readonly string Bpnl = "BPNL00000001TEST"; + + public ExpiryCheckServiceTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _issuerRepositories = A.Fake(); + _companySsiDetailsRepository = A.Fake(); + + A.CallTo(() => _issuerRepositories.GetInstance()) + .Returns(_companySsiDetailsRepository); + + _dateTimeProvider = A.Fake(); + _portalService = A.Fake(); + + var serviceProvider = _fixture.Create(); + A.CallTo(() => serviceProvider.GetService(typeof(IIssuerRepositories))).Returns(_issuerRepositories); + A.CallTo(() => serviceProvider.GetService(typeof(IDateTimeProvider))).Returns(_dateTimeProvider); + A.CallTo(() => serviceProvider.GetService(typeof(IPortalService))).Returns(_portalService); + var serviceScope = _fixture.Create(); + A.CallTo(() => serviceScope.ServiceProvider).Returns(serviceProvider); + var serviceScopeFactory = _fixture.Create(); + A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(serviceScope); + + _settings = new ExpiryCheckServiceSettings + { + ExpiredVcsToDeleteInMonth = 12, + InactiveVcsToDeleteInWeeks = 8 + }; + _sut = new ExpiryCheckService(serviceScopeFactory, _fixture.Create>(), Options.Create(_settings)); + } + + [Fact] + public async Task ExecuteAsync_WithInactiveAndEligibleForDeletion_RemovesEntry() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var inactiveVcsToDelete = now.AddDays(-(_settings.InactiveVcsToDeleteInWeeks * 7)); + var credentialId = Guid.NewGuid(); + var credentialScheduleData = _fixture.Build() + .With(x => x.IsVcToDelete, true) + .Create(); + var data = new CredentialExpiryData[] + { + new(credentialId, inactiveVcsToDelete.AddDays(-1), null, null, Bpnl, CompanySsiDetailStatusId.INACTIVE, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, credentialScheduleData) + }; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetExpiryData(A._, A._, A._)) + .Returns(data.ToAsyncEnumerable()); + + // Act + await _sut.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailsRepository.RemoveSsiDetail(credentialId)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task ExecuteAsync_WithPendingAndExpiryBeforeNow_DeclinesRequest() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var expiredVcsToDeleteInMonth = now.AddMonths(-_settings.ExpiredVcsToDeleteInMonth); + var ssiDetail = new CompanySsiDetail(Guid.NewGuid(), Bpnl, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, CompanySsiDetailStatusId.PENDING, Guid.NewGuid(), now) + { + ExpiryDate = expiredVcsToDeleteInMonth.AddDays(-2) + }; + var credentialScheduleData = _fixture.Build() + .With(x => x.IsVcToDecline, true) + .Create(); + var data = new CredentialExpiryData[] + { + new(ssiDetail.Id, ssiDetail.ExpiryDate!.Value, ssiDetail.ExpiryCheckTypeId, null, Bpnl, ssiDetail.CompanySsiDetailStatusId, ssiDetail.VerifiedCredentialTypeId, credentialScheduleData) + }; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetExpiryData(A._, A._, A._)) + .Returns(data.ToAsyncEnumerable()); + A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(A._, + A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(ssiDetail); + updateFields.Invoke(ssiDetail); + }); + + // Act + await _sut.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailsRepository.RemoveSsiDetail(ssiDetail.Id)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.AddNotification(A._, Bpnl, NotificationTypeId.CREDENTIAL_REJECTED, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.TriggerMail("CredentialRejected", Bpnl, A>._, A._)).MustHaveHappenedOnceExactly(); + + ssiDetail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.INACTIVE); + } + + [Theory] + [InlineData(1, ExpiryCheckTypeId.ONE_DAY, ExpiryCheckTypeId.TWO_WEEKS)] + [InlineData(13, ExpiryCheckTypeId.TWO_WEEKS, ExpiryCheckTypeId.ONE_MONTH)] + [InlineData(27, ExpiryCheckTypeId.ONE_MONTH, null)] + public async Task ExecuteAsync_WithActiveCloseToExpiry_NotifiesCreator(int days, ExpiryCheckTypeId expiryCheckTypeId, ExpiryCheckTypeId? currentExpiryCheckTypeId) + { + // Arrange + var now = DateTimeOffset.UtcNow; + var ssiDetail = new CompanySsiDetail(Guid.NewGuid(), Bpnl, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, CompanySsiDetailStatusId.ACTIVE, Guid.NewGuid(), now) + { + ExpiryDate = now.AddDays(-days), + ExpiryCheckTypeId = currentExpiryCheckTypeId + }; + var credentialScheduleData = _fixture.Build() + .With(x => x.IsVcToDecline, false) + .With(x => x.IsOneDayNotification, expiryCheckTypeId == ExpiryCheckTypeId.ONE_DAY) + .With(x => x.IsTwoWeeksNotification, expiryCheckTypeId == ExpiryCheckTypeId.TWO_WEEKS) + .With(x => x.IsOneMonthNotification, expiryCheckTypeId == ExpiryCheckTypeId.ONE_MONTH) + .Create(); + var data = new CredentialExpiryData[] + { + new(ssiDetail.Id, ssiDetail.ExpiryDate!.Value, ssiDetail.ExpiryCheckTypeId, null, Bpnl, ssiDetail.CompanySsiDetailStatusId, ssiDetail.VerifiedCredentialTypeId, credentialScheduleData) + }; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetExpiryData(A._, A._, A._)) + .Returns(data.ToAsyncEnumerable()); + A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(A._, + A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(ssiDetail); + updateFields.Invoke(ssiDetail); + }); + + // Act + await _sut.ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailsRepository.RemoveSsiDetail(ssiDetail.Id)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.AddNotification(A._, Bpnl, NotificationTypeId.CREDENTIAL_EXPIRY, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.TriggerMail("CredentialExpiry", Bpnl, A>._, A._)).MustHaveHappenedOnceExactly(); + + ssiDetail.ExpiryCheckTypeId.Should().Be(expiryCheckTypeId); + } +} diff --git a/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/GlobalUsings.cs b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/GlobalUsings.cs new file mode 100644 index 00000000..cee81f54 --- /dev/null +++ b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/GlobalUsings.cs @@ -0,0 +1,23 @@ +/******************************************************************************** + * Copyright (c) 2021, 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 Xunit; diff --git a/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/SsiCredentialIssuer.Expiry.App.Tests.csproj b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/SsiCredentialIssuer.Expiry.App.Tests.csproj new file mode 100644 index 00000000..f6c3e3e3 --- /dev/null +++ b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/SsiCredentialIssuer.Expiry.App.Tests.csproj @@ -0,0 +1,55 @@ + + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs new file mode 100644 index 00000000..e17fbe35 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CompanySsiDetailsRepositoryTests.cs @@ -0,0 +1,588 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +public class CompanySsiDetailsRepositoryTests +{ + private const string ValidBpnl = "BPNL00000003AYRE"; + private readonly TestDbFixture _dbTestDbFixture; + private readonly Guid _userId = new("ac1cf001-7fbc-1f2f-817f-bce058020006"); + + public CompanySsiDetailsRepositoryTests(TestDbFixture testDbFixture) + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + } + + #region GetDetailsForCompany + + [Fact] + public async Task GetDetailsForCompany_WithValidData_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetUseCaseParticipationForCompany(ValidBpnl, DateTimeOffset.MinValue).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(5); + result.Where(x => x.Description != null).Should().HaveCount(5).And.Satisfy( + x => x.Description == "T", + x => x.Description == "CO2", + x => x.Description == "BT", + x => x.Description == "CE", + x => x.Description == "QM"); + var traceability = result.Single(x => x.CredentialType == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK); + traceability.VerifiedCredentials.Should().HaveCount(3).And.Satisfy( + x => x.ExternalDetailData.Version == "1.0.0" && x.SsiDetailData.Single().ParticipationStatus == CompanySsiDetailStatusId.PENDING, + x => x.ExternalDetailData.Version == "2.0.0" && !x.SsiDetailData.Any(), + x => x.ExternalDetailData.Version == "3.0.0" && !x.SsiDetailData.Any()); + } + + [Fact] + public async Task GetDetailsForCompany_WithExpiryFilter_ReturnsExpected() + { + // Arrange + var dt = new DateTimeOffset(2023, 9, 29, 0, 0, 0, TimeSpan.Zero); + var sut = await CreateSut(); + + // Act + var result = await sut.GetUseCaseParticipationForCompany(ValidBpnl, dt).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(5); + result.Where(x => x.Description != null).Should().HaveCount(5).And.Satisfy( + x => x.Description == "T", + x => x.Description == "CO2", + x => x.Description == "BT", + x => x.Description == "CE", + x => x.Description == "QM"); + var traceability = result.Single(x => x.CredentialType == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK); + traceability.VerifiedCredentials.Should().HaveCount(3).And.Satisfy( + x => x.ExternalDetailData.Version == "1.0.0" && x.SsiDetailData.Count() == 1, + x => x.ExternalDetailData.Version == "2.0.0" && !x.SsiDetailData.Any(), + x => x.ExternalDetailData.Version == "3.0.0" && !x.SsiDetailData.Any()); + } + + #endregion + + #region GetAllCredentialDetails + + [Fact] + public async Task GetAllCredentialDetails_WithValidData_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetAllCredentialDetails(null, null).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().NotBeNull(); + result.Count.Should().Be(7); + result.Should().HaveCount(7); + result.Where(x => x.Bpnl == ValidBpnl).Should().HaveCount(6) + .And.Satisfy( + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.PCF_FRAMEWORK && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.BEHAVIOR_TWIN_FRAMEWORK && x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE); + result.Where(x => x.Bpnl == "BPNL00000001LLHA").Should().ContainSingle() + .And.Satisfy(x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK); + } + + [Fact] + public async Task GetAllCredentialDetails_WithWithStatusId_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetAllCredentialDetails(CompanySsiDetailStatusId.PENDING, null).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().NotBeNull().And.HaveCount(4); + result.Count.Should().Be(4); + result.Where(x => x.Bpnl == ValidBpnl).Should().HaveCount(3) + .And.Satisfy( + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.PCF_FRAMEWORK, + x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE); + result.Should().ContainSingle(x => x.Bpnl == "BPNL00000001LLHA") + .Which.Should().Match(x => x.VerifiedCredentialTypeId == VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK); + } + + [Fact] + public async Task GetAllCredentialDetails_WithWithCredentialType_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetAllCredentialDetails(null, VerifiedCredentialTypeId.PCF_FRAMEWORK).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().NotBeNull().And.ContainSingle().Which.Bpnl.Should().Be(ValidBpnl); + result.Count.Should().Be(1); + } + + #endregion + + #region GetSsiCertificates + + [Fact] + public async Task GetSsiCertificates_WithValidData_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetSsiCertificates(ValidBpnl, new DateTimeOffset(2023, 01, 01, 01, 01, 01, TimeSpan.Zero)).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(2) + .And.Satisfy( + x => x.CredentialType == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE && + x.Credentials.Count() == 1 && + x.Credentials.Single().SsiDetailData.Count(ssi => ssi.ParticipationStatus == CompanySsiDetailStatusId.PENDING) == 1, + x => x.CredentialType == VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER && !x.Credentials.Any() + ); + } + + #endregion + + #region CreateSsiDetails + + [Fact] + public async Task CreateSsiDetails_WithValidData_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.CreateSsiDetails(new("9f5b9934-4014-4099-91e9-7b1aee696b03"), VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, new Guid("00000000-0000-0000-0000-000000000001"), CompanySsiDetailStatusId.PENDING, _userId, null); + + // Assert + context.ChangeTracker.HasChanges().Should().BeTrue(); + context.ChangeTracker.Entries().Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .Which.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.PENDING); + } + + #endregion + + #region CheckSsiDetailsExistsForCompany + + [Fact] + public async Task CheckCredentialDetailsExistsForCompany_WithExistingData_ReturnsTrue() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckSsiDetailsExistsForCompany(ValidBpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, VerifiedCredentialTypeKindId.FRAMEWORK, new Guid("1268a76a-ca19-4dd8-b932-01f24071d560")).ConfigureAwait(false); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task CheckCredentialDetailsExistsForCompany_WithNotExistingData_ReturnsTrue() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckSsiDetailsExistsForCompany("BPNL000000001TEST", VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, VerifiedCredentialTypeKindId.FRAMEWORK, new Guid("1268a76a-ca19-4dd8-b932-01f24071d560")).ConfigureAwait(false); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task CheckCredentialDetailsExistsForCompany_WithWrongTypeKindId_ReturnsTrue() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckSsiDetailsExistsForCompany("BPNL000000001TEST", VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, VerifiedCredentialTypeKindId.MEMBERSHIP, new Guid("1268a76a-ca19-4dd8-b932-01f24071d560")).ConfigureAwait(false); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public async Task CheckCredentialDetailsExistsForCompany_WithInactive_ReturnsFalse() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckSsiDetailsExistsForCompany(ValidBpnl, VerifiedCredentialTypeId.BEHAVIOR_TWIN_FRAMEWORK, VerifiedCredentialTypeKindId.FRAMEWORK, new Guid("1268a76a-ca19-4dd8-b932-01f24071d562")).ConfigureAwait(false); + + // Assert + result.Should().BeFalse(); + } + + #endregion + + #region CheckUseCaseCredentialAndExternalTypeDetails + + [Theory] + [InlineData(VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, "1268a76a-ca19-4dd8-b932-01f24071d560", "2023-09-30 +0")] + [InlineData(VerifiedCredentialTypeId.PCF_FRAMEWORK, "1268a76a-ca19-4dd8-b932-01f24071d561", "2023-09-30 +0")] +#pragma warning disable xUnit1012 + [InlineData(default, "1268a76a-ca19-6666-b932-01f24071d561", default)] +#pragma warning restore xUnit1012 + public async Task CheckUseCaseCredentialAndExternalTypeDetails_WithTypeId_ReturnsTrue(VerifiedCredentialTypeId typeId, Guid detailId, DateTimeOffset expiry) + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(detailId, typeId).ConfigureAwait(false); + + // Assert + result.Expiry.Should().Be(expiry); + } + + #endregion + + #region CheckSsiCertificateType + + [Theory] + [InlineData(VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, false)] + [InlineData(VerifiedCredentialTypeId.PCF_FRAMEWORK, false)] + [InlineData(VerifiedCredentialTypeId.BEHAVIOR_TWIN_FRAMEWORK, false)] + [InlineData(VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, true)] + public async Task CheckSsiCertificateType_WithTypeId_ReturnsTrue(VerifiedCredentialTypeId typeId, bool expectedResult) + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.CheckSsiCertificateType(typeId).ConfigureAwait(false); + + // Assert + result.Exists.Should().Be(expectedResult); + } + + #endregion + + #region GetSsiApprovalData + + [Fact] + public async Task GetSsiApprovalData_WithValidData_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetSsiApprovalData(new("9f5b9934-4014-4099-91e9-7b1aee696b03")).ConfigureAwait(false); + + // Assert + result.exists.Should().BeTrue(); + result.data.Bpn.Should().Be("BPNL00000003AYRE"); + result.data.DetailData.Should().NotBeNull(); + result.data.DetailData!.VerifiedCredentialExternalTypeId.Should().Be(VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL); + } + + [Fact] + public async Task GetSsiApprovalData_WithNotExisting_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetSsiApprovalData(Guid.NewGuid()).ConfigureAwait(false); + + // Assert + result.exists.Should().BeFalse(); + } + + #endregion + + #region GetAllCredentialDetails + + [Fact] + public async Task GetSsiRejectionData_WithValidData_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetSsiRejectionData(new("9f5b9934-4014-4099-91e9-7b1aee696b03")).ConfigureAwait(false); + + // Assert + result.Exists.Should().BeTrue(); + result.Status.Should().Be(CompanySsiDetailStatusId.PENDING); + result.Type.Should().Be(VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK); + } + + [Fact] + public async Task GetSsiRejectionData_WithNotExisting_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetSsiRejectionData(Guid.NewGuid()).ConfigureAwait(false); + + // Assert + result.Should().Be(default); + } + + #endregion + + #region AttachAndModifyCompanySsiDetails + + [Fact] + public async Task AttachAndModifyCompanySsiDetails_WithValidData_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.AttachAndModifyCompanySsiDetails(new("9f5b9934-4014-4099-91e9-7b1aee696b03"), null, ssi => + { + ssi.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + }); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .And.Match(x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE); + } + + [Fact] + public async Task AttachAndModifyCompanySsiDetails_WithNoChanges_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.AttachAndModifyCompanySsiDetails(new("9f5b9934-4014-4099-91e9-7b1aee696b03"), ssi => + { + ssi.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + }, ssi => + { + ssi.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + }); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeFalse(); + changedEntries.Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .And.Match(x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE); + } + + #endregion + + #region GetCertificateTypes + + [Fact] + public async Task GetCertificateTypes_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetCertificateTypes(ValidBpnl).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().ContainSingle().Which.Should().Be(VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER); + } + + [Fact] + public async Task GetCertificateTypes_WithoutCertificate_ReturnsExpected() + { + // Arrange + var sut = await CreateSut(); + + // Act + var result = await sut.GetCertificateTypes("BPNL0000001TEST").ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(2).And.Satisfy( + x => x == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, + x => x == VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER); + } + + #endregion + + #region GetExpiryData + + [Fact] + public async Task GetExpiryData_ReturnsExpected() + { + // Arrange + var now = new DateTimeOffset(2024, 1, 1, 1, 1, 1, TimeSpan.Zero); + var inactiveVcsToDelete = now.AddMonths(-12); + var expiredVcsToDelete = now.AddDays(-42); + var sut = await CreateSut(); + + // Act + var result = await sut.GetExpiryData(now, inactiveVcsToDelete, expiredVcsToDelete).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(6); + result.Where(x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.PENDING).Should().HaveCount(3); + result.Where(x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.INACTIVE).Should().HaveCount(3); + } + + #endregion + + #region CreateCredentialDetails + + [Fact] + public async Task CreateProcessData_WithValidData_ReturnsExpected() + { + // Arrange + var json = JsonDocument.Parse(""" + { + "root": "test123" + } + """); + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.CreateProcessData(Guid.NewGuid(), json, VerifiedCredentialTypeKindId.BPN, x => x.ClientId = "c1"); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .And.Match(x => x.ClientId == "c1"); + } + + #endregion + + #region RemoveSsiDetail + + [Fact] + public async Task RemoveSsiDetail_WithValidData_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.RemoveSsiDetail(Guid.NewGuid()); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().ContainSingle().Which.State.Should().Be(EntityState.Deleted); + } + + #endregion + + #region AttachAndModifyProcessData + + [Fact] + public async Task AttachAndModifyProcessData_WithValidData_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.AttachAndModifyProcessData(new("9f5b9934-4014-4099-91e9-7b1aee696b03"), null, ssi => + { + ssi.EncryptionMode = 1; + }); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .And.Match(x => x.EncryptionMode == 1); + } + + [Fact] + public async Task AttachAndModifyProcessData_WithNoChanges_ReturnsExpected() + { + // Arrange + var (sut, context) = await CreateSutWithContext(); + + // Act + sut.AttachAndModifyProcessData(new("9f5b9934-4014-4099-91e9-7b1aee696b03"), ssi => + { + ssi.EncryptionMode = 1; + }, ssi => + { + ssi.EncryptionMode = 1; + }); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeFalse(); + changedEntries.Should().ContainSingle() + .Which.Entity.Should().BeOfType() + .And.Match(x => x.EncryptionMode == 1); + } + + #endregion + + #region Setup + + private async Task CreateSut() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + return new CompanySsiDetailsRepository(context); + } + + private async Task<(CompanySsiDetailsRepository sut, IssuerDbContext context)> CreateSutWithContext() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + return (new CompanySsiDetailsRepository(context), context); + } + + #endregion +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/ContentTypeMapperExtensionsTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/ContentTypeMapperExtensionsTests.cs new file mode 100644 index 00000000..9a0f499d --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/ContentTypeMapperExtensionsTests.cs @@ -0,0 +1,83 @@ +/******************************************************************************** + * 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 FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Extensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +public class ContentTypeMapperExtensionsTests +{ + [Theory] + [InlineData(MediaTypeId.GIF, "image/gif")] + [InlineData(MediaTypeId.PDF, "application/pdf")] + [InlineData(MediaTypeId.PNG, "image/png")] + [InlineData(MediaTypeId.SVG, "image/svg+xml")] + [InlineData(MediaTypeId.JSON, "application/json")] + [InlineData(MediaTypeId.JPEG, "image/jpeg")] + [InlineData(MediaTypeId.TIFF, "image/tiff")] + [InlineData(MediaTypeId.PEM, "application/x-pem-file")] + [InlineData(MediaTypeId.CA_CERT, "application/x-x509-ca-cert")] + [InlineData(MediaTypeId.PKX_CER, "application/pkix-cert")] + [InlineData(MediaTypeId.OCTET, "application/octet-stream")] + public void MapToMediaType_WithValid_ReturnsExpected(MediaTypeId mediaTypeId, string result) + { + var mediaType = mediaTypeId.MapToMediaType(); + mediaType.Should().Be(result); + } + + [Fact] + public void MapToMediaType_WithInvalid_ThrowsConflictException() + { + void Act() => ((MediaTypeId)666).MapToMediaType(); + + var ex = Assert.Throws((Action)Act); + ex.Message.Should().Be($"document mediatype 666 is not supported"); + } + + [Theory] + [InlineData(MediaTypeId.GIF, "image/gif")] + [InlineData(MediaTypeId.PDF, "application/pdf")] + [InlineData(MediaTypeId.PNG, "image/png")] + [InlineData(MediaTypeId.SVG, "image/svg+xml")] + [InlineData(MediaTypeId.JSON, "application/json")] + [InlineData(MediaTypeId.JPEG, "image/jpeg")] + [InlineData(MediaTypeId.TIFF, "image/tiff")] + [InlineData(MediaTypeId.PEM, "application/x-pem-file")] + [InlineData(MediaTypeId.CA_CERT, "application/x-x509-ca-cert")] + [InlineData(MediaTypeId.PKX_CER, "application/pkix-cert")] + [InlineData(MediaTypeId.OCTET, "application/octet-stream")] + public void ParseMediaTypeId_WithValid_ReturnsExpected(MediaTypeId expectedResult, string mediaType) + { + var result = mediaType.ParseMediaTypeId(); + result.Should().Be(expectedResult); + } + + [Fact] + public void ParseMediaTypeId_WithInvalid_ThrowsUnsupportedMediaTypeException() + { + void Act() => "just a test".ParseMediaTypeId(); + + var ex = Assert.Throws((Action)Act); + ex.Message.Should().Be($"mediaType 'just a test' is not supported"); + } +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs new file mode 100644 index 00000000..8b31a0ea --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/CredentialRepositoryTests.cs @@ -0,0 +1,148 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +/// +/// Tests the functionality of the +/// +public class CredentialRepositoryTests : IAssemblyFixture +{ + private readonly TestDbFixture _dbTestDbFixture; + + public CredentialRepositoryTests(TestDbFixture testDbFixture) + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + } + + #region GetDataForProcessId + + [Fact] + public async Task GetDataForProcessId_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetDataForProcessId(new Guid("dd371565-9489-4907-a2e4-b8cbfe7a8cd2")); + + // Assert + result.Exists.Should().BeTrue(); + result.CredentialId.Should().Be(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + } + + #endregion + + #region GetCredentialData + + [Fact] + public async Task GetCredentialData_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetCredentialData(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + + // Assert + result.HolderWalletData.WalletUrl.Should().Be("https://example.org/wallet"); + result.HolderWalletData.ClientId.Should().Be("c123"); + result.EncryptionInformation.EncryptionMode.Should().Be(1); + } + + #endregion + + #region GetWalletCredentialId + + [Fact] + public async Task GetWalletCredentialId_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetWalletCredentialId(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + + // Assert + result.Should().Be(new Guid("bd474c60-e7ce-450f-bdf4-73604546fc5e")); + } + + #endregion + + #region GetCredentialStorageInformationById + + [Fact] + public async Task GetCredentialStorageInformationById_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetCredentialStorageInformationById(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + + // Assert + result.CredentialTypeKindId.Should().Be(VerifiedCredentialTypeKindId.FRAMEWORK); + result.Schema.RootElement.GetRawText().Should().Be("{\"root\": \"test123\"}"); + } + + #endregion + + #region GetExternalCredentialAndKindId + + [Fact] + public async Task GetExternalCredentialAndKindId_ReturnsExpectedDocument() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetExternalCredentialAndKindId(new Guid("9f5b9934-4014-4099-91e9-7b1aee696b03")); + + // Assert + result.ExternalCredentialId.Should().Be(new Guid("bd474c60-e7ce-450f-bdf4-73604546fc5e")); + result.KindId.Should().Be(VerifiedCredentialTypeKindId.FRAMEWORK); + } + + #endregion + + #region Setup + + private async Task CreateSut() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new CredentialRepository(context); + return sut; + } + + #endregion +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/DocumentRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/DocumentRepositoryTests.cs new file mode 100644 index 00000000..2d24a59f --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/DocumentRepositoryTests.cs @@ -0,0 +1,116 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Text; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +/// +/// Tests the functionality of the +/// +public class DocumentRepositoryTests : IAssemblyFixture +{ + private readonly TestDbFixture _dbTestDbFixture; + + public DocumentRepositoryTests(TestDbFixture testDbFixture) + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + } + + #region Create Document + + [Fact] + public async Task CreateDocument_ReturnsExpectedDocument() + { + // Arrange + var (sut, context) = await CreateSut().ConfigureAwait(false); + var test = "This is just test content"; + var content = Encoding.UTF8.GetBytes(test); + + // Act + var result = sut.CreateDocument("New Document", content, content, MediaTypeId.PDF, DocumentTypeId.CREDENTIAL, doc => + { + doc.DocumentStatusId = DocumentStatusId.INACTIVE; + }); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + result.DocumentTypeId.Should().Be(DocumentTypeId.CREDENTIAL); + result.DocumentStatusId.Should().Be(DocumentStatusId.INACTIVE); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().NotBeEmpty(); + changedEntries.Should().HaveCount(1); + var changedEntity = changedEntries.Single(); + changedEntity.State.Should().Be(EntityState.Added); + } + + #endregion + + #region AssignDocumentToCompanySsiDetails + + [Fact] + public async Task AssignDocumentToCompanySsiDetails_ReturnsExpectedDocument() + { + // Arrange + var (sut, context) = await CreateSut().ConfigureAwait(false); + var companySsiDetailId = Guid.NewGuid(); + var documentId = Guid.NewGuid(); + + // Act + sut.AssignDocumentToCompanySsiDetails(documentId, companySsiDetailId); + + // Assert + var changeTracker = context.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().NotBeEmpty(); + changedEntries.Should().HaveCount(1); + var changedEntity = changedEntries.Single(); + changedEntity.State.Should().Be(EntityState.Added); + } + + #endregion + + #region Setup + + private async Task<(DocumentRepository, IssuerDbContext)> CreateSut() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new DocumentRepository(context); + return (sut, context); + } + + #endregion +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerDbContextTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerDbContextTests.cs new file mode 100644 index 00000000..8b9b400e --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerDbContextTests.cs @@ -0,0 +1,117 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.AuditEntities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +public class IssuerDbContextTests : IAssemblyFixture +{ + private readonly TestDbFixture _dbTestDbFixture; + private readonly IDateTimeProvider _dateTimeProvider; + + public IssuerDbContextTests(TestDbFixture testDbFixture) + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + _dateTimeProvider = A.Fake(); + } + + #region SaveAuditableEntity + + [Fact] + public async Task SaveCreatedAuditableEntity_SetsLastEditorId() + { + // Arrange + var now = DateTimeOffset.UtcNow; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + + var before = now.AddDays(-1); + var id = Guid.NewGuid(); + var ca = new CompanySsiDetail(id, "BPNL00000001TEST", VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, CompanySsiDetailStatusId.ACTIVE, new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001"), before); + + var sut = await CreateContext().ConfigureAwait(false); + using var trans = await sut.Database.BeginTransactionAsync().ConfigureAwait(false); + + // Act + sut.Add(ca); + await sut.SaveChangesAsync().ConfigureAwait(false); + + // Assert + ca.LastEditorId.Should().NotBeNull().And.Be(new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001")); + ca.DateLastChanged.Should().Be(now); + var auditEntries = await sut.AuditCompanySsiDetail20240228.Where(x => x.Id == id).ToListAsync(); + auditEntries.Should().ContainSingle().Which.Should().Match( + x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && (x.DateCreated - before) < TimeSpan.FromSeconds(1) && x.AuditV1OperationId == AuditOperationId.INSERT && (x.AuditV1DateLastChanged - now) < TimeSpan.FromSeconds(1) && x.LastEditorId == new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001")); + await trans.RollbackAsync().ConfigureAwait(false); + } + + [Fact] + public async Task SaveDeletedAuditableEntity_SetsLastEditorId() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var later = now.AddMinutes(1); + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now).Once().Then.Returns(later); + + var before = now.AddDays(-1); + var id = Guid.NewGuid(); + var ca = new CompanySsiDetail(id, "BPNL00000001TEST", VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, CompanySsiDetailStatusId.ACTIVE, new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001"), before); + + var sut = await CreateContext().ConfigureAwait(false); + using var trans = await sut.Database.BeginTransactionAsync().ConfigureAwait(false); + + // Act + sut.Add(ca); + await sut.SaveChangesAsync().ConfigureAwait(false); + sut.Remove(ca); + await sut.SaveChangesAsync().ConfigureAwait(false); + + // Assert + ca.LastEditorId.Should().NotBeNull().And.Be(new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001")); + ca.DateLastChanged.Should().Be(later); + var auditEntries = await sut.AuditCompanySsiDetail20240228.Where(x => x.Id == id).ToListAsync(); + auditEntries.Should().HaveCount(2).And.Satisfy( + x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && (x.DateCreated - before) < TimeSpan.FromSeconds(1) && x.AuditV1OperationId == AuditOperationId.INSERT && x.LastEditorId == new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001"), + x => x.CompanySsiDetailStatusId == CompanySsiDetailStatusId.ACTIVE && (x.DateCreated - before) < TimeSpan.FromSeconds(1) && x.AuditV1OperationId == AuditOperationId.DELETE && (x.AuditV1DateLastChanged - later) < TimeSpan.FromSeconds(1) && x.LastEditorId == new Guid("ac1cf001-7fbc-1f2f-817f-bce058020001")); + await trans.RollbackAsync().ConfigureAwait(false); + } + + #endregion + + private async Task CreateContext() => + await _dbTestDbFixture.GetDbContext(_dateTimeProvider).ConfigureAwait(false); +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerRepositoriesTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerRepositoriesTests.cs new file mode 100644 index 00000000..9757b7d0 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/IssuerRepositoriesTests.cs @@ -0,0 +1,168 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +public class IssuerRepositoriesTests : IAssemblyFixture +{ + private readonly IFixture _fixture; + private readonly TestDbFixture _dbTestDbFixture; + + public IssuerRepositoriesTests(TestDbFixture testDbFixture) + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + } + + #region GetInstance + + [Fact] + public async Task GetInstance_CompanySsiDetails_CreatesSuccessfully() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = sut.GetInstance(); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task GetInstance_Credential_CreatesSuccessfully() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = sut.GetInstance(); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task GetInstance_DocumentRepo_CreatesSuccessfully() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = sut.GetInstance(); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task GetInstance_ProcessStep_CreatesSuccessfully() + { + // Arrange + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = sut.GetInstance(); + + // Assert + result.Should().BeOfType(); + } + + #endregion + + #region Clear + + [Fact] + public async Task Clear_CreateSuccessfully() + { + // Arrange + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + var changeTracker = dbContext.ChangeTracker; + dbContext.Processes.Add(new Process(Guid.NewGuid(), ProcessTypeId.CREATE_CREDENTIAL, Guid.NewGuid())); + + // Act + sut.Clear(); + + // Assert + changeTracker.HasChanges().Should().BeFalse(); + changeTracker.Entries().Should().BeEmpty(); + } + + #endregion + + #region Attach + + [Fact] + public async Task Attach_CreateSuccessfully() + { + // Arrange + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + var changeTracker = dbContext.ChangeTracker; + var now = DateTimeOffset.Now; + + // Act + sut.Attach(new Process(new Guid("dd371565-9489-4907-a2e4-b8cbfe7a8cd2"), default, Guid.Empty), p => + { + p.LockExpiryDate = now; + p.ProcessTypeId = ProcessTypeId.CREATE_CREDENTIAL; + }); + + // Assert + changeTracker.HasChanges().Should().BeTrue(); + changeTracker.Entries().Should() + .ContainSingle() + .Which.State.Should().Be(EntityState.Modified); + changeTracker.Entries().Select(x => x.Entity).Cast() + .Should().Satisfy(x => x.ProcessTypeId == ProcessTypeId.CREATE_CREDENTIAL); + } + + #endregion + + private async Task<(IssuerRepositories sut, IssuerDbContext dbContext)> CreateSutWithContext() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new IssuerRepositories(context); + return (sut, context); + } + + private async Task CreateSut() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new IssuerRepositories(context); + return sut; + } +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/ProcessStepRepositoryTests.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/ProcessStepRepositoryTests.cs new file mode 100644 index 00000000..e7c6d62c --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/ProcessStepRepositoryTests.cs @@ -0,0 +1,359 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests; + +public class ProcessStepRepositoryTests : IAssemblyFixture +{ + private readonly IFixture _fixture; + private readonly TestDbFixture _dbTestDbFixture; + + public ProcessStepRepositoryTests(TestDbFixture testDbFixture) + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + _dbTestDbFixture = testDbFixture; + } + + #region CreateProcess + + [Fact] + public async Task CreateProcess_CreatesSuccessfully() + { + // Arrange + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + var changeTracker = dbContext.ChangeTracker; + + // Act + var result = sut.CreateProcess(ProcessTypeId.CREATE_CREDENTIAL); + + // Assert + changeTracker.HasChanges().Should().BeTrue(); + changeTracker.Entries().Should().HaveCount(1) + .And.AllSatisfy(x => + { + x.State.Should().Be(EntityState.Added); + x.Entity.Should().BeOfType(); + }); + changeTracker.Entries().Select(x => x.Entity).Cast() + .Should().Satisfy( + x => x.Id == result.Id && x.ProcessTypeId == ProcessTypeId.CREATE_CREDENTIAL + ); + } + + #endregion + + #region CreateProcessStepRange + + [Fact] + + public async Task CreateProcessStepRange_CreateSuccessfully() + { + // Arrange + var processId = Guid.NewGuid(); + var processStepTypeIds = _fixture.CreateMany(3).ToImmutableArray(); + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + var changeTracker = dbContext.ChangeTracker; + + // Act + var result = sut.CreateProcessStepRange(processStepTypeIds.Select(processStepTypeId => (processStepTypeId, ProcessStepStatusId.TODO, processId))); + + // Assert + changeTracker.HasChanges().Should().BeTrue(); + changeTracker.Entries().Should() + .HaveSameCount(processStepTypeIds) + .And.AllSatisfy(x => + { + x.State.Should().Be(EntityState.Added); + x.Entity.Should().BeOfType(); + }); + changeTracker.Entries().Select(x => x.Entity).Cast() + .Should().Satisfy( + x => x.Id == result.ElementAt(0).Id && x.ProcessId == processId && x.ProcessStepTypeId == processStepTypeIds[0] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.Id == result.ElementAt(1).Id && x.ProcessId == processId && x.ProcessStepTypeId == processStepTypeIds[1] && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.Id == result.ElementAt(2).Id && x.ProcessId == processId && x.ProcessStepTypeId == processStepTypeIds[2] && x.ProcessStepStatusId == ProcessStepStatusId.TODO + ); + } + + #endregion + + #region CreateProcessStep + + [Fact] + public async Task CreateProcessStep_CreateSuccessfully() + { + // Arrange + var processId = Guid.NewGuid(); + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + var changeTracker = dbContext.ChangeTracker; + + // Act + sut.CreateProcessStep(ProcessStepTypeId.SIGN_CREDENTIAL, ProcessStepStatusId.TODO, processId); + + // Assert + changeTracker.HasChanges().Should().BeTrue(); + changeTracker.Entries().Should() + .ContainSingle() + .Which.State.Should().Be(EntityState.Added); + changeTracker.Entries().Select(x => x.Entity).Cast() + .Should().Satisfy( + x => x.ProcessId == processId && x.ProcessStepTypeId == ProcessStepTypeId.SIGN_CREDENTIAL && x.ProcessStepStatusId == ProcessStepStatusId.TODO + ); + } + + #endregion + + #region AttachAndModifyProcessStep + + [Fact] + public async Task AttachAndModifyProcessStep_WithExistingProcessStep_UpdatesStatus() + { + // Arrange + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + + // Act + sut.AttachAndModifyProcessStep(new Guid("48f35f84-8d98-4fbd-ba80-8cbce5eeadb5"), + existing => + { + existing.ProcessStepStatusId = ProcessStepStatusId.TODO; + }, + modify => + { + modify.ProcessStepStatusId = ProcessStepStatusId.DONE; + } + ); + + // Assert + var changeTracker = dbContext.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().NotBeEmpty(); + changedEntries.Should().HaveCount(1); + var changedEntity = changedEntries.Single(); + changedEntity.State.Should().Be(EntityState.Modified); + changedEntity.Entity.Should().BeOfType().Which.ProcessStepStatusId.Should().Be(ProcessStepStatusId.DONE); + } + + #endregion + + #region AttachAndModifyProcessSteps + + [Fact] + public async Task AttachAndModifyProcessSteps_UpdatesStatus() + { + // Arrange + var stepData = _fixture.CreateMany<(Guid ProcessStepId, ProcessStep InitialStep, ProcessStep ModifiedStep)>(5).ToImmutableArray(); + + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + + // Act + sut.AttachAndModifyProcessSteps(stepData.Select(data => new ValueTuple?, Action>( + data.ProcessStepId, + step => + { + step.ProcessStepStatusId = data.InitialStep.ProcessStepStatusId; + step.DateLastChanged = data.InitialStep.DateLastChanged; + step.Message = data.InitialStep.Message; + }, + step => + { + step.ProcessStepStatusId = data.ModifiedStep.ProcessStepStatusId; + step.DateLastChanged = data.ModifiedStep.DateLastChanged; + step.Message = data.ModifiedStep.Message; + }))); + + // Assert + var changeTracker = dbContext.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().HaveCount(5).And.AllSatisfy(entry => entry.State.Should().Be(EntityState.Modified)); + changedEntries.Select(entry => entry.Entity).Should().AllBeOfType().Which.Should().Satisfy( + step => step.Id == stepData[0].ProcessStepId && step.ProcessStepStatusId == stepData[0].ModifiedStep.ProcessStepStatusId && step.DateLastChanged == stepData[0].ModifiedStep.DateLastChanged && step.Message == stepData[0].ModifiedStep.Message, + step => step.Id == stepData[1].ProcessStepId && step.ProcessStepStatusId == stepData[1].ModifiedStep.ProcessStepStatusId && step.DateLastChanged == stepData[1].ModifiedStep.DateLastChanged && step.Message == stepData[1].ModifiedStep.Message, + step => step.Id == stepData[2].ProcessStepId && step.ProcessStepStatusId == stepData[2].ModifiedStep.ProcessStepStatusId && step.DateLastChanged == stepData[2].ModifiedStep.DateLastChanged && step.Message == stepData[2].ModifiedStep.Message, + step => step.Id == stepData[3].ProcessStepId && step.ProcessStepStatusId == stepData[3].ModifiedStep.ProcessStepStatusId && step.DateLastChanged == stepData[3].ModifiedStep.DateLastChanged && step.Message == stepData[3].ModifiedStep.Message, + step => step.Id == stepData[4].ProcessStepId && step.ProcessStepStatusId == stepData[4].ModifiedStep.ProcessStepStatusId && step.DateLastChanged == stepData[4].ModifiedStep.DateLastChanged && step.Message == stepData[4].ModifiedStep.Message + ); + } + + [Fact] + public async Task AttachAndModifyProcessSteps_WithUnmodifiedData_SkipsUpdateStatus() + { + // Arrange + var stepData = _fixture.CreateMany<(Guid ProcessStepId, ProcessStep InitialStep)>(5).ToImmutableArray(); + + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + + // Act + sut.AttachAndModifyProcessSteps(stepData.Select(data => new ValueTuple?, Action>( + data.ProcessStepId, + step => + { + step.ProcessStepStatusId = data.InitialStep.ProcessStepStatusId; + step.DateLastChanged = data.InitialStep.DateLastChanged; + step.Message = data.InitialStep.Message; + }, + step => + { + step.DateLastChanged = data.InitialStep.DateLastChanged; + }))); + + // Assert + var changeTracker = dbContext.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeFalse(); + changedEntries.Should().HaveCount(5).And.AllSatisfy(entry => entry.State.Should().Be(EntityState.Unchanged)); + changedEntries.Select(entry => entry.Entity).Should().AllBeOfType().Which.Should().Satisfy( + step => step.Id == stepData[0].ProcessStepId && step.ProcessStepStatusId == stepData[0].InitialStep.ProcessStepStatusId && step.DateLastChanged == stepData[0].InitialStep.DateLastChanged && step.Message == stepData[0].InitialStep.Message, + step => step.Id == stepData[1].ProcessStepId && step.ProcessStepStatusId == stepData[1].InitialStep.ProcessStepStatusId && step.DateLastChanged == stepData[1].InitialStep.DateLastChanged && step.Message == stepData[1].InitialStep.Message, + step => step.Id == stepData[2].ProcessStepId && step.ProcessStepStatusId == stepData[2].InitialStep.ProcessStepStatusId && step.DateLastChanged == stepData[2].InitialStep.DateLastChanged && step.Message == stepData[2].InitialStep.Message, + step => step.Id == stepData[3].ProcessStepId && step.ProcessStepStatusId == stepData[3].InitialStep.ProcessStepStatusId && step.DateLastChanged == stepData[3].InitialStep.DateLastChanged && step.Message == stepData[3].InitialStep.Message, + step => step.Id == stepData[4].ProcessStepId && step.ProcessStepStatusId == stepData[4].InitialStep.ProcessStepStatusId && step.DateLastChanged == stepData[4].InitialStep.DateLastChanged && step.Message == stepData[4].InitialStep.Message + ); + } + + [Fact] + public async Task AttachAndModifyProcessSteps_WithUnmodifiedData_UpdatesLastChanged() + { + // Arrange + var stepData = _fixture.CreateMany<(Guid ProcessStepId, ProcessStep InitialStep)>(5).ToImmutableArray(); + + var (sut, dbContext) = await CreateSutWithContext().ConfigureAwait(false); + + // Act + sut.AttachAndModifyProcessSteps(stepData.Select(data => new ValueTuple?, Action>( + data.ProcessStepId, + step => + { + step.ProcessStepStatusId = data.InitialStep.ProcessStepStatusId; + step.DateLastChanged = data.InitialStep.DateLastChanged; + step.Message = data.InitialStep.Message; + }, + step => { }))); + + // Assert + var changeTracker = dbContext.ChangeTracker; + var changedEntries = changeTracker.Entries().ToList(); + changeTracker.HasChanges().Should().BeTrue(); + changedEntries.Should().HaveCount(5).And.AllSatisfy(entry => entry.State.Should().Be(EntityState.Modified)); + changedEntries.Select(entry => entry.Entity).Should().AllBeOfType().Which.Should().Satisfy( + step => step.Id == stepData[0].ProcessStepId && step.ProcessStepStatusId == stepData[0].InitialStep.ProcessStepStatusId && step.DateLastChanged != stepData[0].InitialStep.DateLastChanged && step.Message == stepData[0].InitialStep.Message, + step => step.Id == stepData[1].ProcessStepId && step.ProcessStepStatusId == stepData[1].InitialStep.ProcessStepStatusId && step.DateLastChanged != stepData[1].InitialStep.DateLastChanged && step.Message == stepData[1].InitialStep.Message, + step => step.Id == stepData[2].ProcessStepId && step.ProcessStepStatusId == stepData[2].InitialStep.ProcessStepStatusId && step.DateLastChanged != stepData[2].InitialStep.DateLastChanged && step.Message == stepData[2].InitialStep.Message, + step => step.Id == stepData[3].ProcessStepId && step.ProcessStepStatusId == stepData[3].InitialStep.ProcessStepStatusId && step.DateLastChanged != stepData[3].InitialStep.DateLastChanged && step.Message == stepData[3].InitialStep.Message, + step => step.Id == stepData[4].ProcessStepId && step.ProcessStepStatusId == stepData[4].InitialStep.ProcessStepStatusId && step.DateLastChanged != stepData[4].InitialStep.DateLastChanged && step.Message == stepData[4].InitialStep.Message + ); + } + + #endregion + + #region GetActiveProcesses + + [Fact] + public async Task GetActiveProcess_LockExpired_ReturnsExpected() + { + // Arrange + var processTypeIds = new[] { ProcessTypeId.CREATE_CREDENTIAL }; + var processStepTypeIds = new[] { + ProcessStepTypeId.CREATE_CREDENTIAL, + ProcessStepTypeId.SIGN_CREDENTIAL, + ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT, + ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER, + }; + + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetActiveProcesses(processTypeIds, processStepTypeIds, DateTimeOffset.Parse("2023-03-02 00:00:00.000000 +00:00")).ToListAsync().ConfigureAwait(false); + result.Should().HaveCount(1) + .And.Satisfy( + x => x.Id == new Guid("dd371565-9489-4907-a2e4-b8cbfe7a8cd2") && x.ProcessTypeId == ProcessTypeId.CREATE_CREDENTIAL && x.LockExpiryDate == DateTimeOffset.Parse("2023-03-01 00:00:00.000000 +00:00") + ); + } + + [Fact] + public async Task GetActiveProcess_Locked_ReturnsExpected() + { + // Arrange + var processTypeIds = new[] { ProcessTypeId.CREATE_CREDENTIAL }; + var processStepTypeIds = new[] { + ProcessStepTypeId.CREATE_CREDENTIAL, + ProcessStepTypeId.SIGN_CREDENTIAL, + ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT, + ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER, + }; + + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetActiveProcesses(processTypeIds, processStepTypeIds, DateTimeOffset.Parse("2023-02-28 00:00:00.000000 +00:00")).ToListAsync().ConfigureAwait(false); + result.Should().BeEmpty(); + } + + #endregion + + #region GetProcessStepData + + [Fact] + public async Task GetProcessStepData_ReturnsExpected() + { + // Arrange + var processId = new Guid("dd371565-9489-4907-a2e4-b8cbfe7a8cd2"); + var sut = await CreateSut().ConfigureAwait(false); + + // Act + var result = await sut.GetProcessStepData(processId).ToListAsync().ConfigureAwait(false); + result.Should().HaveCount(1) + .And.Satisfy( + x => x.ProcessStepId == new Guid("cd231cb8-55de-4ae4-b93f-d440512341fb") && x.ProcessStepTypeId == ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT + ); + } + + #endregion + + private async Task<(ProcessStepRepository sut, IssuerDbContext dbContext)> CreateSutWithContext() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new ProcessStepRepository(context); + return (sut, context); + } + + private async Task CreateSut() + { + var context = await _dbTestDbFixture.GetDbContext().ConfigureAwait(false); + var sut = new ProcessStepRepository(context); + return sut; + } +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_details.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_details.test.json new file mode 100644 index 00000000..661ca3e1 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_details.test.json @@ -0,0 +1,88 @@ +[ + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b03", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 1, + "company_ssi_detail_status_id": 1, + "document_id": "e020787d-1e04-4c0b-9c06-bd1cd44724b1", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "1268a76a-ca19-4dd8-b932-01f24071d560", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null, + "process_id": "dd371565-9489-4907-a2e4-b8cbfe7a8cd2", + "external_credential_id": "bd474c60-e7ce-450f-bdf4-73604546fc5e" + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b04", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 2, + "company_ssi_detail_status_id": 1, + "document_id": "5adbdf90-c6ef-47a5-b596-2f00a731c39b", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "1268a76a-ca19-4dd8-b932-01f24071d561", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b05", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 4, + "company_ssi_detail_status_id": 1, + "document_id": "5adbdf90-c6ef-47a5-b596-2f00a731c39a", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "df3bd7d2-3349-410b-9b30-9a5238eb605e", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b06", + "bpnl": "BPNL00000001LLHA", + "verified_credential_type_id": 1, + "company_ssi_detail_status_id": 1, + "document_id": "3291cae8-3c7b-4862-8cec-93ea0dc8c61e", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "1268a76a-ca19-4dd8-b932-01f24071d560", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b07", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 3, + "company_ssi_detail_status_id": 3, + "document_id": "e020787d-1e04-4c0b-9c06-bd1cd44724b2", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "1268a76a-ca19-4dd8-b932-01f24071d562", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b08", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 4, + "company_ssi_detail_status_id": 3, + "document_id": "9685f744-9d90-4102-a949-fcd0bb86f954", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "df3bd7d2-3349-410b-9b30-9a5238eb605e", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + }, + { + "id": "9f5b9934-4014-4099-91e9-7b1aee696b09", + "bpnl": "BPNL00000003AYRE", + "verified_credential_type_id": 4, + "company_ssi_detail_status_id": 3, + "document_id": "88793f9f-c5a4-4621-847b-3d47cd839283", + "expiry_date": "2023-09-30 00:00:00.000000 +00:00", + "creator_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020006", + "verified_credential_external_type_detail_version_id": "df3bd7d2-3349-410b-9b30-9a5238eb605e", + "date_created": "2023-06-01 00:00:00.000000 +00:00", + "expiry_check_type_id": null + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json new file mode 100644 index 00000000..6921b95b --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/company_ssi_process_datas.test.json @@ -0,0 +1,12 @@ +[ + { + "company_ssi_detail_id": "9f5b9934-4014-4099-91e9-7b1aee696b03", + "schema": { + "root": "test123" + }, + "credential_type_kind_id": 1, + "encryption_mode": 1, + "holder_wallet_url": "https://example.org/wallet", + "client_id": "c123" + } +] \ No newline at end of file diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/documents.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/documents.test.json new file mode 100644 index 00000000..73c3ac4e --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/documents.test.json @@ -0,0 +1,79 @@ +[ + { + "id": "e020787d-1e04-4c0b-9c06-bd1cd44724b1", + "date_created": "2023-01-04T16:05:14.294575+00:00", + "document_name": "Default_App_Image.png", + "media_type_id": 3, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019993", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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", + "document_status_id": 2 + }, + { + "id": "5adbdf90-c6ef-47a5-b596-2f00a731c39b", + "date_created": "2023-01-04T19:29:08.602237+00:00", + "document_name": "Terms&Conditions-Active_Participant.pdf", + "media_type_id": 6, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058020001", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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MDAwMDAwMDkzMyAwMDAwMCBuIAowMDAwMDExNzI2IDAwMDAwIG4gCjAwMDAwMTQ1NTggMDAwMDAgbiAKMDAwMDAxNTA2NiAwMDAwMCBuIAowMDAwMDE0NzcwIDAwMDAwIG4gCjAwMDAwMTUzMDIgMDAwMDAgbiAKMDAwMDAyMjcyMyAwMDAwMCBuIAowMDAwMDIyMzQ0IDAwMDAwIG4gCjAwMDAwMjI5NTkgMDAwMDAgbiAKMDAwMDAzMzI4NSAwMDAwMCBuIAowMDAwMDMyOTc4IDAwMDAwIG4gCjAwMDAwMzM1MjEgMDAwMDAgbiAKMDAwMDA0MDk5MiAwMDAwMCBuIAowMDAwMDQwNjM3IDAwMDAwIG4gCjAwMDAwNDEyMjggMDAwMDAgbiAKMDAwMDA0OTg5MyAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDMwIC9Sb290IDE2IDAgUiAvSW5mbyAyOSAwIFIgL0lEIFsgPDQzZjZkMjJmYzFiM2NiZjk1MDhjNTliMWE0NjViYTdjPgo8NDNmNmQyMmZjMWIzY2JmOTUwOGM1OWIxYTQ2NWJhN2M+IF0gPj4Kc3RhcnR4cmVmCjUwMTEwCiUlRU9GCg==", + "document_status_id": 2 + }, + { + "id": "5adbdf90-c6ef-47a5-b596-2f00a731c39a", + "date_created": "2023-01-04T19:29:08.602237+00:00", + "document_name": "Terms&Conditions-Active_Participant.pdf", + "media_type_id": 6, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019992", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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MDAwMDAwMDkzMyAwMDAwMCBuIAowMDAwMDExNzI2IDAwMDAwIG4gCjAwMDAwMTQ1NTggMDAwMDAgbiAKMDAwMDAxNTA2NiAwMDAwMCBuIAowMDAwMDE0NzcwIDAwMDAwIG4gCjAwMDAwMTUzMDIgMDAwMDAgbiAKMDAwMDAyMjcyMyAwMDAwMCBuIAowMDAwMDIyMzQ0IDAwMDAwIG4gCjAwMDAwMjI5NTkgMDAwMDAgbiAKMDAwMDAzMzI4NSAwMDAwMCBuIAowMDAwMDMyOTc4IDAwMDAwIG4gCjAwMDAwMzM1MjEgMDAwMDAgbiAKMDAwMDA0MDk5MiAwMDAwMCBuIAowMDAwMDQwNjM3IDAwMDAwIG4gCjAwMDAwNDEyMjggMDAwMDAgbiAKMDAwMDA0OTg5MyAwMDAwMCBuIAp0cmFpbGVyCjw8IC9TaXplIDMwIC9Sb290IDE2IDAgUiAvSW5mbyAyOSAwIFIgL0lEIFsgPDQzZjZkMjJmYzFiM2NiZjk1MDhjNTliMWE0NjViYTdjPgo8NDNmNmQyMmZjMWIzY2JmOTUwOGM1OWIxYTQ2NWJhN2M+IF0gPj4Kc3RhcnR4cmVmCjUwMTEwCiUlRU9GCg==", + "document_status_id": 2 + }, + { + "id": "3291cae8-3c7b-4862-8cec-93ea0dc8c61e", + "date_created": "2023-05-23T16:05:14.294575+00:00", + "document_name": "InactiveDocument.pdf", + "media_type_id": 6, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019993", + "document_hash": "", + "document_content": "", + "document_status_id": 3 + }, + { + "id": "e020787d-1e04-4c0b-9c06-bd1cd44724b2", + "date_created": "2023-01-04T16:05:14.294575+00:00", + "document_name": "Default_App_Image.png", + "media_type_id": 3, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019993", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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", + "document_status_id": 2 + }, + { + "id": "9685f744-9d90-4102-a949-fcd0bb86f954", + "date_created": "2023-01-04T16:05:14.294575+00:00", + "document_name": "Default_App_Image.png", + "media_type_id": 3, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019993", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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", + "document_status_id": 2 + }, + { + "id": "88793f9f-c5a4-4621-847b-3d47cd839283", + "date_created": "2023-01-04T16:05:14.294575+00:00", + "document_name": "Test.pdf", + "media_type_id": 6, + "document_type_id": 1, + "company_user_id": "ac1cf001-7fbc-1f2f-817f-bce058019993", + "document_hash": "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==", + "document_content": "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", + "document_status_id": 2 + } +] \ No newline at end of file diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/process_steps.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/process_steps.test.json new file mode 100644 index 00000000..5b84489a --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/process_steps.test.json @@ -0,0 +1,26 @@ +[ + { + "id": "5c9a4f56-0609-49a5-ab86-dd8f93dfd3fa", + "process_step_type_id": 1, + "process_step_status_id": 2, + "process_id": "dd371565-9489-4907-a2e4-b8cbfe7a8cd2", + "date_created": "2023-02-21 08:15:20.479000 +00:00", + "date_last_changed": null + }, + { + "id": "629cc08f-bb0b-43ba-b20a-45c3538789b5", + "process_step_type_id": 2, + "process_step_status_id": 2, + "process_id": "dd371565-9489-4907-a2e4-b8cbfe7a8cd2", + "date_created": "2023-02-21 08:15:20.479000 +00:00", + "date_last_changed": null + }, + { + "id": "cd231cb8-55de-4ae4-b93f-d440512341fb", + "process_step_type_id": 3, + "process_step_status_id": 1, + "process_id": "dd371565-9489-4907-a2e4-b8cbfe7a8cd2", + "date_created": "2023-02-21 08:15:20.479000 +00:00", + "date_last_changed": null + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/processes.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/processes.test.json new file mode 100644 index 00000000..4ed3254e --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/processes.test.json @@ -0,0 +1,8 @@ +[ + { + "id": "dd371565-9489-4907-a2e4-b8cbfe7a8cd2", + "process_type_id" : 1, + "lock_expiry_date" : "2023-03-01 00:00:00.000000 +00:00", + "version" : "deadbeef-dead-beef-dead-beefdeadbeef" + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_external_type_detail_versions.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_external_type_detail_versions.test.json new file mode 100644 index 00000000..02688638 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_external_type_detail_versions.test.json @@ -0,0 +1,34 @@ +[ + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d562", + "verified_credential_external_type_id": 3, + "version": "1.0.0", + "template": "https://catena-x.net/.._Policies_Trace_3.0_EN.pdf", + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-09-30 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d563", + "verified_credential_external_type_id": 1, + "version": "2.0.0", + "template": "https://catena-x.net/.._Policies_Trace_3.0_EN.pdf", + "valid_from": "2023-06-01 00:00:00.000000 +00:00", + "expiry": "2023-12-23 00:00:00.000000 +00:00" + }, + { + "id": "1268a76a-ca19-4dd8-b932-01f24071d564", + "verified_credential_external_type_id": 1, + "version": "3.0.0", + "template": "https://catena-x.net/.._Policies_Trace_3.0_EN.pdf", + "valid_from": "2024-01-01 00:00:00.000000 +00:00", + "expiry": "2024-12-31 00:00:00.000000 +00:00" + }, + { + "id": "df3bd7d2-3349-410b-9b30-9a5238eb605e", + "verified_credential_external_type_id": 4, + "version": null, + "template": null, + "valid_from": "2024-01-01 00:00:00.000000 +00:00", + "expiry": "2999-12-31 23:59:59.000000 +00:00" + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_type_assigned_use_cases.test.json b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_type_assigned_use_cases.test.json new file mode 100644 index 00000000..b66a8edc --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Seeder/Data/verified_credential_type_assigned_use_cases.test.json @@ -0,0 +1,10 @@ +[ + { + "company_credential_detail_id": "9f5b9934-4014-4099-91e9-7b1aee696b03", + "use_case_id": "06b243a4-ba51-4bf3-bc40-5d79a2231b86" + }, + { + "company_credential_detail_id": "9f5b9934-4014-4099-91e9-7b1aee696b04", + "use_case_id": "06b243a4-ba51-4bf3-bc40-5d79a2231b87" + } +] diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/Setup/TestDbFixture.cs b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Setup/TestDbFixture.cs new file mode 100644 index 00000000..868a6153 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/Setup/TestDbFixture.cs @@ -0,0 +1,114 @@ +/******************************************************************************** + * 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; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Seeder; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; +using Testcontainers.PostgreSql; +using Xunit; +using Xunit.Extensions.AssemblyFixture; + +[assembly: TestFramework(AssemblyFixtureFramework.TypeName, AssemblyFixtureFramework.AssemblyName)] +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests.Setup; + +public class TestDbFixture : IAsyncLifetime +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() + .WithDatabase("test_db") + .WithImage("postgres") + .WithCleanUp(true) + .WithName(Guid.NewGuid().ToString()) + .Build(); + + /// + /// Foreach test a new portalDbContext will be created and filled with the custom seeding data. + /// + /// + /// In this method the migrations don't need to get executed since they are already on the testcontainer. + /// Because of that the EnsureCreatedAsync is enough. + /// + /// the datetime provider + /// Additional data for the database + /// Returns the created PortalDbContext + public async Task GetDbContext(IDateTimeProvider? dateTimeProvider = null, params Action[] seedActions) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseNpgsql( + _container.GetConnectionString(), + x => x.MigrationsAssembly(typeof(BatchInsertSeeder).Assembly.GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_portal") + ); + var context = new IssuerDbContext(optionsBuilder.Options, new AuditHandlerV1(new FakeIdentityIdService(), dateTimeProvider ?? new UtcDateTimeProvider())); + await context.Database.EnsureCreatedAsync().ConfigureAwait(false); + foreach (var seedAction in seedActions) + { + seedAction.Invoke(context); + } + + await context.SaveChangesAsync().ConfigureAwait(false); + return context; + } + + /// + /// This method is used to initially setup the database and run all migrations + /// + public async Task InitializeAsync() + { + await _container.StartAsync() + .ConfigureAwait(false); + + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseNpgsql( + _container.GetConnectionString(), + x => x.MigrationsAssembly(typeof(BatchInsertSeeder).Assembly.GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_portal") + ); + var context = new IssuerDbContext(optionsBuilder.Options, new AuditHandlerV1(new FakeIdentityIdService(), new UtcDateTimeProvider())); + await context.Database.MigrateAsync(); + + var seederOptions = Options.Create(new SeederSettings + { + TestDataEnvironments = new[] { "test" }, + DataPaths = new[] { "Seeder/Data" } + }); + var insertSeeder = new BatchInsertSeeder(context, + LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(), + seederOptions); + await insertSeeder.ExecuteAsync(CancellationToken.None); + var updateSeeder = new BatchUpdateSeeder(context, + LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger(), + seederOptions); + await updateSeeder.ExecuteAsync(CancellationToken.None); + } + + /// + public async Task DisposeAsync() + { + await _container.DisposeAsync() + .ConfigureAwait(false); + } +} diff --git a/tests/database/SsiCredentialIssuer.DbAccess.Tests/SsiCredentialIssuer.DbAccess.Tests.csproj b/tests/database/SsiCredentialIssuer.DbAccess.Tests/SsiCredentialIssuer.DbAccess.Tests.csproj new file mode 100644 index 00000000..ba3f11f3 --- /dev/null +++ b/tests/database/SsiCredentialIssuer.DbAccess.Tests/SsiCredentialIssuer.DbAccess.Tests.csproj @@ -0,0 +1,67 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.DbAccess.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + diff --git a/tests/externalservices/Portal.Service.Tests/Portal.Service.Tests.csproj b/tests/externalservices/Portal.Service.Tests/Portal.Service.Tests.csproj new file mode 100644 index 00000000..34f1ce9f --- /dev/null +++ b/tests/externalservices/Portal.Service.Tests/Portal.Service.Tests.csproj @@ -0,0 +1,60 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/externalservices/Portal.Service.Tests/PortalServiceTests.cs b/tests/externalservices/Portal.Service.Tests/PortalServiceTests.cs new file mode 100644 index 00000000..dbcc4142 --- /dev/null +++ b/tests/externalservices/Portal.Service.Tests/PortalServiceTests.cs @@ -0,0 +1,178 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Tests; + +public class PortalServiceTests +{ + #region Initialization + + private readonly ITokenService _tokenService; + private readonly IOptions _options; + + public PortalServiceTests() + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _options = Options.Create(new PortalSettings + { + Password = "passWord", + Scope = "test", + Username = "user@name", + BaseAddress = "https://base.address.com", + ClientId = "CatenaX", + ClientSecret = "pass@Secret", + GrantType = "cred", + TokenAddress = "https://example.org/token" + }); + _tokenService = A.Fake(); + } + + #endregion + + #region AddNotification + + [Fact] + public async Task AddNotification_WithValid_DoesNotThrowException() + { + // Arrange + const string bpn = "123"; + var httpMessageHandlerMock = + new HttpMessageHandlerMock(HttpStatusCode.OK); + using var httpClient = new HttpClient(httpMessageHandlerMock); + httpClient.BaseAddress = new Uri("https://base.address.com"); + A.CallTo(() => _tokenService.GetAuthorizedClient(_options.Value, A._)) + .Returns(httpClient); + var sut = new PortalService(_tokenService, _options); + + // Act + await sut.AddNotification("Test", bpn, NotificationTypeId.CREDENTIAL_APPROVAL, CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(NotificationRequest) && + ((x.Content as JsonContent)!.Value as NotificationRequest)!.Content == "Test" && + ((x.Content as JsonContent)!.Value as NotificationRequest)!.Bpnl == bpn && + ((x.Content as JsonContent)!.Value as NotificationRequest)!.NotificationTypeId == NotificationTypeId.CREDENTIAL_APPROVAL + ); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system notification failed with statuscode 409")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system notification failed with statuscode 400")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system notification failed with statuscode 400")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system notification failed with statuscode 403")] + public async Task AddNotification_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + const string bpn = "123"; + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + using var httpClient = new HttpClient(httpMessageHandlerMock); + httpClient.BaseAddress = new Uri("https://base.address.com"); + A.CallTo(() => _tokenService.GetAuthorizedClient(_options.Value, A._)).Returns(httpClient); + var sut = new PortalService(_tokenService, _options); + + // Act + async Task Act() => await sut.AddNotification("Test", bpn, NotificationTypeId.CREDENTIAL_APPROVAL, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion + + #region TriggerMail + + [Fact] + public async Task TriggerMail_WithValid_DoesNotThrowException() + { + // Arrange + const string bpn = "123"; + var httpMessageHandlerMock = + new HttpMessageHandlerMock(HttpStatusCode.OK); + using var httpClient = new HttpClient(httpMessageHandlerMock); + httpClient.BaseAddress = new Uri("https://base.address.com"); + A.CallTo(() => _tokenService.GetAuthorizedClient(_options.Value, A._)) + .Returns(httpClient); + var sut = new PortalService(_tokenService, _options); + + // Act + await sut.TriggerMail("Test", bpn, new Dictionary(), CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(MailData) && + ((x.Content as JsonContent)!.Value as MailData)!.Template == "Test" && + ((x.Content as JsonContent)!.Value as MailData)!.Bpnl == bpn + ); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system mail failed with statuscode 409")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system mail failed with statuscode 400")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system mail failed with statuscode 400")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system mail failed with statuscode 403")] + public async Task TriggerMail_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + const string bpn = "123"; + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + using var httpClient = new HttpClient(httpMessageHandlerMock); + httpClient.BaseAddress = new Uri("https://base.address.com"); + A.CallTo(() => _tokenService.GetAuthorizedClient(_options.Value, A._)).Returns(httpClient); + var sut = new PortalService(_tokenService, _options); + + // Act + async Task Act() => await sut.TriggerMail("Test", bpn, new Dictionary(), CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion +} diff --git a/tests/externalservices/Wallet.Service.Tests/BusinessLogic/WalletBusinessLogicTests.cs b/tests/externalservices/Wallet.Service.Tests/BusinessLogic/WalletBusinessLogicTests.cs new file mode 100644 index 00000000..359d9b71 --- /dev/null +++ b/tests/externalservices/Wallet.Service.Tests/BusinessLogic/WalletBusinessLogicTests.cs @@ -0,0 +1,236 @@ +using AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Encryption; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Security.Cryptography; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Tests.BusinessLogic; + +public class WalletBusinessLogicTests +{ + private readonly WalletBusinessLogic _sut; + private readonly IWalletService _walletService; + private readonly ICompanySsiDetailsRepository _companySsiDetailRepository; + private readonly IDocumentRepository _documentRepository; + private readonly EncryptionModeConfig _encryptionModeConfig; + + public WalletBusinessLogicTests() + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _encryptionModeConfig = new EncryptionModeConfig + { + Index = 0, + CipherMode = CipherMode.ECB, + PaddingMode = PaddingMode.PKCS7, + EncryptionKey = "202048656c6c6f20202048656c6c6f20202048656c6c6f20202048656c6c6f20" + }; + var options = Options.Create(new WalletSettings + { + BaseAddress = "https://base.address.com", + ClientId = "CatenaX", + ClientSecret = "pass@Secret", + TokenAddress = "https://example.org/token", + EncryptionConfigs = Enumerable.Repeat(_encryptionModeConfig, 1), + EncrptionConfigIndex = 0 + }); + _walletService = A.Fake(); + var issuerRepositories = A.Fake(); + _companySsiDetailRepository = A.Fake(); + _documentRepository = A.Fake(); + A.CallTo(() => issuerRepositories.GetInstance()) + .Returns(_companySsiDetailRepository); + A.CallTo(() => issuerRepositories.GetInstance()) + .Returns(_documentRepository); + + _sut = new WalletBusinessLogic(_walletService, issuerRepositories, options); + } + + #region CreateCredential + + [Fact] + public async Task CreateCredential_CallsExpected() + { + // Arrange + var id = Guid.NewGuid(); + var externalId = Guid.NewGuid(); + var schema = JsonDocument.Parse("{}"); + var ssiDetail = new CompanySsiDetail(id, null!, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, CompanySsiDetailStatusId.ACTIVE, Guid.NewGuid(), DateTimeOffset.UtcNow); + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyCompanySsiDetails(A._, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action setupOptionalFields) => + { + initialize?.Invoke(ssiDetail); + setupOptionalFields(ssiDetail); + }); + A.CallTo(() => _walletService.CreateCredential(schema, A._)) + .Returns(externalId); + + // Act + await _sut.CreateCredential(id, schema, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyCompanySsiDetails(id, A>._, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _walletService.CreateCredential(schema, A._)) + .MustHaveHappenedOnceExactly(); + ssiDetail.ExternalCredentialId = externalId; + } + + #endregion + + #region SignCredential + + [Fact] + public async Task SignCredential_CallsExpected() + { + // Arrange + var id = Guid.NewGuid(); + var credentialId = Guid.NewGuid(); + var ssiDetail = new CompanySsiDetail(id, null!, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, CompanySsiDetailStatusId.ACTIVE, Guid.NewGuid(), DateTimeOffset.UtcNow); + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyCompanySsiDetails(A._, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action setupOptionalFields) => + { + initialize?.Invoke(ssiDetail); + setupOptionalFields(ssiDetail); + }); + A.CallTo(() => _walletService.SignCredential(credentialId, A._)) + .Returns("cred"); + + // Act + await _sut.SignCredential(id, credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyCompanySsiDetails(id, A>._, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _walletService.SignCredential(credentialId, A._)) + .MustHaveHappenedOnceExactly(); + ssiDetail.Credential.Should().Be("cred"); + } + + #endregion + + #region CreateCredentialForHolder + + [Fact] + public async Task CreateCredentialForHolder_CallsExpected() + { + // Arrange + var id = Guid.NewGuid(); + var processData = new CompanySsiProcessData(id, null!, VerifiedCredentialTypeKindId.BPN) { ClientId = "123" }; + var (secret, vector) = CryptoHelper.Encrypt("test", Convert.FromHexString(_encryptionModeConfig.EncryptionKey), _encryptionModeConfig.CipherMode, _encryptionModeConfig.PaddingMode); + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyProcessData(A._, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action setupOptionalFields) => + { + initialize?.Invoke(processData); + setupOptionalFields(processData); + }); + + // Act + await _sut.CreateCredentialForHolder(id, "https://example.org/wallet", "test1", new EncryptionInformation(secret, vector, 0), "thisisatestsecret", CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _companySsiDetailRepository.AttachAndModifyProcessData(id, A>._, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _walletService.CreateCredentialForHolder(A._, A._, A._, A._, A._)) + .MustHaveHappenedOnceExactly(); + processData.ClientId.Should().BeNull(); + processData.ClientSecret.Should().BeNull(); + } + + #endregion + + #region GetCredential + + [Fact] + public async Task GetCredential_CallsExpected() + { + // Arrange + const string data = """ + { + "id": "2e70ee49-5fae-438a-9435-0cce3854650d", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/catenax/credentials/v1.0.0" + ], + "type": [ + "VerifiableCredential", + "BpnCredential" + ], + "issuanceDate": "2022-06-16T18:56:59Z", + "expirationDate": "2022-06-16T18:56:59Z", + "issuer": "2e70ee49-5fae-438a-9435-0cce3854650d", + "credentialSubject": { + "id": "2e70ee49-5fae-438a-9435-0cce3854650d", + "holderIdentifier": "2e70ee49-5fae-438a-9435-0cce3854650d", + "bpn": "2e70ee49-5fae-438a-9435-0cce3854650d" + } + } + """; + var id = Guid.NewGuid(); + var credentialId = Guid.NewGuid(); + var jsonDocument = JsonDocument.Parse(data); + A.CallTo(() => _walletService.GetCredential(credentialId, A._)) + .Returns(jsonDocument); + + // Act + await _sut.GetCredential(id, credentialId, VerifiedCredentialTypeKindId.BPN, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _documentRepository.CreateDocument(A._, A._, A._, MediaTypeId.JSON, DocumentTypeId.VERIFIED_CREDENTIAL, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _documentRepository.AssignDocumentToCompanySsiDetails(A._, id)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task GetCredential_WithSchemaNotMatching_CallsExpected() + { + // Arrange + const string data = """ + { + "id": "2e70ee49-5fae-438a-9435-0cce3854650d", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/catenax/credentials/v1.0.0" + ], + "expirationDate": "2022-06-16T18:56:59Z", + "credentialSubject": { + "id": "2e70ee49-5fae-438a-9435-0cce3854650d", + "holderIdentifier": "2e70ee49-5fae-438a-9435-0cce3854650d", + "bpn": "2e70ee49-5fae-438a-9435-0cce3854650d" + } + } + """; + var id = Guid.NewGuid(); + var credentialId = Guid.NewGuid(); + var jsonDocument = JsonDocument.Parse(data); + A.CallTo(() => _walletService.GetCredential(credentialId, A._)) + .Returns(jsonDocument); + async Task Act() => await _sut.GetCredential(id, credentialId, VerifiedCredentialTypeKindId.BPN, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be("Invalid schema for type BPN"); + } + + #endregion +} diff --git a/tests/externalservices/Wallet.Service.Tests/Services/BasicAuthTokenServiceTests.cs b/tests/externalservices/Wallet.Service.Tests/Services/BasicAuthTokenServiceTests.cs new file mode 100644 index 00000000..37117803 --- /dev/null +++ b/tests/externalservices/Wallet.Service.Tests/Services/BasicAuthTokenServiceTests.cs @@ -0,0 +1,127 @@ +/******************************************************************************** + * 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 AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Token; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared.Extensions; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Net; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Tests.Services; + +public class BasicAuthTokenServiceTests +{ + private readonly string _accessToken; + private readonly CancellationToken _cancellationToken; + private readonly TestException _testException; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IFixture _fixture; + private readonly Uri _validBaseAddress = new("https://validurl.com"); + + public BasicAuthTokenServiceTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _accessToken = _fixture.Create(); + _cancellationToken = new CancellationToken(); + _testException = _fixture.Create(); + + _httpClientFactory = A.Fake(); + } + + #region GetAuthorizedClient + + [Fact] + public async Task GetAuthorizedClient_Success() + { + var authResponse = JsonSerializer.Serialize(_fixture.Build().With(x => x.AccessToken, _accessToken).Create()); + SetupForGetAuthorized(new HttpMessageHandlerMock(HttpStatusCode.OK, authResponse.ToFormContent("application/json"))); + + var settings = _fixture.Create(); + + var sut = new BasicAuthTokenService(_httpClientFactory); + + var result = await sut.GetBasicAuthorizedClient(settings, _cancellationToken).ConfigureAwait(false); + + result.Should().NotBeNull(); + result.BaseAddress.Should().Be(_validBaseAddress); + } + + [Fact] + public async Task GetAuthorizedClient_HttpClientThrows_Throws() + { + SetupForGetAuthorized(new HttpMessageHandlerMock(HttpStatusCode.InternalServerError, ex: _testException)); + var settings = _fixture.Create(); + + var sut = new BasicAuthTokenService(_httpClientFactory); + + var act = () => sut.GetBasicAuthorizedClient(settings, _cancellationToken); + + var error = await Assert.ThrowsAsync(act).ConfigureAwait(false); + + error.Should().NotBeNull(); + error.InnerException.Should().Be(_testException); + error.Message.Should().Be("call to external system token-post failed"); + } + + #endregion + + #region Setup + + private void SetupForGetAuthorized(HttpMessageHandler httpMessageHandler) + { + var httpClientAuth = new HttpClient(httpMessageHandler) + { + BaseAddress = _fixture.Create() + }; + var httpClient = new HttpClient(httpMessageHandler) + { + BaseAddress = _validBaseAddress + }; + A.CallTo(() => _httpClientFactory.CreateClient($"{typeof(T).Name}Auth")) + .Returns(httpClientAuth); + A.CallTo(() => _httpClientFactory.CreateClient(typeof(T).Name)) + .Returns(httpClient); + } + + #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/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs b/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs new file mode 100644 index 00000000..7c72d5ca --- /dev/null +++ b/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs @@ -0,0 +1,274 @@ +using AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Json.More; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Tests.Services; + +public class WalletServiceTests +{ + private readonly WalletService _sut; + private readonly IBasicAuthTokenService _basicAuthTokenService; + private readonly IOptions _options; + + public WalletServiceTests() + { + var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _basicAuthTokenService = A.Fake(); + + _options = Options.Create(new WalletSettings + { + BaseAddress = "https://base.address.com", + ClientId = "CatenaX", + ClientSecret = "pass@Secret", + TokenAddress = "https://example.org/token", + EncrptionConfigIndex = 0 + }); + _sut = new WalletService(_basicAuthTokenService, _options); + } + + #region CreateCredential + + [Fact] + public async Task CreateCredential_WithValid_DoesNotThrowException() + { + // Arrange + var payload = JsonDocument.Parse("{}"); + var id = Guid.NewGuid(); + var response = new CreateCredentialResponse(id); + var httpMessageHandlerMock = new HttpMessageHandlerMock(HttpStatusCode.OK, new StringContent(JsonSerializer.Serialize(response))); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)) + .Returns(httpClient); + + // Act + var result = await _sut.CreateCredential(payload, CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(CreateCredentialRequest) && + ((x.Content as JsonContent)!.Value as CreateCredentialRequest)!.Application == "catena-x-portal" + ); + result.Should().Be(id); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system create-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system create-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system create-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system create-credential failed with statuscode 403")] + public async Task CreateCredential_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + var payload = JsonDocument.Parse("{}"); + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)).Returns(httpClient); + + // Act + async Task Act() => await _sut.CreateCredential(payload, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion + + #region SignCredential + + [Fact] + public async Task SignCredential_WithValid_DoesNotThrowException() + { + // Arrange + var credentialId = Guid.NewGuid(); + const string jwt = "thisisonlyatestexample"; + var response = new SignCredentialResponse(jwt); + var httpMessageHandlerMock = new HttpMessageHandlerMock(HttpStatusCode.OK, new StringContent(JsonSerializer.Serialize(response))); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)) + .Returns(httpClient); + + // Act + var result = await _sut.SignCredential(credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(SignCredentialRequest) && + ((x.Content as JsonContent)!.Value as SignCredentialRequest)!.Payload.Sign.ProofMechanism == "external" && + ((x.Content as JsonContent)!.Value as SignCredentialRequest)!.Payload.Sign.ProofType == "jwt" + ); + result.Should().Be(jwt); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system sign-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system sign-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system sign-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system sign-credential failed with statuscode 403")] + public async Task SignCredential_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + var credentialId = Guid.NewGuid(); + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)).Returns(httpClient); + + // Act + async Task Act() => await _sut.SignCredential(credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion + + #region GetCredential + + [Fact] + public async Task GetCredential_WithValid_DoesNotThrowException() + { + // Arrange + var credentialId = Guid.NewGuid(); + var json = """ + { + "root": "123" + } + """; + var response = new GetCredentialResponse("test", JsonDocument.Parse(json), "test123"); + var httpMessageHandlerMock = new HttpMessageHandlerMock(HttpStatusCode.OK, new StringContent(JsonSerializer.Serialize(response))); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)) + .Returns(httpClient); + + // Act + var result = await _sut.GetCredential(credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + result.RootElement.ToJsonString().Should().Be("{\"root\":\"123\"}"); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system get-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system get-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system get-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system get-credential failed with statuscode 403")] + public async Task GetCredential_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + var credentialId = Guid.NewGuid(); + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(_options.Value, A._)).Returns(httpClient); + + // Act + async Task Act() => await _sut.GetCredential(credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion + + #region CreateCredentialForHolder + + [Fact] + public async Task CreateCredentialForHolder_WithValid_DoesNotThrowException() + { + // Arrange + var id = Guid.NewGuid(); + var response = new CreateCredentialResponse(id); + var httpMessageHandlerMock = new HttpMessageHandlerMock(HttpStatusCode.OK, new StringContent(JsonSerializer.Serialize(response))); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(A._, A._)) + .Returns(httpClient); + + // Act + var result = await _sut.CreateCredentialForHolder("https://example.org", "test", "testSec", "testCred", CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(DeriveCredentialData) && + ((x.Content as JsonContent)!.Value as DeriveCredentialData)!.Application == "catena-x-portal" + ); + result.Should().Be(id); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system create-holder-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system create-holder-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system create-holder-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system create-holder-credential failed with statuscode 403")] + public async Task CreateCredentialForHolder_WithConflict_ThrowsServiceExceptionWithErrorContent(HttpStatusCode statusCode, string? content, string message) + { + // Arrange + var httpMessageHandlerMock = content == null + ? new HttpMessageHandlerMock(statusCode) + : new HttpMessageHandlerMock(statusCode, new StringContent(content)); + var httpClient = new HttpClient(httpMessageHandlerMock) + { + BaseAddress = new Uri("https://base.address.com") + }; + A.CallTo(() => _basicAuthTokenService.GetBasicAuthorizedClient(A._, A._)).Returns(httpClient); + + // Act + async Task Act() => await _sut.CreateCredentialForHolder("https://example.org", "test", "testSec", "testCred", CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion +} diff --git a/tests/externalservices/Wallet.Service.Tests/Wallet.Service.Tests.csproj b/tests/externalservices/Wallet.Service.Tests/Wallet.Service.Tests.csproj new file mode 100644 index 00000000..8334c270 --- /dev/null +++ b/tests/externalservices/Wallet.Service.Tests/Wallet.Service.Tests.csproj @@ -0,0 +1,61 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Authentication/KeycloakClaimsTransformationTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Authentication/KeycloakClaimsTransformationTests.cs new file mode 100644 index 00000000..45a7a059 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Authentication/KeycloakClaimsTransformationTests.cs @@ -0,0 +1,97 @@ +/******************************************************************************** + * 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.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Authentication; +using System.Security.Claims; +using System.Text.Json; +using IssuerClaims = Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Authentication; + +public class KeycloakClaimsTransformationTests +{ + private readonly KeycloakClaimsTransformation _sut; + + public KeycloakClaimsTransformationTests() + { + var options = Options.Create(new JwtBearerOptions + { + TokenValidationParameters = new TokenValidationParameters + { + ValidAudience = "validAudience" + } + }); + _sut = new KeycloakClaimsTransformation(options); + } + + [Fact] + public async Task TransformAsync_WithoutRoles_ReturnsExpected() + { + // Arrange + var claims = new Claim[] + { + new(IssuerClaims.ClaimTypes.Bpn, "BPNL00001243TEST") + }; + var identity = new ClaimsIdentity(claims); + var principal = new ClaimsPrincipal(identity); + + // Act + var result = await _sut.TransformAsync(principal).ConfigureAwait(false); + + // Assert + result.Claims.Should().ContainSingle() + .And.Satisfy(x => x.Type == IssuerClaims.ClaimTypes.Bpn && x.Value == "BPNL00001243TEST"); + } + + [Fact] + public async Task TransformAsync_WithRoles_ReturnsExpected() + { + // Arrange + var json = JsonSerializer.Serialize(new { validAudience = new { roles = Enumerable.Repeat("testRole", 1) } }); + var identity = new ClaimsIdentity(Enumerable.Repeat(new Claim(CustomClaimTypes.ResourceAccess, json, "JSON"), 1)); + var principal = new ClaimsPrincipal(identity); + + // Act + var result = await _sut.TransformAsync(principal).ConfigureAwait(false); + + // Assert + result.Claims.Should().HaveCount(2).And.Satisfy( + x => x.Type == CustomClaimTypes.ResourceAccess && x.Value == json, + x => x.Type == ClaimTypes.Role && x.Value == "testRole"); + } + + [Fact] + public async Task TransformAsync_WithIntRole_ReturnsExpected() + { + // Arrange + var json = JsonSerializer.Serialize(new { validAudience = new { roles = Enumerable.Repeat(1, 1) } }); + var identity = new ClaimsIdentity(Enumerable.Repeat(new Claim(CustomClaimTypes.ResourceAccess, json, "JSON"), 1)); + var principal = new ClaimsPrincipal(identity); + + // Act + var result = await _sut.TransformAsync(principal).ConfigureAwait(false); + + // Assert + result.Claims.Should().ContainSingle() + .And.Satisfy(x => x.Type == CustomClaimTypes.ResourceAccess && x.Value == json); + } +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs new file mode 100644 index 00000000..1ad7d06d --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/BusinessLogic/CredentialBusinessLogicTests.cs @@ -0,0 +1,694 @@ +/******************************************************************************** + * 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.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.DateTimeProvider; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; +using System.Net; +using System.Security.Cryptography; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.BusinessLogic; + +public class CredentialBusinessLogicTests +{ + private static readonly Guid CredentialId = Guid.NewGuid(); + private static readonly string Bpnl = "BPNL00000001TEST"; + + private readonly IFixture _fixture; + private readonly ICompanySsiDetailsRepository _companySsiDetailsRepository; + private readonly IDocumentRepository _documentRepository; + private readonly IProcessStepRepository _processStepRepository; + + private readonly ICredentialBusinessLogic _sut; + private readonly IIdentityService _identityService; + private readonly IDateTimeProvider _dateTimeProvider; + private readonly IHttpClientFactory _clientFactory; + private readonly IPortalService _portalService; + private readonly IIssuerRepositories _issuerRepositories; + private readonly IIdentityData _identity; + + public CredentialBusinessLogicTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _issuerRepositories = A.Fake(); + _companySsiDetailsRepository = A.Fake(); + _documentRepository = A.Fake(); + _processStepRepository = A.Fake(); + _identity = A.Fake(); + + _identityService = A.Fake(); + _dateTimeProvider = A.Fake(); + _clientFactory = A.Fake(); + _portalService = A.Fake(); + + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_companySsiDetailsRepository); + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_documentRepository); + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_processStepRepository); + + A.CallTo(() => _identity.IdentityId).Returns(Guid.NewGuid()); + A.CallTo(() => _identity.Bpnl).Returns(Bpnl); + A.CallTo(() => _identityService.IdentityData).Returns(_identity); + + var options = A.Fake>(); + A.CallTo(() => options.Value).Returns(new CredentialSettings + { + EncryptionConfigs = Enumerable.Repeat(new EncryptionModeConfig + { + Index = 0, + CipherMode = CipherMode.ECB, + PaddingMode = PaddingMode.PKCS7, + EncryptionKey = "zlWxjv54PrNDbjYx7d3m4nz88qmCHG0AhYwu0UYSFGTo9psPbcVsNiqr14zhRgSd" + }, 1), + MaxPageSize = 15, + IssuerDid = "did:web:example:org:bpn:18273z682734rt", + EncrptionConfigIndex = 0 + }); + + _sut = new CredentialBusinessLogic(_issuerRepositories, _identityService, _dateTimeProvider, _clientFactory, _portalService, options); + } + + #region GetUseCaseParticipationAsync + + [Fact] + public async Task GetUseCaseParticipationAsync_ReturnsExpected() + { + // Arrange + Setup_GetUseCaseParticipationAsync(); + + // Act + var result = await _sut.GetUseCaseParticipationAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(5); + } + + #endregion + + #region GetSsiCertificatesAsync + + [Fact] + public async Task GetSsiCertificatesAsync_ReturnsExpected() + { + // Arrange + Setup_GetSsiCertificatesAsync(); + + // Act + var result = await _sut.GetSsiCertificatesAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(5); + } + + #endregion + + #region ApproveCredential + + [Fact] + public async Task ApproveCredential_WithoutExistingSsiDetail_ThrowsNotFoundException() + { + // Arrange + var notExistingId = Guid.NewGuid(); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(notExistingId)) + .Returns(new ValueTuple()); + async Task Act() => await _sut.ApproveCredential(notExistingId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.SSI_DETAILS_NOT_FOUND.ToString()); + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Theory] + [InlineData(CompanySsiDetailStatusId.ACTIVE)] + [InlineData(CompanySsiDetailStatusId.INACTIVE)] + public async Task ApproveCredential_WithStatusNotPending_ThrowsConflictException(CompanySsiDetailStatusId statusId) + { + // Arrange + var alreadyActiveId = Guid.NewGuid(); + var approvalData = _fixture.Build() + .With(x => x.Status, statusId) + .Create(); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(alreadyActiveId)) + .Returns(new ValueTuple(true, approvalData)); + async Task Act() => await _sut.ApproveCredential(alreadyActiveId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.CREDENTIAL_NOT_PENDING.ToString()); + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ApproveCredential_WithBpnNotSetActiveSsiDetail_ThrowsConflictException() + { + // Arrange + var alreadyActiveId = Guid.NewGuid(); + var approvalData = _fixture.Build() + .With(x => x.Status, CompanySsiDetailStatusId.PENDING) + .With(x => x.Bpn, (string?)null) + .Create(); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(alreadyActiveId)) + .Returns(new ValueTuple(true, approvalData)); + async Task Act() => await _sut.ApproveCredential(alreadyActiveId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.BPN_NOT_SET.ToString()); + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ApproveCredential_WithExpiryInThePast_ReturnsExpected() + { + // Arrange + const VerifiedCredentialTypeId typeId = VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK; + var now = DateTimeOffset.Now; + var detailData = new DetailData( + VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL, + "test", + "1.0.0", + DateTimeOffset.Now.AddDays(-5) + ); + + var data = new SsiApprovalData( + CompanySsiDetailStatusId.PENDING, + typeId, + VerifiedCredentialTypeKindId.FRAMEWORK, + Bpnl, + detailData + ); + + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId)) + .Returns(new ValueTuple(true, data)); + async Task Act() => await _sut.ApproveCredential(CredentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.EXPIRY_DATE_IN_PAST.ToString()); + + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _portalService.AddNotification(A._, A._, A._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ApproveCredential_WithInvalidCredentialType_ThrowsException() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var useCaseData = new DetailData( + VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL, + "test", + "1.0.0", + DateTimeOffset.UtcNow + ); + + var data = new SsiApprovalData( + CompanySsiDetailStatusId.PENDING, + default, + VerifiedCredentialTypeKindId.FRAMEWORK, + Bpnl, + useCaseData + ); + + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId)) + .Returns(new ValueTuple(true, data)); + + // Act + async Task Act() => await _sut.ApproveCredential(CredentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + ex.Message.Should().Be(CompanyDataErrors.CREDENTIAL_TYPE_NOT_FOUND.ToString()); + } + + [Fact] + public async Task ApproveCredential_WithDetailVersionNotSet_ThrowsConflictException() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var data = new SsiApprovalData( + CompanySsiDetailStatusId.PENDING, + VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, + VerifiedCredentialTypeKindId.FRAMEWORK, + Bpnl, + null + ); + + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId)) + .Returns(new ValueTuple(true, data)); + async Task Act() => await _sut.ApproveCredential(CredentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + + A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.CREATE_CREDENTIAL)) + .MustNotHaveHappened(); + ex.Message.Should().Be(CompanyDataErrors.EXTERNAL_TYPE_DETAIL_ID_NOT_SET.ToString()); + } + + [Theory] + [InlineData(VerifiedCredentialTypeKindId.FRAMEWORK, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, VerifiedCredentialExternalTypeId.TRACEABILITY_CREDENTIAL)] + [InlineData(VerifiedCredentialTypeKindId.MEMBERSHIP, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, VerifiedCredentialExternalTypeId.VEHICLE_DISMANTLE)] + [InlineData(VerifiedCredentialTypeKindId.BPN, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, VerifiedCredentialExternalTypeId.BUSINESS_PARTNER_NUMBER)] + public async Task ApproveCredential_WithValid_ReturnsExpected(VerifiedCredentialTypeKindId kindId, VerifiedCredentialTypeId typeId, VerifiedCredentialExternalTypeId externalTypeId) + { + // Arrange + var now = DateTimeOffset.UtcNow; + var detailData = new DetailData( + externalTypeId, + "test", + "1.0.0", + DateTimeOffset.UtcNow + ); + + var data = new SsiApprovalData( + CompanySsiDetailStatusId.PENDING, + typeId, + kindId, + Bpnl, + detailData + ); + + var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, typeId, CompanySsiDetailStatusId.PENDING, Guid.NewGuid(), DateTimeOffset.Now); + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetSsiApprovalData(CredentialId)) + .Returns(new ValueTuple(true, data)); + A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A?>._, A>._!)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(detail); + updateFields.Invoke(detail); + }); + + // Act + await _sut.ApproveCredential(CredentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _portalService.AddNotification(A._, A._, NotificationTypeId.CREDENTIAL_APPROVAL, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.TriggerMail("CredentialApproval", A._, A>._, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.CREATE_CREDENTIAL)) + .MustHaveHappenedOnceExactly(); + + detail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.ACTIVE); + detail.DateLastChanged.Should().Be(now); + } + + #endregion + + #region RejectCredential + + [Fact] + public async Task RejectCredential_WithoutExistingSsiDetail_ThrowsNotFoundException() + { + // Arrange + var notExistingId = Guid.NewGuid(); + A.CallTo(() => _companySsiDetailsRepository.GetSsiRejectionData(notExistingId)) + .Returns(new ValueTuple()); + async Task Act() => await _sut.RejectCredential(notExistingId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.SSI_DETAILS_NOT_FOUND.ToString()); + A.CallTo(() => _portalService.TriggerMail("CredentialRejected", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Theory] + [InlineData(CompanySsiDetailStatusId.ACTIVE)] + [InlineData(CompanySsiDetailStatusId.INACTIVE)] + public async Task RejectCredential_WithNotPendingSsiDetail_ThrowsNotFoundException(CompanySsiDetailStatusId status) + { + // Arrange + var alreadyInactiveId = Guid.NewGuid(); + A.CallTo(() => _companySsiDetailsRepository.GetSsiRejectionData(alreadyInactiveId)) + .Returns(new ValueTuple(true, status, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)); + async Task Act() => await _sut.RejectCredential(alreadyInactiveId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.CREDENTIAL_NOT_PENDING.ToString()); + A.CallTo(() => _portalService.TriggerMail("CredentialRejected", A._, A>._, A._)).MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task RejectCredential_WithValidRequest_ReturnsExpected() + { + // Arrange + var now = DateTimeOffset.UtcNow; + var detail = new CompanySsiDetail(CredentialId, _identity.Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, CompanySsiDetailStatusId.PENDING, Guid.NewGuid(), DateTimeOffset.Now); + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + A.CallTo(() => _companySsiDetailsRepository.GetSsiRejectionData(CredentialId)) + .Returns(new ValueTuple(true, CompanySsiDetailStatusId.PENDING, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)); + A.CallTo(() => _companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(CredentialId, A?>._, A>._!)) + .Invokes((Guid _, Action? initialize, Action updateFields) => + { + initialize?.Invoke(detail); + updateFields.Invoke(detail); + }); + + // Act + await _sut.RejectCredential(CredentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _portalService.TriggerMail("CredentialRejected", A._, A>._, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _portalService.AddNotification(A._, A._, NotificationTypeId.CREDENTIAL_REJECTED, A._)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + + detail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.INACTIVE); + detail.DateLastChanged.Should().Be(now); + } + + #endregion + + #region GetCertificateTypes + + [Fact] + public async Task GetCertificateTypes_ReturnsExpected() + { + // Arrange + A.CallTo(() => _companySsiDetailsRepository.GetCertificateTypes(A._)) + .Returns(Enum.GetValues().ToAsyncEnumerable()); + + // Act + var result = await _sut.GetCertificateTypes().ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(7); + } + + #endregion + + #region CreateBpnCredential + + [Fact] + public async Task CreateBpnCredential_ReturnsExpected() + { + // Arrange + var didId = Guid.NewGuid().ToString(); + var didDocument = new DidDocument(didId); + var data = new CreateBpnCredentialRequest("https://example.org/holder/BPNL12343546", Bpnl, null); + HttpRequestMessage? request = null; + ConfigureHttpClientFactoryFixture(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(didDocument)) + }, requestMessage => request = requestMessage); + + // Act + await _sut.CreateBpnCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _documentRepository.CreateDocument("schema.json", A._, A._, MediaTypeId.JSON, DocumentTypeId.PRESENTATION, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(_identity.Bpnl, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _documentRepository.AssignDocumentToCompanySsiDetails(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(Bpnl, VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + } + + #endregion + + #region CreateMembershipCredential + + [Fact] + public async Task CreateMembershipCredential_ReturnsExpected() + { + // Arrange + var didId = Guid.NewGuid().ToString(); + var didDocument = new DidDocument(didId); + var data = new CreateMembershipCredentialRequest("https://example.org/holder/BPNL12343546", Bpnl, "Test", null); + HttpRequestMessage? request = null; + A.CallTo(() => _companySsiDetailsRepository.GetCertificateTypes(A._)) + .Returns(Enum.GetValues().ToAsyncEnumerable()); + ConfigureHttpClientFactoryFixture(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(didDocument)) + }, requestMessage => request = requestMessage); + + // Act + await _sut.CreateMembershipCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _documentRepository.CreateDocument("schema.json", A._, A._, MediaTypeId.JSON, DocumentTypeId.PRESENTATION, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(_identity.Bpnl, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _documentRepository.AssignDocumentToCompanySsiDetails(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(Bpnl, VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + } + + #endregion + + #region CreateFrameworkCredential + + [Fact] + public async Task CreateFrameworkCredential_WithVersionNotExisting_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>()); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.EXTERNAL_TYPE_DETAIL_NOT_FOUND.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_WithExpiryInPast_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, null, null, Enumerable.Empty(), now.AddDays(-5))); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.EXPIRY_DATE_IN_PAST.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_WithEmptyVersion_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, null, null, Enumerable.Empty(), now.AddDays(5))); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.EMPTY_VERSION.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_WithEmptyTemplate_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, "1.0.0", null, Enumerable.Empty(), now.AddDays(5))); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.EMPTY_TEMPLATE.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_WithMoreThanOneUseCase_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, "1.0.0", "https://example.org/tempalte", new[] { "test", "test" }, now.AddDays(5))); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.MULTIPLE_USE_CASES.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_WithNoUseCase_ThrowsControllerArgumentException() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("BPNL0012HOLDER", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, "1.0.0", "https://example.org/tempalte", Enumerable.Empty(), now.AddDays(5))); + async Task Act() => await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + ex.Message.Should().Be(CompanyDataErrors.MULTIPLE_USE_CASES.ToString()); + } + + [Fact] + public async Task CreateFrameworkCredential_ReturnsExpected() + { + // Arrange + var useCaseId = Guid.NewGuid(); + var didId = Guid.NewGuid().ToString(); + var didDocument = new DidDocument(didId); + var now = DateTimeOffset.Now; + A.CallTo(() => _dateTimeProvider.OffsetNow).Returns(now); + var data = new CreateFrameworkCredentialRequest("https://example.org/holder/BPNL12343546", Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, useCaseId, null); + HttpRequestMessage? request = null; + A.CallTo(() => _companySsiDetailsRepository.CheckCredentialTypeIdExistsForExternalTypeDetailVersionId(useCaseId, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK)) + .Returns(new ValueTuple, DateTimeOffset>(true, "1.0.0", "https://example.org/tempalte", Enumerable.Repeat("Test", 1), now.AddDays(5))); + ConfigureHttpClientFactoryFixture(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(didDocument)) + }, requestMessage => request = requestMessage); + + // Act + await _sut.CreateFrameworkCredential(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _documentRepository.CreateDocument("schema.json", A._, A._, MediaTypeId.JSON, DocumentTypeId.PRESENTATION, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(_identity.Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _documentRepository.AssignDocumentToCompanySsiDetails(A._, A._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _companySsiDetailsRepository.CreateSsiDetails(Bpnl, VerifiedCredentialTypeId.TRACEABILITY_FRAMEWORK, A._, CompanySsiDetailStatusId.PENDING, _identity.IdentityId, A>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); + } + + #endregion + + #region Setup + + private void Setup_GetUseCaseParticipationAsync() + { + var verifiedCredentials = _fixture.Build() + .With(x => x.SsiDetailData, _fixture.CreateMany(1)) + .CreateMany(5); + A.CallTo(() => _companySsiDetailsRepository.GetUseCaseParticipationForCompany(Bpnl, A._)) + .Returns(_fixture.Build().With(x => x.VerifiedCredentials, verifiedCredentials).CreateMany(5).ToAsyncEnumerable()); + } + + private void Setup_GetSsiCertificatesAsync() + { + A.CallTo(() => _companySsiDetailsRepository.GetSsiCertificates(Bpnl, A._)) + .Returns(_fixture.Build().With(x => x.Credentials, Enumerable.Repeat(new SsiCertificateExternalTypeDetailTransferData(_fixture.Create(), _fixture.CreateMany(1)), 1)).CreateMany(5).ToAsyncEnumerable()); + } + + private void ConfigureHttpClientFactoryFixture(HttpResponseMessage httpResponseMessage, Action? setMessage = null) + { + var messageHandler = A.Fake(); + A.CallTo(messageHandler) // mock protected method + .Where(x => x.Method.Name == "SendAsync") + .WithReturnType>() + .ReturnsLazily(call => + { + var message = call.Arguments.Get(0); + setMessage?.Invoke(message); + return Task.FromResult(httpResponseMessage); + }); + var httpClient = new HttpClient(messageHandler); + _fixture.Inject(httpClient); + + A.CallTo(() => _clientFactory.CreateClient("didDocumentDownload")).Returns(httpClient); + } + + #endregion +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/CredentialControllerTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/CredentialControllerTests.cs new file mode 100644 index 00000000..376e6d4e --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Controllers/CredentialControllerTests.cs @@ -0,0 +1,77 @@ +/******************************************************************************** + * 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.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Controllers; + +public class CredentialControllerTests : IClassFixture +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + Converters = { new JsonStringEnumConverter() }, + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase + }; + + private const string BaseUrl = "/api/issuer"; + private readonly HttpClient _client; + + public CredentialControllerTests(IntegrationTestFactory factory) + { + _client = factory.CreateClient(); + } + + #region GetCertificateTypes + + [Fact] + public async Task GetCertificateTypes() + { + // Act + var types = await _client.GetFromJsonAsync>($"{BaseUrl}/certificateTypes", JsonOptions).ConfigureAwait(false); + + // Assert + types.Should().NotBeNull().And.HaveCount(2).And.Satisfy( + x => x == VerifiedCredentialTypeId.DISMANTLER_CERTIFICATE, + x => x == VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER + ); + } + + #endregion + + #region Swagger + + [Fact] + public async Task CheckSwagger_ReturnsExpected() + { + // Act + var response = await _client.GetAsync($"{BaseUrl}/swagger/v1/swagger.json").ConfigureAwait(false); + + // Assert + response.Should().NotBeNull(); + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + #endregion +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Identity/MandatoryIdentityClaimHandlerTests.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Identity/MandatoryIdentityClaimHandlerTests.cs new file mode 100644 index 00000000..767614d6 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Identity/MandatoryIdentityClaimHandlerTests.cs @@ -0,0 +1,128 @@ +/******************************************************************************** + * 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.AspNetCore.Authorization; +using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Tests.Shared; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using System.Security.Claims; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Identity; + +public class MandatoryIdentityClaimHandlerTests +{ + private readonly IFixture _fixture; + private readonly IClaimsIdentityDataBuilder _claimsIdentityDataBuilder; + private readonly IMockLogger _mockLogger; + private readonly ILogger _logger; + private readonly string Bpnl = "BPNL000000001TEST"; + + public MandatoryIdentityClaimHandlerTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _claimsIdentityDataBuilder = new ClaimsIdentityDataBuilder(); + + _mockLogger = A.Fake>(); + _logger = new MockLogger(_mockLogger); + } + + [Fact] + public async Task HandleValidRequirement_WithoutUsername_ReturnsExpected() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity[] + { + new(new[] { new Claim("preferred_username", "00000000-0000-0000-0000-000000000000") }) + }); + + var context = new AuthorizationHandlerContext(Enumerable.Repeat(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity), 1), principal, null); + var sut = new MandatoryIdentityClaimHandler(_claimsIdentityDataBuilder, _logger); + + // Act + await sut.HandleAsync(context).ConfigureAwait(false); + + // Assert + context.HasSucceeded.Should().Be(false); + _claimsIdentityDataBuilder.Status.Should().Be(IClaimsIdentityDataBuilderStatus.Empty); + + Assert.Throws(() => _claimsIdentityDataBuilder.IdentityId); + Assert.Throws(() => _claimsIdentityDataBuilder.Bpnl); + A.CallTo(() => _mockLogger.Log(LogLevel.Information, A._, A._)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task HandleValidRequirement_WithUsernameWithoutBpn_ReturnsExpected() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity[] + { + new(new[] + { + new Claim("preferred_username", "eb4f6b1d-cde2-4e7b-86d5-e678421c0bd3"), + }) + }); + + var context = new AuthorizationHandlerContext(Enumerable.Repeat(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity), 1), principal, null); + var sut = new MandatoryIdentityClaimHandler(_claimsIdentityDataBuilder, _logger); + + // Act + await sut.HandleAsync(context).ConfigureAwait(false); + + // Assert + context.HasSucceeded.Should().Be(false); + _claimsIdentityDataBuilder.Status.Should().Be(IClaimsIdentityDataBuilderStatus.Empty); + + Assert.Throws(() => _claimsIdentityDataBuilder.Bpnl); + } + + [Fact] + public async Task HandleValidRequirement_WithAllSet_ReturnsExpected() + { + // Arrange + var principal = new ClaimsPrincipal(new ClaimsIdentity[] + { + new(new[] + { + new Claim("preferred_username", "eb4f6b1d-cde2-4e7b-86d5-e678421c0bd3"), + new Claim("bpn", Bpnl) + }) + }); + + var context = new AuthorizationHandlerContext(Enumerable.Repeat(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity), 1), principal, null); + var sut = new MandatoryIdentityClaimHandler(_claimsIdentityDataBuilder, _logger); + + // Act + await sut.HandleAsync(context).ConfigureAwait(false); + + // Assert + context.HasSucceeded.Should().Be(true); + _claimsIdentityDataBuilder.Status.Should().Be(IClaimsIdentityDataBuilderStatus.Complete); + + _claimsIdentityDataBuilder.IdentityId.Should().Be(new Guid("eb4f6b1d-cde2-4e7b-86d5-e678421c0bd3")); + _claimsIdentityDataBuilder.Bpnl.Should().Be(Bpnl); + A.CallTo(() => _mockLogger.Log(A._, A._, A._)) + .MustNotHaveHappened(); + } +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumerableStub.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumerableStub.cs new file mode 100644 index 00000000..cf86c8a8 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumerableStub.cs @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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.Linq.Expressions; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup +{ + public class AsyncEnumerableStub : EnumerableQuery, IAsyncEnumerable, IQueryable + { + public AsyncEnumerableStub(IEnumerable enumerable) + : base(enumerable) + { } + + public AsyncEnumerableStub(Expression expression) + : base(expression) + { } + + IQueryProvider IQueryable.Provider => new AsyncQueryProviderStub(this); + + public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + return new AsyncEnumeratorStub(this.AsEnumerable().GetEnumerator()); + } + } +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumeratorStub.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumeratorStub.cs new file mode 100644 index 00000000..a9a3c9d0 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncEnumeratorStub.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 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; + +public class AsyncEnumeratorStub : IAsyncEnumerator +{ + private readonly IEnumerator _inner; + + public AsyncEnumeratorStub(IEnumerator inner) + { + _inner = inner; + } + + public T Current => _inner.Current; + + public ValueTask MoveNextAsync() + { + return new ValueTask(_inner.MoveNext()); + } + + public ValueTask DisposeAsync() + { + _inner.Dispose(); + return ValueTask.CompletedTask; + } +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncQueryProviderStub.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncQueryProviderStub.cs new file mode 100644 index 00000000..8aa18bcc --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/AsyncQueryProviderStub.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 Microsoft.EntityFrameworkCore.Query; +using System.Linq.Expressions; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; + +public class AsyncQueryProviderStub : IAsyncQueryProvider +{ + private readonly IQueryProvider _inner; + + public AsyncQueryProviderStub(IQueryProvider inner) + { + _inner = inner; + } + + public IQueryable CreateQuery(Expression expression) + { + return new AsyncEnumerableStub(expression); + } + + public IQueryable CreateQuery(Expression expression) + { + return new AsyncEnumerableStub(expression); + } + + public object? Execute(Expression expression) + { + return _inner.Execute(expression); + } + + public TResult Execute(Expression expression) + { + return _inner.Execute(expression); + } + + public TResult ExecuteAsync(Expression expression, CancellationToken cancellationToken = default) + { + // https://stackoverflow.com/questions/57314896/iasyncqueryprovider-mock-issue-when-migrated-to-net-core-3-adding-tresult-iasyn + var expectedResultType = typeof(TResult).GetGenericArguments()[0]; + var executionResult = typeof(IQueryProvider) + .GetMethod( + name: nameof(IQueryProvider.Execute), + genericParameterCount: 1, + types: new[] { typeof(Expression) })? + .MakeGenericMethod(expectedResultType) + .Invoke(this, new[] { expression }); + + return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult)) + ?.MakeGenericMethod(expectedResultType) + .Invoke(null, new[] { executionResult })!; + } +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/FakePolicyEvaluator.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/FakePolicyEvaluator.cs new file mode 100644 index 00000000..53e5bc6b --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/FakePolicyEvaluator.cs @@ -0,0 +1,36 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 BMW Group AG + * Copyright (c) 2021, 2023 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.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Http; +using System.Security.Claims; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; + +public class FakePolicyEvaluator : IPolicyEvaluator +{ + public Task AuthenticateAsync(AuthorizationPolicy policy, HttpContext context) => + Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), new AuthenticationProperties(), "FakeScheme"))); + + public Task AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object? resource) => + Task.FromResult(PolicyAuthorizationResult.Success()); +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/IntegrationTestFactory.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/IntegrationTestFactory.cs new file mode 100644 index 00000000..e223e799 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Setup/IntegrationTestFactory.cs @@ -0,0 +1,139 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 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 Laraue.EfCoreTriggers.PostgreSql.Extensions; +using Microsoft.AspNetCore.Authorization.Policy; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Seeding; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Handler; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Auditing.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Migrations.Seeder; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Tests.Shared; +using System.Reflection; +using System.Text.Json.Serialization; +using Testcontainers.PostgreSql; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests.Setup; + +public class IntegrationTestFactory : WebApplicationFactory, IAsyncLifetime +{ + private readonly PostgreSqlContainer _container = new PostgreSqlBuilder() + .WithDatabase("test_db") + .WithImage("postgres") + .WithCleanUp(true) + .WithName(Guid.NewGuid().ToString()) + .Build(); + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + var projectDir = Directory.GetCurrentDirectory(); + var configPath = Path.Combine(projectDir, "appsettings.IntegrationTests.json"); + + builder.ConfigureAppConfiguration((_, conf) => + { + conf.AddJsonFile(configPath, true) + .AddUserSecrets(Assembly.GetExecutingAssembly(), true) + .AddEnvironmentVariables(); + }); + builder.ConfigureTestServices(services => + { + var identityService = services.SingleOrDefault(d => d.ServiceType.GetInterfaces().Contains(typeof(IIdentityService))); + if (identityService != null) + services.Remove(identityService); + services.AddScoped(); + services.AddScoped(); + + var identityIdService = services.SingleOrDefault(d => d.ServiceType.GetInterfaces().Contains(typeof(IIdentityIdService))); + if (identityIdService != null) + services.Remove(identityIdService); + services.AddScoped(); + + services.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + services.Configure(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); + if (descriptor != null) + services.Remove(descriptor); + + services.AddDbContext(options => + { + options.UseNpgsql(_container.GetConnectionString(), + x => x.MigrationsAssembly(typeof(BatchInsertSeeder).Assembly.GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_issuer")) + .UsePostgreSqlTriggers(); + }); + services.AddSingleton(); + }); + } + + /// + protected override IHost CreateHost(IHostBuilder builder) + { + builder.AddLogging(); + var host = base.CreateHost(builder); + + var optionsBuilder = new DbContextOptionsBuilder(); + + optionsBuilder.UseNpgsql( + _container.GetConnectionString(), + x => x.MigrationsAssembly(typeof(BatchInsertSeeder).Assembly.GetName().Name) + .MigrationsHistoryTable("__efmigrations_history_hub", "public") + ); + var context = new IssuerDbContext(optionsBuilder.Options, new NoAuditHandler()); + context.Database.Migrate(); + + var seederOptions = Options.Create(new SeederSettings + { + TestDataEnvironments = new[] { "test" }, + DataPaths = new[] { "Seeder/Data" } + }); + var insertSeeder = new BatchInsertSeeder(context, + LoggerFactory.Create(c => c.AddConsole()).CreateLogger(), + seederOptions); + insertSeeder.ExecuteAsync(CancellationToken.None).GetAwaiter().GetResult(); + var updateSeeder = new BatchUpdateSeeder(context, + LoggerFactory.Create(c => c.AddConsole()).CreateLogger(), + seederOptions); + updateSeeder.ExecuteAsync(CancellationToken.None).GetAwaiter().GetResult(); + return host; + } + + public async Task InitializeAsync() => await _container.StartAsync(); + + public new async Task DisposeAsync() => await _container.DisposeAsync(); +} diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/SsiCredentialIssuer.Service.Tests.csproj b/tests/issuer/SsiCredentialIssuer.Service.Tests/SsiCredentialIssuer.Service.Tests.csproj new file mode 100644 index 00000000..e5eba4b9 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/SsiCredentialIssuer.Service.Tests.csproj @@ -0,0 +1,67 @@ + + + + + Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + Always + + + + + + + + + + diff --git a/tests/issuer/SsiCredentialIssuer.Service.Tests/Usings.cs b/tests/issuer/SsiCredentialIssuer.Service.Tests/Usings.cs new file mode 100644 index 00000000..65016aec --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/Usings.cs @@ -0,0 +1,24 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 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/issuer/SsiCredentialIssuer.Service.Tests/appsettings.IntegrationTests.json b/tests/issuer/SsiCredentialIssuer.Service.Tests/appsettings.IntegrationTests.json new file mode 100644 index 00000000..107e7db0 --- /dev/null +++ b/tests/issuer/SsiCredentialIssuer.Service.Tests/appsettings.IntegrationTests.json @@ -0,0 +1,39 @@ +{ + "Serilog": { + "Using": [ "Serilog.Sinks.Console" ], + "MinimumLevel": { + "Default": "Debug" + }, + "WriteTo": [ + { "Name": "Console" } + ], + "Enrich": [ + "WithCorrelationId" + ], + "Properties": { + "Application": "PolicyHub" + } + }, + "SwaggerEnabled": true, + "HealthChecks": [], + "Cors": { + "AllowedOrigins": [] + }, + "ConnectionStrings": { + "PolicyHubDb": "Server=placeholder;Database=placeholder;Port=5432;User Id=placeholder;Password=;Ssl Mode=Disable;" + }, + "JwtBearerOptions": { + "RequireHttpsMetadata": true, + "MetadataAddress": "", + "SaveToken": true, + "TokenValidationParameters": { + "ValidateIssuer": true, + "ValidIssuer": "", + "ValidateIssuerSigningKey": true, + "ValidAudience": "", + "ValidateAudience": true, + "ValidateLifetime": true, + "ClockSkew": 600000 + } + } +} diff --git a/tests/processes/CredentialProcess.Library.Tests/CredentialProcess.Library.Tests.csproj b/tests/processes/CredentialProcess.Library.Tests/CredentialProcess.Library.Tests.csproj new file mode 100644 index 00000000..8ac25f57 --- /dev/null +++ b/tests/processes/CredentialProcess.Library.Tests/CredentialProcess.Library.Tests.csproj @@ -0,0 +1,48 @@ + + + + + net7.0 + enable + enable + false + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + diff --git a/tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs b/tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs new file mode 100644 index 00000000..c63cc3a2 --- /dev/null +++ b/tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs @@ -0,0 +1,245 @@ +using AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests; + +public class CredentialProcessHandlerTests +{ + private readonly Guid _credentialId = Guid.NewGuid(); + + private readonly IWalletBusinessLogic _walletBusinessLogic; + private readonly IIssuerRepositories _issuerRepositories; + private readonly ICredentialRepository _credentialRepository; + + private readonly CredentialProcessHandler _sut; + private readonly IFixture _fixture; + + public CredentialProcessHandlerTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _issuerRepositories = A.Fake(); + _credentialRepository = A.Fake(); + + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_credentialRepository); + + _walletBusinessLogic = A.Fake(); + + _sut = new CredentialProcessHandler(_issuerRepositories, _walletBusinessLogic); + } + + #region CreateCredential + + [Fact] + public async Task CreateCredential_WithValidData_ReturnsExpected() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialStorageInformationById(_credentialId)) + .Returns(new ValueTuple()); + + // Act + var result = await _sut.CreateCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _walletBusinessLogic.CreateCredential(_credentialId, 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.SIGN_CREDENTIAL); + } + + #endregion + + #region SignCredential + + [Fact] + public async Task SignCredential_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _credentialRepository.GetWalletCredentialId(_credentialId)) + .Returns((Guid?)null); + async Task Act() => await _sut.SignCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ExternalCredentialId must be set here"); + } + + [Fact] + public async Task SignCredential_WithValidData_ReturnsExpected() + { + // Arrange + var externalCredentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetWalletCredentialId(_credentialId)) + .Returns(externalCredentialId); + + // Act + var result = await _sut.SignCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _walletBusinessLogic.SignCredential(_credentialId, externalCredentialId, A._)) + .MustHaveHappenedOnceExactly(); + + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT); + } + + #endregion + + #region SaveCredentialDocument + + [Fact] + public async Task SaveCredentialDocument_WithNotExisting_ReturnsExpected() + { + // Arrange + A.CallTo(() => _credentialRepository.GetExternalCredentialAndKindId(_credentialId)) + .Returns(new ValueTuple()); + async Task Act() => await _sut.SaveCredentialDocument(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("ExternalCredentialId must be set here"); + } + + [Fact] + public async Task SaveCredentialDocument_WithValidData_ReturnsExpected() + { + // Arrange + var externalCredentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetExternalCredentialAndKindId(_credentialId)) + .Returns(new ValueTuple(externalCredentialId, VerifiedCredentialTypeKindId.BPN)); + + // Act + var result = await _sut.SaveCredentialDocument(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _walletBusinessLogic.GetCredential(_credentialId, externalCredentialId, VerifiedCredentialTypeKindId.BPN, 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_CREDENTIAL_FOR_HOLDER); + } + + #endregion + + #region CreateCredentialForHolder + + [Fact] + public async Task CreateCredentialForHolder_WithCredentialNotSet_SkipsStep() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialData(_credentialId)) + .Returns(new ValueTuple()); + async Task Act() => await _sut.CreateCredentialForHolder(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("Credential must be set here"); + } + + [Fact] + public async Task CreateCredentialForHolder_WithClientIdNull_SkipsStep() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialData(_credentialId)) + .Returns(new ValueTuple(new HolderWalletData(null, null), "test", _fixture.Create())); + + // Act + var result = await _sut.CreateCredentialForHolder(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.SKIPPED); + result.nextStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task CreateCredentialForHolder_WithWalletUrlNull_SkipsStep() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialData(_credentialId)) + .Returns(new ValueTuple(new HolderWalletData(null, "c1"), "test", _fixture.Create())); + + // Act + var result = await _sut.CreateCredentialForHolder(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.SKIPPED); + result.nextStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task CreateCredentialForHolder_WithEncryptionNotSet_SkipsStep() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialData(_credentialId)) + .Returns(new ValueTuple( + new HolderWalletData("https://example.org", "c1"), + "test", + new EncryptionTransformationData("test"u8.ToArray(), "test"u8.ToArray(), null))); + + // Act + var result = await _sut.CreateCredentialForHolder(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.SKIPPED); + result.nextStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task CreateCredentialForHolder_WithValidData_ReturnsExpected() + { + // Arrange + A.CallTo(() => _credentialRepository.GetCredentialData(_credentialId)) + .Returns(new ValueTuple( + new HolderWalletData("https://example.org", "c1"), + "test", + _fixture.Create())); + + // Act + var result = await _sut.CreateCredentialForHolder(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _walletBusinessLogic.CreateCredentialForHolder(_credentialId, "https://example.org", "c1", A._, "test", 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/CredentialProcess.Worker.Tests/CredentialProcess.Worker.Tests.csproj b/tests/processes/CredentialProcess.Worker.Tests/CredentialProcess.Worker.Tests.csproj new file mode 100644 index 00000000..09e4f541 --- /dev/null +++ b/tests/processes/CredentialProcess.Worker.Tests/CredentialProcess.Worker.Tests.csproj @@ -0,0 +1,47 @@ + + + + + net7.0 + enable + enable + false + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs b/tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs new file mode 100644 index 00000000..963e2bb2 --- /dev/null +++ b/tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs @@ -0,0 +1,215 @@ +using AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests; + +public class CredentialProcessTypeExecutorTests +{ + private readonly CredentialProcessTypeExecutor _sut; + private readonly ICredentialProcessHandler _credentialProcessHandler; + private readonly ICredentialRepository _credentialRepository; + + 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 issuerRepositories = A.Fake(); + _credentialProcessHandler = A.Fake(); + + _credentialRepository = A.Fake(); + + A.CallTo(() => issuerRepositories.GetInstance()).Returns(_credentialRepository); + + _sut = new CredentialProcessTypeExecutor(issuerRepositories, _credentialProcessHandler); + } + + [Fact] + public void GetProcessTypeId_ReturnsExpected() + { + // Assert + _sut.GetProcessTypeId().Should().Be(ProcessTypeId.CREATE_CREDENTIAL); + } + + [Fact] + public void IsExecutableStepTypeId_WithValid_ReturnsExpected() + { + // Assert + _sut.IsExecutableStepTypeId(ProcessStepTypeId.SIGN_CREDENTIAL).Should().BeTrue(); + } + + [Fact] + public void GetExecutableStepTypeIds_ReturnsExpected() + { + // Assert + _sut.GetExecutableStepTypeIds().Should().HaveCount(4).And.Satisfy( + x => x == ProcessStepTypeId.CREATE_CREDENTIAL, + x => x == ProcessStepTypeId.SIGN_CREDENTIAL, + x => x == ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT, + x => x == ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER); + } + + [Fact] + public async Task IsLockRequested_ReturnsExpected() + { + // Act + var result = await _sut.IsLockRequested(ProcessStepTypeId.SIGN_CREDENTIAL).ConfigureAwait(false); + + // Assert + result.Should().BeFalse(); + } + + #region InitializeProcess + + [Fact] + public async Task InitializeProcess_WithExistingProcess_ReturnsExpected() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, Guid.NewGuid())); + + // Act + var result = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert + result.Modified.Should().BeFalse(); + result.ScheduleStepTypeIds.Should().BeNull(); + } + + [Fact] + public async Task InitializeProcess_WithNotExistingProcess_ThrowsNotFoundException() + { + // Arrange + var validProcessId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetDataForProcessId(validProcessId)) + .Returns(new ValueTuple(false, Guid.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 credential"); + } + + #endregion + + #region ExecuteProcessStep + + [Fact] + public async Task ExecuteProcessStep_WithoutRegistrationId_ThrowsUnexpectedConditionException() + { + // Act + async Task Act() => await _sut.ExecuteProcessStep(ProcessStepTypeId.SIGN_CREDENTIAL, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be("credentialId should never be empty here"); + } + + [Fact] + public async Task ExecuteProcessStep_WithValidData_CallsExpected() + { + // Arrange InitializeProcess + var validProcessId = Guid.NewGuid(); + var credentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, credentialId)); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_CREDENTIAL, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // 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 credentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, credentialId)); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + .Throws(new ServiceException("this is a test", true)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_CREDENTIAL, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // 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 credentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetDataForProcessId(validProcessId)) + .Returns(new ValueTuple(true, credentialId)); + + // Act InitializeProcess + var initializeResult = await _sut.InitializeProcess(validProcessId, Enumerable.Empty()).ConfigureAwait(false); + + // Assert InitializeProcess + initializeResult.Modified.Should().BeFalse(); + initializeResult.ScheduleStepTypeIds.Should().BeNull(); + + // Arrange + A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + .Throws(new ServiceException("this is a test")); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.CREATE_CREDENTIAL, Enumerable.Empty(), CancellationToken.None).ConfigureAwait(false); + + // 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 +} diff --git a/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.cs new file mode 100644 index 00000000..280c989c --- /dev/null +++ b/tests/processes/Processes.Library.Tests/ManualProcessDataExtensionsTests.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 Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using ProcessStepTypeId = Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums.ProcessStepTypeId; +using ProcessTypeId = Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums.ProcessTypeId; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Library.Tests; + +public class ManualProcessDataExtensionsTests +{ + private readonly IIssuerRepositories _issuerRepositories; + 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()); + + _issuerRepositories = A.Fake(); + _processStepRepository = A.Fake(); + + A.CallTo(() => _issuerRepositories.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(), 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, _issuerRepositories, _getProcessEntityName); + + // Assert + result.Should().NotBeNull().And.BeOfType().And.Match( + data => + data.ProcessStepTypeId == stepTypeId && + data.Process == sut.Process && + data.ProcessSteps.SequenceEqual(sut.ProcessSteps!) && + data.PortalRepositories == _issuerRepositories); + } + + [Fact] + public void CreateManualProcessData_WithNullVerifyProcessData_Throws() + { + // Arrange + var sut = (VerifyProcessData?)null; + + var Act = () => sut.CreateManualProcessData(_fixture.Create(), _issuerRepositories, _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(), _issuerRepositories, _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(), _issuerRepositories, _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(), 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(), _issuerRepositories, _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(), 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(), _issuerRepositories, _getProcessEntityName); + + // Act + var result = Assert.Throws(Act); + + // Assert + result.Message.Should().Be("processSteps should never have any other status than TODO here"); + } + + #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, _issuerRepositories) + .Create(); + + // Act + sut.RequestLock(expiryDate); + + // Assert + sut.Process.LockExpiryDate.Should().Be(expiryDate); + A.CallTo(() => _issuerRepositories.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, _issuerRepositories) + .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(() => _issuerRepositories.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], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], 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, _issuerRepositories) + .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 == ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps1.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps2.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == 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], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[0], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps1 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[1], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps2 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[2], ProcessStepStatusId.TODO, process.Id, before) + }; + var processSteps3 = new ProcessStep[] + { + new(Guid.NewGuid(), stepTypeIds[3], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], ProcessStepStatusId.TODO, process.Id, before), + new(Guid.NewGuid(), stepTypeIds[3], 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, _issuerRepositories) + .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 == ProcessStepStatusId.SKIPPED && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before, + x => processSteps0.Any(step => step.Id == x.Id) && x.ProcessStepStatusId == ProcessStepStatusId.DUPLICATE && x.DateLastChanged != before + ); + } + + #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, _issuerRepositories); + + // Act + sut.FinalizeProcessStep(); + + // Assert + A.CallTo(() => _processStepRepository.AttachAndModifyProcessSteps(A?, Action)>>._)) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.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 00000000..a5652e21 --- /dev/null +++ b/tests/processes/Processes.Library.Tests/Processes.Library.Tests.csproj @@ -0,0 +1,47 @@ + + + + + net7.0 + enable + enable + false + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Library.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..14669b9c --- /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/MockLogger.cs b/tests/processes/Processes.Worker.Library.Tests/MockLogger.cs new file mode 100644 index 00000000..7db4706d --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/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 Org.Eclipse.TractusX.Portal.Backend.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/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutionServiceTests.cs new file mode 100644 index 00000000..c395fb96 --- /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 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.Tests.Shared; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using System.Linq.Expressions; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library.Tests; + +public class ProcessExecutionServiceTests +{ + private readonly IProcessStepRepository _processStepRepository; + private readonly IIssuerRepositories _issuerRepositories; + 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(); + _issuerRepositories = A.Fake(); + _processStepRepository = A.Fake(); + _processExecutor = A.Fake(); + + _mockLogger = A.Fake>(); + ILogger logger = new MockLogger(_mockLogger); + + A.CallTo(() => _issuerRepositories.GetInstance()) + .Returns(_processStepRepository); + + var settings = _fixture.Create(); + + var options = Options.Create(settings); + var serviceProvider = A.Fake(); + A.CallTo(() => serviceProvider.GetService(typeof(IIssuerRepositories))).Returns(_issuerRepositories); + 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((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithPendingItems_CallsProcessExpectedNumberOfTimes() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.CREATE_CREDENTIAL, 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((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappened(processData.Length * 2, Times.Exactly); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, 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((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappened(processData.Length, Times.Exactly); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, 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(() => _issuerRepositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _issuerRepositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, 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(() => _issuerRepositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _issuerRepositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, firstVersion); + var secondProcess = new Process(secondId, ProcessTypeId.CREATE_CREDENTIAL, 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(() => _issuerRepositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add(process == null + ? (Guid.Empty, Guid.Empty, null, true) + : (process.Id, process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _issuerRepositories.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((Expression)(() => _processExecutor.ExecuteProcess(firstId, A._, A._))) + .MustHaveHappenedOnceExactly(); + A.CallTo((Expression)(() => _processExecutor.ExecuteProcess(secondId, A._, A._))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, 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(() => _issuerRepositories.SaveAsync()) + .ReturnsLazily(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, true)); + return 1; + }); + + A.CallTo(() => _issuerRepositories.Clear()) + .Invokes(() => + { + changeHistory.Add((process.Version, process.LockExpiryDate, false)); + }); + + // Act + await _service.ExecuteAsync(CancellationToken.None); + + // Assert + A.CallTo((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, firstVersion); + var secondProcessId = Guid.NewGuid(); + var secondVersion = Guid.NewGuid(); + var secondProcess = new Process(secondProcessId, ProcessTypeId.CREATE_CREDENTIAL, secondVersion); + var thirdProcessId = Guid.NewGuid(); + var thirdVersion = Guid.NewGuid(); + var thirdProcess = new Process(thirdProcessId, ProcessTypeId.CREATE_CREDENTIAL, 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(() => _issuerRepositories.SaveAsync()) + .Throws(error); + + A.CallTo(() => _issuerRepositories.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((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustHaveHappened(3, Times.Exactly); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustHaveHappenedTwiceExactly(); + A.CallTo(() => _issuerRepositories.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.CREATE_CREDENTIAL, 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((Expression)(() => _processExecutor.ExecuteProcess(A._, A._, A._))) + .MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.SaveAsync()) + .MustNotHaveHappened(); + A.CallTo(() => _issuerRepositories.Clear()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithException_LogsError() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.CREATE_CREDENTIAL, Guid.NewGuid())).ToImmutableArray(); + var error = new Exception("Only a test"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo((Expression)(() => _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(() => _issuerRepositories.SaveAsync()).MustNotHaveHappened(); + } + + [Fact] + public async Task ExecuteAsync_WithSystemException_Exits() + { + // Arrange + var processData = _fixture.CreateMany().Select(x => new Process(x, ProcessTypeId.CREATE_CREDENTIAL, Guid.NewGuid())).ToImmutableArray(); + var error = new SystemException("unrecoverable failure"); + A.CallTo(() => _processStepRepository.GetActiveProcesses(A>._, A>._, A._)) + .Returns(processData.ToAsyncEnumerable()); + A.CallTo((Expression)(() => _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(() => _issuerRepositories.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 00000000..7373b7e2 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/ProcessExecutorTests.cs @@ -0,0 +1,881 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 BMW Group AG + * Copyright (c) 2021, 2023 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; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using System.Collections.Immutable; +using System.Linq.Expressions; +using ProcessTypeId = Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums.ProcessTypeId; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.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 issuerRepositories = A.Fake(); + var logger = A.Fake>(); + + A.CallTo(() => issuerRepositories.GetInstance()) + .Returns(_processStepRepository); + + A.CallTo(() => _processTypeExecutor.GetProcessTypeId()) + .Returns(ProcessTypeId.CREATE_CREDENTIAL); + + _sut = new ProcessExecutor( + new[] { _processTypeExecutor }, + issuerRepositories, + logger); + } + + #region GetRegisteredProcessTypeIds + + [Fact] + public void GetRegisteredProcessTypeIds_ReturnsExpected() + { + // Act + var result = _sut.GetRegisteredProcessTypeIds(); + + // Assert + result.Should().HaveCount(1).And.Contain(ProcessTypeId.CREATE_CREDENTIAL); + } + + #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).ConfigureAwait(false); + + // 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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // 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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // 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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveCount(1).And.Contain(IProcessExecutor.ProcessExecutionResult.Unmodified); + + A.CallTo((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Assert + result. + Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + 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((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo((Expression)(() => _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((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ToListAsync().ConfigureAwait(false); + + // Assert + result.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + A.CallTo((Expression)(() => _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.CREATE_CREDENTIAL, CancellationToken.None).ConfigureAwait(false)) + { + stepResults.Add(stepResult); + } + }; + + // Act + var result = await Assert.ThrowsAsync(Act).ConfigureAwait(false); + + // Assert + stepResults.Should().HaveSameCount(executionResults).And.ContainInOrder(executionResults); + + result.Message.Should().Be(error.Message); + + A.CallTo((Expression)(() => _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 00000000..c5e5b725 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Processes.Worker.Library.Tests.csproj @@ -0,0 +1,47 @@ + + + + + net7.0 + enable + enable + false + Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library.Tests + Org.Eclipse.TractusX.SsiCredentialIssuer.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 00000000..d0c35ff4 --- /dev/null +++ b/tests/processes/Processes.Worker.Library.Tests/Usings.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 BMW Group AG + * Copyright (c) 2021, 2023 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;