diff --git a/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs b/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs index 729d7e4a..7061c2d8 100644 --- a/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs +++ b/src/credentials/SsiCredentialIssuer.Expiry.App/ExpiryCheckService.cs @@ -80,6 +80,7 @@ public async Task ExecuteAsync(CancellationToken stoppingToken) var now = dateTimeProvider.OffsetNow; var companySsiDetailsRepository = repositories.GetInstance(); + var processStepRepository = repositories.GetInstance(); var inactiveVcsToDelete = now.AddDays(-(_settings.InactiveVcsToDeleteInWeeks * 7)); var expiredVcsToDelete = now.AddMonths(-_settings.ExpiredVcsToDeleteInMonth); @@ -87,8 +88,7 @@ public async Task ExecuteAsync(CancellationToken stoppingToken) .GetExpiryData(now, inactiveVcsToDelete, expiredVcsToDelete); await foreach (var credential in credentials.WithCancellation(stoppingToken).ConfigureAwait(false)) { - await ProcessCredentials(credential, companySsiDetailsRepository, repositories, portalService, - stoppingToken); + await ProcessCredentials(credential, companySsiDetailsRepository, repositories, portalService, processStepRepository, stoppingToken); } } catch (Exception ex) @@ -104,6 +104,7 @@ private static async Task ProcessCredentials( ICompanySsiDetailsRepository companySsiDetailsRepository, IIssuerRepositories repositories, IPortalService portalService, + IProcessStepRepository processStepRepository, CancellationToken cancellationToken) { if (data.ScheduleData.IsVcToDelete) @@ -112,7 +113,7 @@ private static async Task ProcessCredentials( } else if (data.ScheduleData.IsVcToDecline) { - await HandleDecline(data, companySsiDetailsRepository, portalService, cancellationToken).ConfigureAwait(false); + HandleDecline(data.Id, companySsiDetailsRepository, processStepRepository); } else { @@ -123,30 +124,21 @@ private static async Task ProcessCredentials( await repositories.SaveAsync().ConfigureAwait(false); } - private static async ValueTask HandleDecline( - CredentialExpiryData data, + private static void HandleDecline( + Guid credentialId, ICompanySsiDetailsRepository companySsiDetailsRepository, - IPortalService portalService, - CancellationToken cancellationToken) + IProcessStepRepository processStepRepository) { - var content = JsonSerializer.Serialize(new { Type = data.VerifiedCredentialTypeId, CredentialId = data.Id }, Options); - await portalService.AddNotification(content, data.RequesterId, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken); - companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(data.Id, c => + var processId = processStepRepository.CreateProcess(ProcessTypeId.DECLINE_CREDENTIAL).Id; + processStepRepository.CreateProcessStep(ProcessStepTypeId.REVOKE_CREDENTIAL, ProcessStepStatusId.TODO, processId); + companySsiDetailsRepository.AttachAndModifyCompanySsiDetails(credentialId, c => { - c.CompanySsiDetailStatusId = data.CompanySsiDetailStatusId; + c.ProcessId = null; }, c => { - c.CompanySsiDetailStatusId = CompanySsiDetailStatusId.INACTIVE; + c.ProcessId = processId; }); - - 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.RequesterId, mailParameters, cancellationToken); } private static async ValueTask HandleNotification( diff --git a/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs b/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs index 5f2202d4..eea92f79 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Models/DocumentData.cs @@ -21,8 +21,7 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; -public record DocumentData -( +public record DocumentData( Guid DocumentId, string DocumentName, - DocumentTypeId argDocumentTypeId); + DocumentTypeId DocumentTypeId); diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs index 719103c5..48ed9b32 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/CredentialRepository.cs @@ -20,6 +20,7 @@ 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; @@ -76,4 +77,28 @@ public CredentialRepository(IssuerDbContext dbContext) .Where(x => x.CompanySsiDetailId == credentialId) .Select(x => new ValueTuple(x.CompanySsiDetail!.Bpnl, x.CallbackUrl)) .SingleOrDefaultAsync(); + + public Task<(bool Exists, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId) => + _dbContext.CompanySsiDetails + .Where(x => x.Id == credentialId) + .Select(x => new ValueTuple>( + true, + x.ExternalCredentialId, + x.CompanySsiDetailStatusId, + x.Documents.Select(d => new ValueTuple(d.Id, d.DocumentStatusId)))) + .SingleOrDefaultAsync(); + + public void AttachAndModifyCredential(Guid credentialId, Action? initialize, Action modify) + { + var entity = new CompanySsiDetail(credentialId, string.Empty, default!, default!, null!, Guid.Empty, default!); + initialize?.Invoke(entity); + _dbContext.CompanySsiDetails.Attach(entity); + modify(entity); + } + + public Task<(VerifiedCredentialTypeId TypeId, Guid RequesterId)> GetCredentialNotificationData(Guid credentialId) => + _dbContext.CompanySsiDetails + .Where(x => x.Id == credentialId) + .Select(x => new ValueTuple(x.VerifiedCredentialTypeId, x.CreatorUserId)) + .SingleOrDefaultAsync(); } diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs index b6bf1b6d..5b2e01f3 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/DocumentRepository.cs @@ -60,4 +60,17 @@ public void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDe var document = new CompanySsiDetailAssignedDocument(documentId, companySsiDetailId); _dbContext.CompanySsiDetailAssignedDocuments.Add(document); } + + public void AttachAndModifyDocuments(IEnumerable<(Guid DocumentId, Action? Initialize, Action Modify)> documentData) + { + var initial = documentData.Select(x => + { + var document = new Document(x.DocumentId, null!, null!, null!, default, default, default, default); + x.Initialize?.Invoke(document); + return (Document: document, x.Modify); + } + ).ToList(); + _dbContext.AttachRange(initial.Select(x => x.Document)); + initial.ForEach(x => x.Modify(x.Document)); + } } diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs index 3902ad2e..7b088c6a 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/ICredentialRepository.cs @@ -18,6 +18,7 @@ ********************************************************************************/ 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; @@ -31,4 +32,7 @@ public interface ICredentialRepository Task<(VerifiedCredentialTypeKindId CredentialTypeKindId, JsonDocument Schema)> GetCredentialStorageInformationById(Guid credentialId); Task<(Guid? ExternalCredentialId, VerifiedCredentialTypeKindId KindId, bool HasEncryptionInformation, string? CallbackUrl)> GetExternalCredentialAndKindId(Guid credentialId); Task<(string Bpn, string? CallbackUrl)> GetCallbackUrl(Guid credentialId); + Task<(bool Exists, Guid? ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> GetRevocationDataById(Guid credentialId); + void AttachAndModifyCredential(Guid credentialId, Action? initialize, Action modify); + Task<(VerifiedCredentialTypeId TypeId, Guid RequesterId)> GetCredentialNotificationData(Guid credentialId); } diff --git a/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs b/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs index 5b5c105b..e447f0de 100644 --- a/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs +++ b/src/database/SsiCredentialIssuer.DbAccess/Repositories/IDocumentRepository.cs @@ -40,4 +40,5 @@ public interface IDocumentRepository Document CreateDocument(string documentName, byte[] documentContent, byte[] hash, MediaTypeId mediaTypeId, DocumentTypeId documentTypeId, Action? setupOptionalFields); void AssignDocumentToCompanySsiDetails(Guid documentId, Guid companySsiDetailId); + void AttachAndModifyDocuments(IEnumerable<(Guid DocumentId, Action? Initialize, Action Modify)> documentData); } diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs index a44fed17..e870cf07 100644 --- a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs +++ b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessStepTypeId.cs @@ -21,10 +21,15 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; public enum ProcessStepTypeId { - // Issuer Process + // CREATE CREDENTIAL PROCESS CREATE_CREDENTIAL = 1, SIGN_CREDENTIAL = 2, SAVE_CREDENTIAL_DOCUMENT = 3, CREATE_CREDENTIAL_FOR_HOLDER = 4, TRIGGER_CALLBACK = 5, + + // DECLINE PROCESS + REVOKE_CREDENTIAL = 100, + TRIGGER_NOTIFICATION = 101, + TRIGGER_MAIL = 102 } diff --git a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs index a07c1bca..faedae91 100644 --- a/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs +++ b/src/database/SsiCredentialIssuer.Entities/Enums/ProcessTypeId.cs @@ -22,4 +22,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; public enum ProcessTypeId { CREATE_CREDENTIAL = 1, + DECLINE_CREDENTIAL = 2 } 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 index 58bb092c..b5a15ba4 100644 --- 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 @@ -2,7 +2,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d562", "verified_credential_external_type_id": 3, - "version": "1.0.0", + "version": "1.0", "template": null, "valid_from": "2023-06-01 00:00:00.000000 +00:00", "expiry": "2023-09-30 00:00:00.000000 +00:00" @@ -10,7 +10,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d563", "verified_credential_external_type_id": 1, - "version": "2.0.0", + "version": "2.0", "template": null, "valid_from": "2023-06-01 00:00:00.000000 +00:00", "expiry": "2023-12-23 00:00:00.000000 +00:00" @@ -18,7 +18,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d564", "verified_credential_external_type_id": 1, - "version": "3.0.0", + "version": "3.0", "template": null, "valid_from": "2024-01-01 00:00:00.000000 +00:00", "expiry": "2024-12-31 00:00:00.000000 +00:00" @@ -26,7 +26,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d565", "verified_credential_external_type_id": 5, - "version": "1.0.0", + "version": "1.0", "template": null, "valid_from": "2024-01-01 00:00:00.000000 +00:00", "expiry": "2024-12-31 00:00:00.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 index ee5ccd40..808199f8 100644 --- 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 @@ -2,7 +2,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d560", "verified_credential_external_type_id": 1, - "version": "1.0.0", + "version": "1.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" @@ -10,7 +10,7 @@ { "id": "1268a76a-ca19-4dd8-b932-01f24071d561", "verified_credential_external_type_id": 2, - "version": "1.0.0", + "version": "1.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" @@ -18,7 +18,7 @@ { "id": "37aa6259-b452-4d50-b09e-827929dcfa15", "verified_credential_external_type_id": 6, - "version": "1.0.0", + "version": "1.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/externalservices/Wallet.Service/Models/RevokeCredentialRequest.cs b/src/externalservices/Wallet.Service/Models/RevokeCredentialRequest.cs new file mode 100644 index 00000000..5c565baf --- /dev/null +++ b/src/externalservices/Wallet.Service/Models/RevokeCredentialRequest.cs @@ -0,0 +1,30 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; + +public record RevokeCredentialRequest( + [property: JsonPropertyName("payload")] RevokePayload Payload +); + +public record RevokePayload( + [property: JsonPropertyName("revoke")] bool Revoke +); diff --git a/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs b/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs index e379cb34..693cd234 100644 --- a/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs +++ b/src/externalservices/Wallet.Service/Services/IBasicAuthTokenService.cs @@ -21,5 +21,5 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; public interface IBasicAuthTokenService { - Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken); + Task GetBasicAuthorizedClient(BasicAuthSettings settings, CancellationToken cancellationToken = default); } diff --git a/src/externalservices/Wallet.Service/Services/IWalletService.cs b/src/externalservices/Wallet.Service/Services/IWalletService.cs index 27365f47..9f775310 100644 --- a/src/externalservices/Wallet.Service/Services/IWalletService.cs +++ b/src/externalservices/Wallet.Service/Services/IWalletService.cs @@ -27,4 +27,6 @@ public interface IWalletService Task SignCredential(Guid credentialId, CancellationToken cancellationToken); Task CreateCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, string credential, CancellationToken cancellationToken); Task GetCredential(Guid externalCredentialId, CancellationToken cancellationToken); + Task RevokeCredentialForIssuer(Guid externalCredentialId, CancellationToken cancellationToken); + Task RevokeCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, Guid externalCredentialId, CancellationToken cancellationToken); } diff --git a/src/externalservices/Wallet.Service/Services/WalletService.cs b/src/externalservices/Wallet.Service/Services/WalletService.cs index e5fb59ff..7dda3d40 100644 --- a/src/externalservices/Wallet.Service/Services/WalletService.cs +++ b/src/externalservices/Wallet.Service/Services/WalletService.cs @@ -113,4 +113,30 @@ public async Task CreateCredentialForHolder(string holderWalletUrl, string return response.Id; } + + public async Task RevokeCredentialForIssuer(Guid externalCredentialId, CancellationToken cancellationToken) + { + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(_settings, cancellationToken); + var data = new RevokeCredentialRequest(new RevokePayload(true)); + await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{externalCredentialId}", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("revoke-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + } + + public async Task RevokeCredentialForHolder(string holderWalletUrl, string clientId, string clientSecret, Guid externalCredentialId, CancellationToken cancellationToken) + { + var authSettings = new BasicAuthSettings + { + ClientId = clientId, + ClientSecret = clientSecret, + TokenAddress = $"{holderWalletUrl}/oauth/token" + }; + var client = await _basicAuthTokenService.GetBasicAuthorizedClient(authSettings, cancellationToken); + var data = new RevokeCredentialRequest(new RevokePayload(true)); + await client.PatchAsJsonAsync($"/api/v2.0.0/credentials/{externalCredentialId}", data, Options, cancellationToken) + .CatchingIntoServiceExceptionFor("revoke-credential", HttpAsyncResponseMessageExtension.RecoverOptions.INFRASTRUCTURE, + async x => (false, await x.Content.ReadAsStringAsync().ConfigureAwait(false))) + .ConfigureAwait(false); + } } diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IRevocationBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IRevocationBusinessLogic.cs new file mode 100644 index 00000000..a91c9b21 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IRevocationBusinessLogic.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.DBAccess.Models; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; + +public interface IRevocationBusinessLogic +{ + Task RevokeIssuerCredential(Guid credentialId, CancellationToken cancellationToken); + Task RevokeHolderCredential(Guid credentialId, TechnicalUserDetails walletInformation, CancellationToken cancellationToken); +} diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs index 8dd4b588..72cd81e4 100644 --- a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/IssuerBusinessLogic.cs @@ -197,8 +197,6 @@ public async Task ApproveCredential(Guid credentialId, CancellationToken cancell c.ProcessId = processId; }); var typeValue = data.Type.GetEnumValue() ?? throw UnexpectedConditionException.Create(CredentialErrors.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.IdentityId, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(false); var mailParameters = new Dictionary { { "requestName", typeValue }, @@ -206,6 +204,10 @@ public async Task ApproveCredential(Guid credentialId, CancellationToken cancell { "expiryDate", expiry.ToString("o", CultureInfo.InvariantCulture) } }; await _portalService.TriggerMail("CredentialApproval", _identity.IdentityId, mailParameters, cancellationToken).ConfigureAwait(false); + + var content = JsonSerializer.Serialize(new { data.Type, CredentialId = credentialId }, Options); + await _portalService.AddNotification(content, _identity.IdentityId, NotificationTypeId.CREDENTIAL_APPROVAL, cancellationToken).ConfigureAwait(false); + await _repositories.SaveAsync().ConfigureAwait(false); } @@ -505,8 +507,9 @@ private async Task HandleCredentialProcessCreation( c.ClientId = technicalUserDetails.ClientId; c.ClientSecret = secret; - c.InitializationVector = initializationVector; c.HolderWalletUrl = technicalUserDetails.WalletUrl; + c.EncryptionMode = cryptoConfig.Index; + c.InitializationVector = initializationVector; c.CallbackUrl = callbackUrl; }); diff --git a/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/RevocationBusinessLogic.cs b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/RevocationBusinessLogic.cs new file mode 100644 index 00000000..09b68cd8 --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/BusinessLogic/RevocationBusinessLogic.cs @@ -0,0 +1,102 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using 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.Entities; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.BusinessLogic; + +public class RevocationBusinessLogic : IRevocationBusinessLogic +{ + private readonly IIssuerRepositories _repositories; + private readonly IWalletService _walletService; + + public RevocationBusinessLogic(IIssuerRepositories repositories, IWalletService walletService) + { + _repositories = repositories; + _walletService = walletService; + } + + public async Task RevokeIssuerCredential(Guid credentialId, CancellationToken cancellationToken) + { + // check for is issuer + var credentialRepository = _repositories.GetInstance(); + var data = await RevokeCredentialInternal(credentialId, credentialRepository).ConfigureAwait(false); + if (data.StatusId != CompanySsiDetailStatusId.ACTIVE) + { + return; + } + + // call walletService + await _walletService.RevokeCredentialForIssuer(data.ExternalCredentialId, cancellationToken).ConfigureAwait(false); + UpdateData(credentialId, data.StatusId, data.Documents, credentialRepository); + } + + public async Task RevokeHolderCredential(Guid credentialId, TechnicalUserDetails walletInformation, CancellationToken cancellationToken) + { + // check for is holder + var credentialRepository = _repositories.GetInstance(); + var data = await RevokeCredentialInternal(credentialId, credentialRepository).ConfigureAwait(false); + if (data.StatusId != CompanySsiDetailStatusId.ACTIVE) + { + return; + } + + // call walletService + await _walletService.RevokeCredentialForHolder(walletInformation.WalletUrl, walletInformation.ClientId, walletInformation.ClientSecret, data.ExternalCredentialId, cancellationToken).ConfigureAwait(false); + UpdateData(credentialId, data.StatusId, data.Documents, credentialRepository); + } + + private static async Task<(Guid ExternalCredentialId, CompanySsiDetailStatusId StatusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> Documents)> RevokeCredentialInternal(Guid credentialId, ICredentialRepository credentialRepository) + { + var data = await credentialRepository.GetRevocationDataById(credentialId) + .ConfigureAwait(false); + if (!data.Exists) + { + throw NotFoundException.Create(RevocationDataErrors.CREDENTIAL_NOT_FOUND, new ErrorParameter[] { new("credentialId", credentialId.ToString()) }); + } + + if (data.ExternalCredentialId is null) + { + throw ConflictException.Create(RevocationDataErrors.EXTERNAL_CREDENTIAL_ID_NOT_SET, new ErrorParameter[] { new("credentialId", credentialId.ToString()) }); + } + + return (data.ExternalCredentialId.Value, data.StatusId, data.Documents); + } + + private void UpdateData(Guid credentialId, CompanySsiDetailStatusId statusId, IEnumerable<(Guid DocumentId, DocumentStatusId DocumentStatusId)> documentData, ICredentialRepository credentialRepository) + { + _repositories.GetInstance().AttachAndModifyDocuments( + documentData.Select(d => new ValueTuple?, Action>( + d.DocumentId, + document => document.DocumentStatusId = d.DocumentStatusId, + document => document.DocumentStatusId = DocumentStatusId.INACTIVE + ))); + + credentialRepository.AttachAndModifyCredential(credentialId, + x => x.CompanySsiDetailStatusId = statusId, + x => x.CompanySsiDetailStatusId = CompanySsiDetailStatusId.REVOKED); + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs b/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs index 62fd8f06..8d0f0222 100644 --- a/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs +++ b/src/issuer/SsiCredentialIssuer.Service/Controllers/IssuerController.cs @@ -97,11 +97,11 @@ public static RouteGroupBuilder MapIssuerApi(this RouteGroupBuilder group) .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.ValidIdentity)); - }) + // .RequireAuthorization(r => + // { + // r.RequireRole(RequestSsiRole); + // r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + // }) .WithDefaultResponses() .Produces(StatusCodes.Status200OK, typeof(Guid), contentType: Constants.JsonContentType); diff --git a/src/issuer/SsiCredentialIssuer.Service/Controllers/RevocationController.cs b/src/issuer/SsiCredentialIssuer.Service/Controllers/RevocationController.cs new file mode 100644 index 00000000..050e9c3a --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/Controllers/RevocationController.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.AspNetCore.Mvc; +using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; +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; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Controllers; + +/// +/// Creates a new instance of +/// +public static class RevocationController +{ + public static RouteGroupBuilder MapRevocationApi(this RouteGroupBuilder group) + { + var revocation = group.MapGroup("/revocation"); + + revocation.MapPost("issuer/credentials/{credentialId}", ([FromRoute] Guid credentialId, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeIssuerCredential(credentialId, cancellationToken)) + .WithSwaggerDescription("Revokes an credential which was issued by the given issuer", + "POST: api/revocation/issuer/credentials/{credentialId}", + "Id of the credential that should be revoked") + .AllowAnonymous() + // .RequireAuthorization(r => + // { + // r.RequireRole("revoke_credentials_issuer"); + // r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + // r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + // }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(Guid)); + revocation.MapPost("credentials/{credentialId}", ([FromRoute] Guid credentialId, [FromBody] TechnicalUserDetails data, CancellationToken cancellationToken, [FromServices] IRevocationBusinessLogic logic) => logic.RevokeHolderCredential(credentialId, data, cancellationToken)) + .WithSwaggerDescription("Revokes an credential of an holder", + "POST: api/revocation/credentials/{credentialId}", + "Id of the credential that should be revoked", + "The information for the holder wallet", + "CancellationToken") + .RequireAuthorization(r => + { + r.RequireRole("revoke_credential"); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidBpn)); + r.AddRequirements(new MandatoryIdentityClaimRequirement(PolicyTypeId.ValidIdentity)); + }) + .WithDefaultResponses() + .Produces(StatusCodes.Status200OK, typeof(Guid), contentType: Constants.JsonContentType); + + return group; + } +} diff --git a/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/RevocationServiceCollectionExtensions.cs b/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/RevocationServiceCollectionExtensions.cs new file mode 100644 index 00000000..1dd0233f --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/DependencyInjection/RevocationServiceCollectionExtensions.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.Service.BusinessLogic; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Service.DependencyInjection; + +public static class RevocationServiceCollectionExtensions +{ + public static IServiceCollection AddRevocationService(this IServiceCollection services) => + services + .AddTransient(); +} diff --git a/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/RevocationErrorMessageContainer.cs b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/RevocationErrorMessageContainer.cs new file mode 100644 index 00000000..2b52828e --- /dev/null +++ b/src/issuer/SsiCredentialIssuer.Service/ErrorHandling/RevocationErrorMessageContainer.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 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 RevocationErrorMessageContainer : IErrorMessageContainer +{ + private static readonly IReadOnlyDictionary _messageContainer = new Dictionary { + { RevocationDataErrors.CREDENTIAL_NOT_FOUND, "Credential {credentialId} does not exist" }, + { RevocationDataErrors.EXTERNAL_CREDENTIAL_ID_NOT_SET, "External Credential Id must be set for {credentialId}" } + }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); + + public Type Type { get => typeof(RevocationDataErrors); } + public IReadOnlyDictionary MessageContainer { get => _messageContainer; } +} + +public enum RevocationDataErrors +{ + CREDENTIAL_NOT_FOUND, + EXTERNAL_CREDENTIAL_ID_NOT_SET +} diff --git a/src/issuer/SsiCredentialIssuer.Service/Program.cs b/src/issuer/SsiCredentialIssuer.Service/Program.cs index 3b1dcd7f..b5596fcb 100644 --- a/src/issuer/SsiCredentialIssuer.Service/Program.cs +++ b/src/issuer/SsiCredentialIssuer.Service/Program.cs @@ -29,6 +29,7 @@ using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.DependencyInjection; using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.ErrorHandling; using Org.Eclipse.TractusX.SsiCredentialIssuer.Service.Identity; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; using System.Text.Json.Serialization; const string VERSION = "v1"; @@ -52,13 +53,17 @@ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }) .AddCredentialService(builder.Configuration.GetSection("Credential")) + .AddRevocationService() + .AddWalletService(builder.Configuration) .AddPortalService(builder.Configuration.GetSection("Portal")) .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); }, (app, _) => { app.MapGroup("/api") .WithOpenApi() - .MapIssuerApi(); + .MapIssuerApi() + .MapRevocationApi(); }); diff --git a/src/issuer/SsiCredentialIssuer.Service/appsettings.json b/src/issuer/SsiCredentialIssuer.Service/appsettings.json index 67731a38..9726c0c8 100644 --- a/src/issuer/SsiCredentialIssuer.Service/appsettings.json +++ b/src/issuer/SsiCredentialIssuer.Service/appsettings.json @@ -53,6 +53,18 @@ "KeycloakTokenAddress": "", "BaseAddress": "" }, + "Wallet": { + "Username": "", + "Password": "", + "ClientId": "", + "GrantType": "", + "ClientSecret": "", + "Scope": "", + "TokenAddress": "", + "BaseAddress": "", + "EncryptionConfigIndex": 0, + "EncryptionConfigs": [] + }, "Credential": { "IssuerDid": "", "IssuerBpn": "", diff --git a/src/processes/CredentialProcess.Library/CredentialProcessHandler.cs b/src/processes/CredentialProcess.Library/Creation/CredentialCreationProcessHandler.cs similarity index 96% rename from src/processes/CredentialProcess.Library/CredentialProcessHandler.cs rename to src/processes/CredentialProcess.Library/Creation/CredentialCreationProcessHandler.cs index 0fa0f39a..12819124 100644 --- a/src/processes/CredentialProcess.Library/CredentialProcessHandler.cs +++ b/src/processes/CredentialProcess.Library/Creation/CredentialCreationProcessHandler.cs @@ -26,15 +26,15 @@ using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.BusinessLogic; using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Models; -namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; -public class CredentialProcessHandler : ICredentialProcessHandler +public class CredentialCreationProcessHandler : ICredentialCreationProcessHandler { private readonly IIssuerRepositories _issuerRepositories; private readonly IWalletBusinessLogic _walletBusinessLogic; private readonly ICallbackService _callbackService; - public CredentialProcessHandler(IIssuerRepositories issuerRepositories, IWalletBusinessLogic walletBusinessLogic, ICallbackService callbackService) + public CredentialCreationProcessHandler(IIssuerRepositories issuerRepositories, IWalletBusinessLogic walletBusinessLogic, ICallbackService callbackService) { _issuerRepositories = issuerRepositories; _walletBusinessLogic = walletBusinessLogic; diff --git a/src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs b/src/processes/CredentialProcess.Library/Creation/ICredentialCreationProcessHandler.cs similarity index 96% rename from src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs rename to src/processes/CredentialProcess.Library/Creation/ICredentialCreationProcessHandler.cs index d21b1c9e..f68c31ab 100644 --- a/src/processes/CredentialProcess.Library/ICredentialProcessHandler.cs +++ b/src/processes/CredentialProcess.Library/Creation/ICredentialCreationProcessHandler.cs @@ -19,9 +19,9 @@ using Org.Eclipse.TractusX.SsiCredentialIssuer.Entities.Enums; -namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; -public interface ICredentialProcessHandler +public interface ICredentialCreationProcessHandler { 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); diff --git a/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj b/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj index 5a100318..bb58a792 100644 --- a/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj +++ b/src/processes/CredentialProcess.Library/CredentialProcess.Library.csproj @@ -30,6 +30,7 @@ + diff --git a/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs b/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs index 6296675f..ac774cbd 100644 --- a/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs +++ b/src/processes/CredentialProcess.Library/DependencyInjection/CredentialHandlerExtensions.cs @@ -19,16 +19,27 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Expiry; using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.DependencyInjection; namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.DependencyInjection; public static class CredentialHandlerExtensions { - public static IServiceCollection AddCredentialProcessHandler(this IServiceCollection services, IConfiguration config) + public static IServiceCollection AddCredentialCreationProcessHandler(this IServiceCollection services, IConfiguration config) { services - .AddTransient() + .AddTransient() + .AddWalletService(config); + + return services; + } + + public static IServiceCollection AddCredentialExpiryProcessHandler(this IServiceCollection services, IConfiguration config) + { + services + .AddTransient() .AddWalletService(config); return services; diff --git a/src/processes/CredentialProcess.Library/Expiry/CredentialCreationProcessHandler.cs b/src/processes/CredentialProcess.Library/Expiry/CredentialCreationProcessHandler.cs new file mode 100644 index 00000000..2bd7e056 --- /dev/null +++ b/src/processes/CredentialProcess.Library/Expiry/CredentialCreationProcessHandler.cs @@ -0,0 +1,113 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, 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.Portal.Backend.Framework.Models; +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.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Text.Json; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Expiry; + +public class CredentialExpiryProcessHandler : ICredentialExpiryProcessHandler +{ + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + private readonly IIssuerRepositories _repositories; + private readonly IWalletService _walletService; + private readonly IPortalService _portalService; + + public CredentialExpiryProcessHandler(IIssuerRepositories repositories, IWalletService walletService, IPortalService portalService) + { + _repositories = repositories; + _walletService = walletService; + _portalService = portalService; + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> RevokeCredential(Guid credentialId, CancellationToken cancellationToken) + { + var credentialRepository = _repositories.GetInstance(); + var data = await credentialRepository.GetRevocationDataById(credentialId) + .ConfigureAwait(false); + if (!data.Exists) + { + throw new NotFoundException($"Credential {credentialId} does not exist"); + } + + if (data.ExternalCredentialId is null) + { + throw new ConflictException($"External Credential Id must be set for {credentialId}"); + } + + // call walletService + await _walletService.RevokeCredentialForIssuer(data.ExternalCredentialId.Value, cancellationToken).ConfigureAwait(false); + + _repositories.GetInstance().AttachAndModifyDocuments( + data.Documents.Select(d => new ValueTuple?, Action>( + d.DocumentId, + document => document.DocumentStatusId = d.DocumentStatusId, + document => document.DocumentStatusId = DocumentStatusId.INACTIVE + ))); + + credentialRepository.AttachAndModifyCredential(credentialId, + x => x.CompanySsiDetailStatusId = data.StatusId, + x => x.CompanySsiDetailStatusId = CompanySsiDetailStatusId.REVOKED); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.TRIGGER_NOTIFICATION, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> TriggerNotification(Guid credentialId, CancellationToken cancellationToken) + { + var (typeId, requesterId) = await _repositories.GetInstance().GetCredentialNotificationData(credentialId).ConfigureAwait(false); + var content = JsonSerializer.Serialize(new { Type = typeId, CredentialId = credentialId }, Options); + await _portalService.AddNotification(content, requesterId, NotificationTypeId.CREDENTIAL_REJECTED, cancellationToken); + + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + Enumerable.Repeat(ProcessStepTypeId.TRIGGER_MAIL, 1), + ProcessStepStatusId.DONE, + false, + null); + } + + public async Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> TriggerMail(Guid credentialId, CancellationToken cancellationToken) + { + var (typeId, requesterId) = await _repositories.GetInstance().GetCredentialNotificationData(credentialId).ConfigureAwait(false); + + var typeValue = typeId.GetEnumValue() ?? throw new UnexpectedConditionException($"VerifiedCredentialType {typeId} does not exists"); + var mailParameters = new Dictionary + { + { "requestName", typeValue }, + { "reason", "The credential is already expired" } + }; + await _portalService.TriggerMail("CredentialRejected", requesterId, mailParameters, cancellationToken); + return new ValueTuple?, ProcessStepStatusId, bool, string?>( + null, + ProcessStepStatusId.DONE, + false, + null); + } +} diff --git a/src/processes/CredentialProcess.Library/Expiry/ICredentialExpiryProcessHandler.cs b/src/processes/CredentialProcess.Library/Expiry/ICredentialExpiryProcessHandler.cs new file mode 100644 index 00000000..33bcc790 --- /dev/null +++ b/src/processes/CredentialProcess.Library/Expiry/ICredentialExpiryProcessHandler.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.Enums; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Expiry; + +public interface ICredentialExpiryProcessHandler +{ + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> RevokeCredential(Guid credentialId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> TriggerNotification(Guid credentialId, CancellationToken cancellationToken); + Task<(IEnumerable? nextStepTypeIds, ProcessStepStatusId stepStatusId, bool modified, string? processMessage)> TriggerMail(Guid credentialId, CancellationToken cancellationToken); +} diff --git a/src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs b/src/processes/CredentialProcess.Worker/Creation/CredentialCreationProcessTypeExecutor.cs similarity index 85% rename from src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs rename to src/processes/CredentialProcess.Worker/Creation/CredentialCreationProcessTypeExecutor.cs index b260f92d..acdcb4ac 100644 --- a/src/processes/CredentialProcess.Worker/CredentialProcessTypeExecutor.cs +++ b/src/processes/CredentialProcess.Worker/Creation/CredentialCreationProcessTypeExecutor.cs @@ -18,19 +18,19 @@ ********************************************************************************/ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; -using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; 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; +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Creation; -public class CredentialProcessTypeExecutor : IProcessTypeExecutor +public class CredentialCreationProcessTypeExecutor : IProcessTypeExecutor { private readonly IIssuerRepositories _issuerRepositories; - private readonly ICredentialProcessHandler _credentialProcessHandler; + private readonly ICredentialCreationProcessHandler _credentialCreationProcessHandler; private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( ProcessStepTypeId.CREATE_CREDENTIAL, @@ -41,12 +41,12 @@ public class CredentialProcessTypeExecutor : IProcessTypeExecutor private Guid _credentialId; - public CredentialProcessTypeExecutor( + public CredentialCreationProcessTypeExecutor( IIssuerRepositories issuerRepositories, - ICredentialProcessHandler credentialProcessHandler) + ICredentialCreationProcessHandler credentialCreationProcessHandler) { _issuerRepositories = issuerRepositories; - _credentialProcessHandler = credentialProcessHandler; + _credentialCreationProcessHandler = credentialCreationProcessHandler; } public ProcessTypeId GetProcessTypeId() => ProcessTypeId.CREATE_CREDENTIAL; @@ -82,15 +82,15 @@ public CredentialProcessTypeExecutor( { (nextStepTypeIds, stepStatusId, modified, processMessage) = processStepTypeId switch { - ProcessStepTypeId.CREATE_CREDENTIAL => await _credentialProcessHandler.CreateCredential(_credentialId, cancellationToken) + ProcessStepTypeId.CREATE_CREDENTIAL => await _credentialCreationProcessHandler.CreateCredential(_credentialId, cancellationToken) .ConfigureAwait(false), - ProcessStepTypeId.SIGN_CREDENTIAL => await _credentialProcessHandler.SignCredential(_credentialId, cancellationToken) + ProcessStepTypeId.SIGN_CREDENTIAL => await _credentialCreationProcessHandler.SignCredential(_credentialId, cancellationToken) .ConfigureAwait(false), - ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT => await _credentialProcessHandler.SaveCredentialDocument(_credentialId, cancellationToken) + ProcessStepTypeId.SAVE_CREDENTIAL_DOCUMENT => await _credentialCreationProcessHandler.SaveCredentialDocument(_credentialId, cancellationToken) .ConfigureAwait(false), - ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER => await _credentialProcessHandler.CreateCredentialForHolder(_credentialId, cancellationToken) + ProcessStepTypeId.CREATE_CREDENTIAL_FOR_HOLDER => await _credentialCreationProcessHandler.CreateCredentialForHolder(_credentialId, cancellationToken) .ConfigureAwait(false), - ProcessStepTypeId.TRIGGER_CALLBACK => await _credentialProcessHandler.TriggerCallback(_credentialId, cancellationToken) + ProcessStepTypeId.TRIGGER_CALLBACK => await _credentialCreationProcessHandler.TriggerCallback(_credentialId, cancellationToken) .ConfigureAwait(false), _ => (null, ProcessStepStatusId.TODO, false, null) }; diff --git a/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs b/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs index ba49a183..503203d7 100644 --- a/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs +++ b/src/processes/CredentialProcess.Worker/DependencyInjection/CredentialProcessCollectionExtensions.cs @@ -20,14 +20,20 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.DependencyInjection; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Expiry; using Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.DependencyInjection; public static class CredentialProcessCollectionExtensions { - public static IServiceCollection AddCredentialProcessExecutor(this IServiceCollection services, IConfiguration config) => + public static IServiceCollection AddCredentialCreationProcessExecutor(this IServiceCollection services, IConfiguration config) => services - .AddTransient() - .AddCredentialProcessHandler(config); + .AddTransient() + .AddCredentialCreationProcessHandler(config); + + public static IServiceCollection AddCredentialExpiryProcessExecutor(this IServiceCollection services, IConfiguration config) => + services + .AddTransient() + .AddCredentialExpiryProcessHandler(config); } diff --git a/src/processes/CredentialProcess.Worker/Expiry/CredentialCreationProcessTypeExecutor.cs b/src/processes/CredentialProcess.Worker/Expiry/CredentialCreationProcessTypeExecutor.cs new file mode 100644 index 00000000..d3710403 --- /dev/null +++ b/src/processes/CredentialProcess.Worker/Expiry/CredentialCreationProcessTypeExecutor.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 Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Expiry; +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.Expiry; + +public class CredentialExpiryProcessTypeExecutor : IProcessTypeExecutor +{ + private readonly IIssuerRepositories _issuerRepositories; + private readonly ICredentialExpiryProcessHandler _credentialExpiryProcessHandler; + + private readonly IEnumerable _executableProcessSteps = ImmutableArray.Create( + ProcessStepTypeId.REVOKE_CREDENTIAL, + ProcessStepTypeId.TRIGGER_NOTIFICATION, + ProcessStepTypeId.TRIGGER_MAIL); + + private Guid _credentialId; + + public CredentialExpiryProcessTypeExecutor( + IIssuerRepositories issuerRepositories, + ICredentialExpiryProcessHandler credentialExpiryProcessHandler) + { + _issuerRepositories = issuerRepositories; + _credentialExpiryProcessHandler = credentialExpiryProcessHandler; + } + + public ProcessTypeId GetProcessTypeId() => ProcessTypeId.DECLINE_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.REVOKE_CREDENTIAL => await _credentialExpiryProcessHandler.RevokeCredential(_credentialId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.TRIGGER_NOTIFICATION => await _credentialExpiryProcessHandler.TriggerNotification(_credentialId, cancellationToken) + .ConfigureAwait(false), + ProcessStepTypeId.TRIGGER_MAIL => await _credentialExpiryProcessHandler.TriggerMail(_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/Processes.Library/Processes.Library.csproj b/src/processes/Processes.Library/Processes.Library.csproj index 56ca7374..f9aed95e 100644 --- a/src/processes/Processes.Library/Processes.Library.csproj +++ b/src/processes/Processes.Library/Processes.Library.csproj @@ -35,6 +35,7 @@ + diff --git a/src/processes/Processes.Worker/Program.cs b/src/processes/Processes.Worker/Program.cs index 125aee70..396cddb3 100644 --- a/src/processes/Processes.Worker/Program.cs +++ b/src/processes/Processes.Worker/Program.cs @@ -42,14 +42,15 @@ .AddProcessExecutionService(hostContext.Configuration.GetSection("Processes")) .AddPortalService(hostContext.Configuration.GetSection("Portal")) .AddCallbackService(hostContext.Configuration.GetSection("Callback")) - .AddCredentialProcessExecutor(hostContext.Configuration); + .AddCredentialCreationProcessExecutor(hostContext.Configuration) + .AddCredentialExpiryProcessExecutor(hostContext.Configuration); }) .AddLogging() .Build(); Log.Information("Building worker completed"); using var tokenSource = new CancellationTokenSource(); - Console.CancelKeyPress += (s, e) => + Console.CancelKeyPress += (_, e) => { Log.Information("Canceling..."); tokenSource.Cancel(); diff --git a/src/processes/Processes.Worker/appsettings.json b/src/processes/Processes.Worker/appsettings.json index 50b53b47..db783398 100644 --- a/src/processes/Processes.Worker/appsettings.json +++ b/src/processes/Processes.Worker/appsettings.json @@ -46,14 +46,7 @@ "Scope": "", "TokenAddress": "", "BaseAddress": "", - "EncrptionConfigIndex": 0, - "EncryptionConfigs": [ - { - "Index": 0, - "EncryptionKey": "", - "CipherMode": "", - "PaddingMode": "" - } - ] + "EncryptionConfigIndex": 0, + "EncryptionConfigs": [] } } diff --git a/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs index ffa94ee0..9af80c3f 100644 --- a/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs +++ b/tests/credentials/SsiCredentialIssuer.Expiry.App.Tests/ExpiryCheckServiceTests.cs @@ -30,6 +30,7 @@ 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 Org.Eclipse.TractusX.SsiCredentialIssuer.Processes.Worker.Library; namespace Org.Eclipse.TractusX.SsiCredentialIssuer.Expiry.App.Tests; @@ -40,6 +41,7 @@ public class ExpiryCheckServiceTests private readonly IDateTimeProvider _dateTimeProvider; private readonly IIssuerRepositories _issuerRepositories; + private readonly IProcessStepRepository _processStepRepository; private readonly IPortalService _portalService; private readonly ICompanySsiDetailsRepository _companySsiDetailsRepository; private readonly ExpiryCheckServiceSettings _settings; @@ -56,9 +58,12 @@ public ExpiryCheckServiceTests() _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); _issuerRepositories = A.Fake(); _companySsiDetailsRepository = A.Fake(); + _processStepRepository = A.Fake(); A.CallTo(() => _issuerRepositories.GetInstance()) .Returns(_companySsiDetailsRepository); + A.CallTo(() => _issuerRepositories.GetInstance()) + .Returns(_processStepRepository); _dateTimeProvider = A.Fake(); _portalService = A.Fake(); @@ -127,13 +132,6 @@ public async Task ExecuteAsync_WithPendingAndExpiryBeforeNow_DeclinesRequest() 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); @@ -141,10 +139,8 @@ public async Task ExecuteAsync_WithPendingAndExpiryBeforeNow_DeclinesRequest() // Assert A.CallTo(() => _companySsiDetailsRepository.RemoveSsiDetail(ssiDetail.Id)).MustNotHaveHappened(); A.CallTo(() => _issuerRepositories.SaveAsync()).MustHaveHappenedOnceExactly(); - A.CallTo(() => _portalService.AddNotification(A._, ssiDetail.CreatorUserId, NotificationTypeId.CREDENTIAL_REJECTED, A._)).MustHaveHappenedOnceExactly(); - A.CallTo(() => _portalService.TriggerMail("CredentialRejected", ssiDetail.CreatorUserId, A>._, A._)).MustHaveHappenedOnceExactly(); - - ssiDetail.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.INACTIVE); + A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.DECLINE_CREDENTIAL)).MustHaveHappenedOnceExactly(); + A.CallTo(() => _processStepRepository.CreateProcessStep(ProcessStepTypeId.REVOKE_CREDENTIAL, ProcessStepStatusId.TODO, A._)).MustHaveHappenedOnceExactly(); } [Theory] diff --git a/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs b/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs index 7c72d5ca..64452bb4 100644 --- a/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs +++ b/tests/externalservices/Wallet.Service.Tests/Services/WalletServiceTests.cs @@ -271,4 +271,114 @@ public async Task CreateCredentialForHolder_WithConflict_ThrowsServiceExceptionW } #endregion + + #region RevokeCredentialForIssuer + + [Fact] + public async Task RevokeCredentialForIssuer_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 + await _sut.RevokeCredentialForIssuer(Guid.NewGuid(), CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(RevokeCredentialRequest) && + ((x.Content as JsonContent)!.Value as RevokeCredentialRequest)!.Payload.Revoke == true + ); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system revoke-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system revoke-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system revoke-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system revoke-credential failed with statuscode 403")] + public async Task RevokeCredentialForIssuer_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.RevokeCredentialForIssuer(Guid.NewGuid(), CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be(message); + ex.StatusCode.Should().Be(statusCode); + } + + #endregion + + #region RevokeCredentialForHolder + + [Fact] + public async Task RevokeCredentialForHolder_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 + await _sut.RevokeCredentialForHolder("https://test.de", "test123", "cl1", Guid.NewGuid(), CancellationToken.None).ConfigureAwait(false); + + // Assert + httpMessageHandlerMock.RequestMessage.Should().Match(x => + x.Content is JsonContent && + (x.Content as JsonContent)!.ObjectType == typeof(RevokeCredentialRequest) && + ((x.Content as JsonContent)!.Value as RevokeCredentialRequest)!.Payload.Revoke == true + ); + } + + [Theory] + [InlineData(HttpStatusCode.Conflict, "{ \"message\": \"Framework test!\" }", "call to external system revoke-credential failed with statuscode 409 - Message: { \"message\": \"Framework test!\" }")] + [InlineData(HttpStatusCode.BadRequest, "{ \"test\": \"123\" }", "call to external system revoke-credential failed with statuscode 400 - Message: { \"test\": \"123\" }")] + [InlineData(HttpStatusCode.BadRequest, "this is no json", "call to external system revoke-credential failed with statuscode 400 - Message: this is no json")] + [InlineData(HttpStatusCode.Forbidden, null, "call to external system revoke-credential failed with statuscode 403")] + public async Task RevokeCredentialForHolder_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.RevokeCredentialForHolder("https://test.de", "test123", "cl1", Guid.NewGuid(), 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/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs b/tests/processes/CredentialProcess.Library.Tests/CredentialCreationProcessHandlerTests.cs similarity index 97% rename from tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs rename to tests/processes/CredentialProcess.Library.Tests/CredentialCreationProcessHandlerTests.cs index d580a86b..ddf2f15e 100644 --- a/tests/processes/CredentialProcess.Library.Tests/CredentialProcessHandlerTests.cs +++ b/tests/processes/CredentialProcess.Library.Tests/CredentialCreationProcessHandlerTests.cs @@ -24,6 +24,7 @@ using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; using Org.Eclipse.TractusX.SsiCredentialIssuer.Callback.Service.Models; using Org.Eclipse.TractusX.SsiCredentialIssuer.Callback.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess; using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Models; using Org.Eclipse.TractusX.SsiCredentialIssuer.DBAccess.Repositories; @@ -35,7 +36,7 @@ namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests; -public class CredentialProcessHandlerTests +public class CredentialCreationProcessHandlerTests { private readonly Guid _credentialId = Guid.NewGuid(); @@ -43,11 +44,11 @@ public class CredentialProcessHandlerTests private readonly IIssuerRepositories _issuerRepositories; private readonly ICredentialRepository _credentialRepository; - private readonly CredentialProcessHandler _sut; + private readonly CredentialCreationProcessHandler _sut; private readonly IFixture _fixture; private readonly ICallbackService _callbackService; - public CredentialProcessHandlerTests() + public CredentialCreationProcessHandlerTests() { _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); _fixture.Behaviors.OfType().ToList() @@ -62,7 +63,7 @@ public CredentialProcessHandlerTests() _walletBusinessLogic = A.Fake(); _callbackService = A.Fake(); - _sut = new CredentialProcessHandler(_issuerRepositories, _walletBusinessLogic, _callbackService); + _sut = new CredentialCreationProcessHandler(_issuerRepositories, _walletBusinessLogic, _callbackService); } #region CreateCredential diff --git a/tests/processes/CredentialProcess.Library.Tests/CredentialExpiryProcessHandlerTests.cs b/tests/processes/CredentialProcess.Library.Tests/CredentialExpiryProcessHandlerTests.cs new file mode 100644 index 00000000..bd7021f1 --- /dev/null +++ b/tests/processes/CredentialProcess.Library.Tests/CredentialExpiryProcessHandlerTests.cs @@ -0,0 +1,210 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, 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.SsiCredentialIssuer.CredentialProcess.Library.Expiry; +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.Portal.Service.Models; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Portal.Service.Services; +using Org.Eclipse.TractusX.SsiCredentialIssuer.Wallet.Service.Services; +using System.Collections.Immutable; +using Xunit; + +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Tests; + +public class CredentialExpiryProcessHandlerTests +{ + private readonly Guid _credentialId = Guid.NewGuid(); + + private readonly IWalletService _walletService; + private readonly IIssuerRepositories _issuerRepositories; + private readonly ICredentialRepository _credentialRepository; + private readonly IPortalService _portalService; + + private readonly CredentialExpiryProcessHandler _sut; + private readonly IFixture _fixture; + private readonly IDocumentRepository _documentRepository; + + public CredentialExpiryProcessHandlerTests() + { + _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(); + _documentRepository = A.Fake(); + + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_credentialRepository); + A.CallTo(() => _issuerRepositories.GetInstance()).Returns(_documentRepository); + + _walletService = A.Fake(); + _portalService = A.Fake(); + + _sut = new CredentialExpiryProcessHandler(_issuerRepositories, _walletService, _portalService); + } + + #region RevokeCredential + + [Fact] + public async Task RevokeCredential_WithValidData_ReturnsExpected() + { + // Arrange + var externalCredentialId = Guid.NewGuid(); + var credential = new CompanySsiDetail(_credentialId, "Test", VerifiedCredentialTypeId.BUSINESS_PARTNER_NUMBER, CompanySsiDetailStatusId.ACTIVE, "Test123", Guid.NewGuid(), DateTimeOffset.UtcNow); + var document = _fixture + .Build() + .With(x => x.DocumentStatusId, DocumentStatusId.ACTIVE) + .Create(); + A.CallTo(() => _credentialRepository.GetRevocationDataById(_credentialId)) + .Returns(new ValueTuple>>(true, externalCredentialId, credential.CompanySsiDetailStatusId, Enumerable.Repeat(new ValueTuple(document.Id, document.DocumentStatusId), 1))); + A.CallTo(() => _credentialRepository.AttachAndModifyCredential(credential.Id, A>._, A>._)) + .Invokes((Guid _, Action? initialize, Action modify) => + { + initialize?.Invoke(credential); + modify(credential); + }); + + A.CallTo(() => _documentRepository.AttachAndModifyDocuments(A? Initialize, Action Modify)>>._)) + .Invokes((IEnumerable<(Guid DocumentId, Action? Initialize, Action Modify)> data) => + { + data.Select(x => + { + x.Initialize?.Invoke(document); + return document; + } + ).ToImmutableArray(); + data.Select(x => + { + x.Modify(document); + return document; + } + ).ToImmutableArray(); + }); + + // Act + var result = await _sut.RevokeCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _walletService.RevokeCredentialForIssuer(externalCredentialId, A._)) + .MustHaveHappenedOnceExactly(); + + credential.CompanySsiDetailStatusId.Should().Be(CompanySsiDetailStatusId.REVOKED); + document.DocumentStatusId.Should().Be(DocumentStatusId.INACTIVE); + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.TRIGGER_NOTIFICATION); + } + + [Fact] + public async Task RevokeCredential_WithNotExisting_ThrowsNotFoundException() + { + // Arrange + var externalCredentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetRevocationDataById(_credentialId)) + .Returns(new ValueTuple>>(false, null, default, Enumerable.Empty>())); + async Task Act() => await _sut.RevokeCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be($"Credential {_credentialId} does not exist"); + A.CallTo(() => _walletService.RevokeCredentialForIssuer(externalCredentialId, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task RevokeCredential_WithEmptyExternalCredentialId_ThrowsConflictException() + { + // Arrange + var externalCredentialId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetRevocationDataById(_credentialId)) + .Returns(new ValueTuple>>(true, null, default, Enumerable.Empty>())); + async Task Act() => await _sut.RevokeCredential(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be($"External Credential Id must be set for {_credentialId}"); + A.CallTo(() => _walletService.RevokeCredentialForIssuer(externalCredentialId, A._)) + .MustNotHaveHappened(); + } + + #endregion + + #region TriggerNotification + + [Fact] + public async Task TriggerNotification_WithValid_CallsExpected() + { + // Arrange + var requesterId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetCredentialNotificationData(_credentialId)) + .Returns(new ValueTuple(VerifiedCredentialTypeId.PCF_FRAMEWORK, requesterId)); + + // Act + var result = await _sut.TriggerNotification(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _portalService.AddNotification(A._, requesterId, NotificationTypeId.CREDENTIAL_REJECTED, A._)) + .MustHaveHappenedOnceExactly(); + result.modified.Should().BeFalse(); + result.processMessage.Should().BeNull(); + result.stepStatusId.Should().Be(ProcessStepStatusId.DONE); + result.nextStepTypeIds.Should().ContainSingle().Which.Should().Be(ProcessStepTypeId.TRIGGER_MAIL); + } + + #endregion + + #region TriggerMail + + [Fact] + public async Task TriggerMail_WithValid_CallsExpected() + { + // Arrange + var requesterId = Guid.NewGuid(); + A.CallTo(() => _credentialRepository.GetCredentialNotificationData(_credentialId)) + .Returns(new ValueTuple(VerifiedCredentialTypeId.PCF_FRAMEWORK, requesterId)); + + // Act + var result = await _sut.TriggerMail(_credentialId, CancellationToken.None).ConfigureAwait(false); + + // Assert + A.CallTo(() => _portalService.TriggerMail("CredentialRejected", requesterId, A>._, 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/CredentialProcessTypeExecutorTests.cs b/tests/processes/CredentialProcess.Worker.Tests/CredentialCreationProcessTypeExecutorTests.cs similarity index 90% rename from tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs rename to tests/processes/CredentialProcess.Worker.Tests/CredentialCreationProcessTypeExecutorTests.cs index 761d2289..4bfe9c34 100644 --- a/tests/processes/CredentialProcess.Worker.Tests/CredentialProcessTypeExecutorTests.cs +++ b/tests/processes/CredentialProcess.Worker.Tests/CredentialCreationProcessTypeExecutorTests.cs @@ -22,21 +22,22 @@ using FakeItEasy; using FluentAssertions; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; -using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Creation; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Creation; 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; +namespace Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Tests; -public class CredentialProcessTypeExecutorTests +public class CredentialCreationProcessTypeExecutorTests { - private readonly CredentialProcessTypeExecutor _sut; - private readonly ICredentialProcessHandler _credentialProcessHandler; + private readonly CredentialCreationProcessTypeExecutor _sut; + private readonly ICredentialCreationProcessHandler _credentialCreationProcessHandler; private readonly ICredentialRepository _credentialRepository; - public CredentialProcessTypeExecutorTests() + public CredentialCreationProcessTypeExecutorTests() { var fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); fixture.Behaviors.OfType().ToList() @@ -44,13 +45,13 @@ public CredentialProcessTypeExecutorTests() fixture.Behaviors.Add(new OmitOnRecursionBehavior()); var issuerRepositories = A.Fake(); - _credentialProcessHandler = A.Fake(); + _credentialCreationProcessHandler = A.Fake(); _credentialRepository = A.Fake(); A.CallTo(() => issuerRepositories.GetInstance()).Returns(_credentialRepository); - _sut = new CredentialProcessTypeExecutor(issuerRepositories, _credentialProcessHandler); + _sut = new CredentialCreationProcessTypeExecutor(issuerRepositories, _credentialCreationProcessHandler); } [Fact] @@ -155,7 +156,7 @@ public async Task ExecuteProcessStep_WithValidData_CallsExpected() initializeResult.ScheduleStepTypeIds.Should().BeNull(); // Arrange - A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + A.CallTo(() => _credentialCreationProcessHandler.CreateCredential(credentialId, A._)) .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); // Act @@ -186,7 +187,7 @@ public async Task ExecuteProcessStep_WithRecoverableServiceException_ReturnsToDo initializeResult.ScheduleStepTypeIds.Should().BeNull(); // Arrange - A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + A.CallTo(() => _credentialCreationProcessHandler.CreateCredential(credentialId, A._)) .Throws(new ServiceException("this is a test", true)); // Act @@ -217,7 +218,7 @@ public async Task ExecuteProcessStep_WithServiceException_ReturnsFailedAndRetrig initializeResult.ScheduleStepTypeIds.Should().BeNull(); // Arrange - A.CallTo(() => _credentialProcessHandler.CreateCredential(credentialId, A._)) + A.CallTo(() => _credentialCreationProcessHandler.CreateCredential(credentialId, A._)) .Throws(new ServiceException("this is a test")); // Act diff --git a/tests/processes/CredentialProcess.Worker.Tests/CredentialExpiryProcessTypeExecutorTests.cs b/tests/processes/CredentialProcess.Worker.Tests/CredentialExpiryProcessTypeExecutorTests.cs new file mode 100644 index 00000000..724f9e46 --- /dev/null +++ b/tests/processes/CredentialProcess.Worker.Tests/CredentialExpiryProcessTypeExecutorTests.cs @@ -0,0 +1,236 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, 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.SsiCredentialIssuer.CredentialProcess.Library.Creation; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Library.Expiry; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Creation; +using Org.Eclipse.TractusX.SsiCredentialIssuer.CredentialProcess.Worker.Expiry; +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.Worker.Tests; + +public class CredentialExpiryProcessTypeExecutorTests +{ + private readonly CredentialExpiryProcessTypeExecutor _sut; + private readonly ICredentialExpiryProcessHandler _credentialExpiryProcessHandler; + private readonly ICredentialRepository _credentialRepository; + + public CredentialExpiryProcessTypeExecutorTests() + { + 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(); + _credentialExpiryProcessHandler = A.Fake(); + + _credentialRepository = A.Fake(); + + A.CallTo(() => issuerRepositories.GetInstance()).Returns(_credentialRepository); + + _sut = new CredentialExpiryProcessTypeExecutor(issuerRepositories, _credentialExpiryProcessHandler); + } + + [Fact] + public void GetProcessTypeId_ReturnsExpected() + { + // Assert + _sut.GetProcessTypeId().Should().Be(ProcessTypeId.DECLINE_CREDENTIAL); + } + + [Fact] + public void IsExecutableStepTypeId_WithValid_ReturnsExpected() + { + // Assert + _sut.IsExecutableStepTypeId(ProcessStepTypeId.REVOKE_CREDENTIAL).Should().BeTrue(); + } + + [Fact] + public void GetExecutableStepTypeIds_ReturnsExpected() + { + // Assert + _sut.GetExecutableStepTypeIds().Should().HaveCount(3).And.Satisfy( + x => x == ProcessStepTypeId.REVOKE_CREDENTIAL, + x => x == ProcessStepTypeId.TRIGGER_MAIL, + x => x == ProcessStepTypeId.TRIGGER_NOTIFICATION); + } + + [Fact] + public async Task IsLockRequested_ReturnsExpected() + { + // Act + var result = await _sut.IsLockRequested(ProcessStepTypeId.REVOKE_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(() => _credentialExpiryProcessHandler.RevokeCredential(credentialId, A._)) + .Returns(new ValueTuple?, ProcessStepStatusId, bool, string?>(null, ProcessStepStatusId.DONE, false, null)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.REVOKE_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(() => _credentialExpiryProcessHandler.RevokeCredential(credentialId, A._)) + .Throws(new ServiceException("this is a test", true)); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.REVOKE_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(() => _credentialExpiryProcessHandler.RevokeCredential(credentialId, A._)) + .Throws(new ServiceException("this is a test")); + + // Act + var result = await _sut.ExecuteProcessStep(ProcessStepTypeId.REVOKE_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 +}