From 8caaca3e8c84f50c8d7a4ace230644ccf751034e Mon Sep 17 00:00:00 2001 From: Phil Schneider Date: Tue, 10 Oct 2023 10:16:26 +0200 Subject: [PATCH] chore: adjust process worker workflow (#276) * fix(n2n): create process steps on submit * feat(n2n): move submit endpoint to registration service * chore: adjust copyright header --------- Refs: CPLP-3197 Co-authored-by: Norbert Truchsess Reviewed-by: Norbert Truchsess --- .../BusinessLogic/INetworkBusinessLogic.cs | 3 - .../BusinessLogic/NetworkBusinessLogic.cs | 70 ---- .../Controllers/NetworkController.cs | 29 +- .../Models/PartnerSubmitData.cs | 9 - .../Repositories/INetworkRepository.cs | 2 +- .../Repositories/NetworkRepository.cs | 6 +- .../ApplicationChecklistExtensions.cs | 12 +- .../BusinessLogic/INetworkBusinessLogic.cs | 27 ++ .../BusinessLogic/NetworkBusinessLogic.cs | 111 +++++ .../Controllers/NetworkController.cs | 68 ++++ .../Model/PartnerSubmitData.cs | 40 ++ .../Registration.Service.csproj | 145 +++---- .../NetworkBusinessLogicTests.cs | 246 +----------- .../Controllers/NetworkControllerTests.cs | 16 - .../NetworkRepositoryTests.cs | 17 +- .../NetworkBusinessLogicTests.cs | 379 ++++++++++++++++++ .../Controller/NetworkControllerTests.cs | 57 +++ .../Registration.Service.Tests.csproj | 111 ++--- 18 files changed, 829 insertions(+), 519 deletions(-) delete mode 100644 src/administration/Administration.Service/Models/PartnerSubmitData.cs create mode 100644 src/registration/Registration.Service/BusinessLogic/INetworkBusinessLogic.cs create mode 100644 src/registration/Registration.Service/BusinessLogic/NetworkBusinessLogic.cs create mode 100644 src/registration/Registration.Service/Controllers/NetworkController.cs create mode 100644 src/registration/Registration.Service/Model/PartnerSubmitData.cs create mode 100644 tests/registration/Registration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs create mode 100644 tests/registration/Registration.Service.Tests/Controller/NetworkControllerTests.cs diff --git a/src/administration/Administration.Service/BusinessLogic/INetworkBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/INetworkBusinessLogic.cs index 180e8df421..5446b619d4 100644 --- a/src/administration/Administration.Service/BusinessLogic/INetworkBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/INetworkBusinessLogic.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -26,7 +25,5 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLog public interface INetworkBusinessLogic { Task HandlePartnerRegistration(PartnerRegistrationData data); - Task RetriggerProcessStep(Guid externalId, ProcessStepTypeId processStepTypeId); - Task Submit(PartnerSubmitData submitData, CancellationToken cancellationToken); } diff --git a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs index 84e8f3c8cd..a181587251 100644 --- a/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/NetworkBusinessLogic.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -33,7 +32,6 @@ using Org.Eclipse.TractusX.Portal.Backend.Processes.NetworkRegistration.Library; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Models; using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; -using System.Collections.Immutable; using System.Text.RegularExpressions; namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLogic; @@ -307,72 +305,4 @@ private static void ValidateUsers(UserDetailData user) throw new ControllerArgumentException("Lastname does not match expected format"); } } - - public async Task Submit(PartnerSubmitData submitData, CancellationToken cancellationToken) - { - var companyId = _identityService.IdentityData.CompanyId; - var userId = _identityService.IdentityData.UserId; - var userRoleIds = await _portalRepositories.GetInstance() - .GetUserRoleIdsUntrackedAsync(_settings.InitialRoles) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); - var data = await _portalRepositories.GetInstance() - .GetSubmitData(companyId, userId, userRoleIds) - .ConfigureAwait(false); - if (!data.Exists) - { - throw new NotFoundException($"Company {companyId} not found"); - } - - if (!data.IsUserInRole) - { - throw new ForbiddenException($"User must be in role {string.Join(",", _settings.InitialRoles.SelectMany(x => x.UserRoleNames))}"); - } - - if (data.CompanyApplications.Count() != 1) - { - throw new ConflictException($"Company {companyId} has no or more than one application"); - } - - if (data.ProcessId == null) - { - throw new ConflictException("There must be an process"); - } - - var companyApplication = data.CompanyApplications.Single(); - if (companyApplication.CompanyApplicationStatusId != CompanyApplicationStatusId.CREATED) - { - throw new ConflictException($"Application {companyApplication.CompanyApplicationId} is not in state CREATED"); - } - - submitData.Agreements.Where(x => x.ConsentStatusId != ConsentStatusId.ACTIVE).IfAny(inactive => - throw new ControllerArgumentException($"All agreements must be agreed to. Agreements that are not active: {string.Join(",", inactive.Select(x => x.AgreementId))}", nameof(submitData.Agreements))); - - data.CompanyRoleAgreementIds - .ExceptBy(submitData.CompanyRoles, x => x.CompanyRoleId) - .IfAny(missing => - throw new ControllerArgumentException($"CompanyRoles {string.Join(",", missing.Select(x => x.CompanyRoleId))} are missing", nameof(submitData.CompanyRoles))); - - var requiredAgreementIds = data.CompanyRoleAgreementIds - .SelectMany(x => x.AgreementIds) - .Distinct().ToImmutableList(); - - requiredAgreementIds.Except(submitData.Agreements.Where(x => x.ConsentStatusId == ConsentStatusId.ACTIVE).Select(x => x.AgreementId)) - .IfAny(missing => - throw new ControllerArgumentException($"All Agreements for the company roles must be agreed to, missing agreementIds: {string.Join(",", missing)}", nameof(submitData.Agreements))); - - _portalRepositories.GetInstance() - .CreateConsents(requiredAgreementIds.Select(agreementId => (agreementId, companyId, userId, ConsentStatusId.ACTIVE))); - - var processId = _portalRepositories.GetInstance().CreateProcess(ProcessTypeId.APPLICATION_CHECKLIST).Id; - _portalRepositories.GetInstance().AttachAndModifyCompanyApplication(companyApplication.CompanyApplicationId, - ca => - { - ca.ApplicationStatusId = CompanyApplicationStatusId.SUBMITTED; - ca.ChecklistProcessId = processId; - }); - _portalRepositories.GetInstance().CreateProcessStepRange(Enumerable.Repeat(new ValueTuple(ProcessStepTypeId.TRIGGER_CALLBACK_OSP_SUBMITTED, ProcessStepStatusId.TODO, data.ProcessId.Value), 1)); - - await _portalRepositories.SaveAsync().ConfigureAwait(false); - } } diff --git a/src/administration/Administration.Service/Controllers/NetworkController.cs b/src/administration/Administration.Service/Controllers/NetworkController.cs index 0c9d0fd64d..c0421c6977 100644 --- a/src/administration/Administration.Service/Controllers/NetworkController.cs +++ b/src/administration/Administration.Service/Controllers/NetworkController.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -70,7 +69,7 @@ public async Task PartnerRegister([FromBody] PartnerRegistrationData d /// Empty response on success. /// No registration found for the externalId. [HttpPost] - //[Authorize(Roles = "tbd")] + [Authorize(Roles = "approve_new_partner")] [Authorize(Policy = PolicyTypes.CompanyUser)] [Route("{externalId}/retrigger-synchronize-users")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -82,28 +81,6 @@ public async Task RetriggerSynchronizeUser([FromRoute] Guid ext return NoContent(); } - /// - /// Submits the application - /// - /// The agreements for the companyRoles - /// Cancellation Token - /// NoContent - /// Example: POST: api/administration/registration/network/partnerRegistration/submit - /// Empty response on success. - /// No registration found for the externalId. - [HttpPost] - [Authorize(Roles = "submit_registration")] - [Authorize(Policy = PolicyTypes.CompanyUser)] - [Route("partnerRegistration/submit")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] - public async Task Submit([FromBody] PartnerSubmitData data, CancellationToken cancellationToken) - { - await _logic.Submit(data, cancellationToken).ConfigureAwait(false); - return NoContent(); - } - /// /// Retriggers the last failed step /// @@ -113,7 +90,7 @@ public async Task Submit([FromBody] PartnerSubmitData data, Can /// Empty response on success. /// No registration found for the externalId. [HttpPost] - //[Authorize(Roles = "tbd")] + [Authorize(Roles = "approve_new_partner")] [Authorize(Policy = PolicyTypes.CompanyUser)] [Route("{externalId}/retrigger-callback-osp-approve")] [ProducesResponseType(StatusCodes.Status204NoContent)] @@ -155,7 +132,7 @@ public async Task RetriggerCallbackOspDecline([FromRoute] Guid /// Empty response on success. /// No registration found for the externalId. [HttpPost] - //[Authorize(Roles = "tbd")] + [Authorize(Roles = "approve_new_partner")] [Authorize(Policy = PolicyTypes.CompanyUser)] [Route("{externalId}/retrigger-callback-osp-submitted")] [ProducesResponseType(StatusCodes.Status204NoContent)] diff --git a/src/administration/Administration.Service/Models/PartnerSubmitData.cs b/src/administration/Administration.Service/Models/PartnerSubmitData.cs deleted file mode 100644 index 6c91c94cb3..0000000000 --- a/src/administration/Administration.Service/Models/PartnerSubmitData.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; - -namespace Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models; - -public record PartnerSubmitData -( - IEnumerable CompanyRoles, - IEnumerable Agreements -); diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs index 12c03b8306..90a6f94073 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/INetworkRepository.cs @@ -30,6 +30,6 @@ public interface INetworkRepository Task CheckExternalIdExists(Guid externalId, Guid onboardingServiceProviderId); Task GetNetworkRegistrationDataForProcessIdAsync(Guid processId); Task<(bool RegistrationIdExists, VerifyProcessData processData)> IsValidRegistration(Guid externalId, IEnumerable processStepTypeIds); - Task<(bool Exists, IEnumerable<(Guid CompanyApplicationId, CompanyApplicationStatusId CompanyApplicationStatusId, string? CallbackUrl)> CompanyApplications, bool IsUserInRole, IEnumerable<(CompanyRoleId CompanyRoleId, IEnumerable AgreementIds)> CompanyRoleAgreementIds, Guid? ProcessId)> GetSubmitData(Guid companyId, Guid userId, IEnumerable roleIds); + Task<(bool Exists, IEnumerable<(Guid CompanyApplicationId, CompanyApplicationStatusId CompanyApplicationStatusId, string? CallbackUrl)> CompanyApplications, IEnumerable<(CompanyRoleId CompanyRoleId, IEnumerable AgreementIds)> CompanyRoleAgreementIds, Guid? ProcessId)> GetSubmitData(Guid companyId); Task<(OspDetails? OspDetails, Guid? ExternalId, string? Bpn, Guid ApplicationId, IEnumerable Comments)> GetCallbackData(Guid networkRegistrationId, ProcessStepTypeId processStepTypeId); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs index 5ab7108cdd..45cf0f094e 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/NetworkRepository.cs @@ -66,11 +66,11 @@ public Task GetNetworkRegistrationDataForProcessIdAsync(Guid processId) => )) .SingleOrDefaultAsync(); - public Task<(bool Exists, IEnumerable<(Guid CompanyApplicationId, CompanyApplicationStatusId CompanyApplicationStatusId, string? CallbackUrl)> CompanyApplications, bool IsUserInRole, IEnumerable<(CompanyRoleId CompanyRoleId, IEnumerable AgreementIds)> CompanyRoleAgreementIds, Guid? ProcessId)> GetSubmitData(Guid companyId, Guid userId, IEnumerable roleIds) => + public Task<(bool Exists, IEnumerable<(Guid CompanyApplicationId, CompanyApplicationStatusId CompanyApplicationStatusId, string? CallbackUrl)> CompanyApplications, IEnumerable<(CompanyRoleId CompanyRoleId, IEnumerable AgreementIds)> CompanyRoleAgreementIds, Guid? ProcessId)> GetSubmitData(Guid companyId) => _context.Companies .AsSplitQuery() .Where(x => x.Id == companyId) - .Select(x => new ValueTuple, bool, IEnumerable<(CompanyRoleId, IEnumerable)>, Guid?>( + .Select(x => new ValueTuple, IEnumerable<(CompanyRoleId, IEnumerable)>, Guid?>( true, x.CompanyApplications .Where(ca => ca.CompanyApplicationTypeId == CompanyApplicationTypeId.EXTERNAL) @@ -78,8 +78,6 @@ public Task GetNetworkRegistrationDataForProcessIdAsync(Guid processId) => ca.Id, ca.ApplicationStatusId, ca.OnboardingServiceProvider!.OnboardingServiceProviderDetail!.CallbackUrl)), - x.Identities - .Any(i => i.Id == userId && i.IdentityAssignedRoles.Any(roles => roleIds.Any(r => r == roles.UserRoleId))), x.CompanyAssignedRoles.Select(assigned => new ValueTuple>( assigned.CompanyRoleId, assigned.CompanyRole!.AgreementAssignedCompanyRoles.Select(a => a.AgreementId))), diff --git a/src/processes/ApplicationChecklist.Config/ApplicationChecklistExtensions.cs b/src/processes/ApplicationChecklist.Config/ApplicationChecklistExtensions.cs index 1b25a887cb..f0c60d92c9 100644 --- a/src/processes/ApplicationChecklist.Config/ApplicationChecklistExtensions.cs +++ b/src/processes/ApplicationChecklist.Config/ApplicationChecklistExtensions.cs @@ -31,20 +31,16 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Con public static class ApplicationChecklistExtensions { - public static IServiceCollection AddApplicationChecklist(this IServiceCollection services, IConfigurationSection section) - { - return services + public static IServiceCollection AddApplicationChecklist(this IServiceCollection services, IConfigurationSection section) => + services .AddTransient() .AddTransient() .AddBpdmService(section.GetSection("Bpdm")) .AddCustodianService(section.GetSection("Custodian")) .AddClearinghouseService(section.GetSection("Clearinghouse")) .AddSdFactoryService(section.GetSection("SdFactory")); - } - public static IServiceCollection AddApplicationChecklistCreation(this IServiceCollection services) - { - return services + public static IServiceCollection AddApplicationChecklistCreation(this IServiceCollection services) => + services .AddTransient(); - } } diff --git a/src/registration/Registration.Service/BusinessLogic/INetworkBusinessLogic.cs b/src/registration/Registration.Service/BusinessLogic/INetworkBusinessLogic.cs new file mode 100644 index 0000000000..b6092212c6 --- /dev/null +++ b/src/registration/Registration.Service/BusinessLogic/INetworkBusinessLogic.cs @@ -0,0 +1,27 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.BusinessLogic; + +public interface INetworkBusinessLogic +{ + Task Submit(PartnerSubmitData submitData, CancellationToken cancellationToken); +} diff --git a/src/registration/Registration.Service/BusinessLogic/NetworkBusinessLogic.cs b/src/registration/Registration.Service/BusinessLogic/NetworkBusinessLogic.cs new file mode 100644 index 0000000000..24a7b61e70 --- /dev/null +++ b/src/registration/Registration.Service/BusinessLogic/NetworkBusinessLogic.cs @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Linq; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Identities; +using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.BusinessLogic; + +public class NetworkBusinessLogic : INetworkBusinessLogic +{ + private readonly IPortalRepositories _portalRepositories; + private readonly IIdentityService _identityService; + private readonly IApplicationChecklistCreationService _checklistService; + + public NetworkBusinessLogic(IPortalRepositories portalRepositories, IIdentityService identityService, IApplicationChecklistCreationService checklistService) + { + _portalRepositories = portalRepositories; + _identityService = identityService; + _checklistService = checklistService; + } + + public async Task Submit(PartnerSubmitData submitData, CancellationToken cancellationToken) + { + var companyId = _identityService.IdentityData.CompanyId; + var userId = _identityService.IdentityData.UserId; + var data = await _portalRepositories.GetInstance() + .GetSubmitData(companyId) + .ConfigureAwait(false); + if (!data.Exists) + { + throw new NotFoundException($"Company {companyId} not found"); + } + + if (data.CompanyApplications.Count() != 1) + { + throw new ConflictException($"Company {companyId} has no or more than one application"); + } + + if (data.ProcessId == null) + { + throw new ConflictException("There must be an process"); + } + + var companyApplication = data.CompanyApplications.Single(); + if (companyApplication.CompanyApplicationStatusId != CompanyApplicationStatusId.CREATED) + { + throw new ConflictException($"Application {companyApplication.CompanyApplicationId} is not in state CREATED"); + } + + submitData.Agreements.Where(x => x.ConsentStatusId != ConsentStatusId.ACTIVE).IfAny(inactive => + throw new ControllerArgumentException($"All agreements must be agreed to. Agreements that are not active: {string.Join(",", inactive.Select(x => x.AgreementId))}", nameof(submitData.Agreements))); + + data.CompanyRoleAgreementIds + .ExceptBy(submitData.CompanyRoles, x => x.CompanyRoleId) + .IfAny(missing => + throw new ControllerArgumentException($"CompanyRoles {string.Join(",", missing.Select(x => x.CompanyRoleId))} are missing", nameof(submitData.CompanyRoles))); + + var requiredAgreementIds = data.CompanyRoleAgreementIds + .SelectMany(x => x.AgreementIds) + .Distinct().ToImmutableList(); + + requiredAgreementIds.Except(submitData.Agreements.Where(x => x.ConsentStatusId == ConsentStatusId.ACTIVE).Select(x => x.AgreementId)) + .IfAny(missing => + throw new ControllerArgumentException($"All Agreements for the company roles must be agreed to, missing agreementIds: {string.Join(",", missing)}", nameof(submitData.Agreements))); + + _portalRepositories.GetInstance() + .CreateConsents(requiredAgreementIds.Select(agreementId => (agreementId, companyId, userId, ConsentStatusId.ACTIVE))); + + var entries = await _checklistService.CreateInitialChecklistAsync(companyApplication.CompanyApplicationId); + var processId = _portalRepositories.GetInstance().CreateProcess(ProcessTypeId.APPLICATION_CHECKLIST).Id; + _portalRepositories.GetInstance() + .CreateProcessStepRange( + _checklistService + .GetInitialProcessStepTypeIds(entries) + .Select(processStepTypeId => (processStepTypeId, ProcessStepStatusId.TODO, processId)) + // in addition to the initial steps of new process application_checklist also create next step for process network_registration + .Append((ProcessStepTypeId.TRIGGER_CALLBACK_OSP_SUBMITTED, ProcessStepStatusId.TODO, data.ProcessId.Value))); + + _portalRepositories.GetInstance().AttachAndModifyCompanyApplication(companyApplication.CompanyApplicationId, + ca => + { + ca.ApplicationStatusId = CompanyApplicationStatusId.SUBMITTED; + ca.ChecklistProcessId = processId; + }); + + await _portalRepositories.SaveAsync().ConfigureAwait(false); + } +} diff --git a/src/registration/Registration.Service/Controllers/NetworkController.cs b/src/registration/Registration.Service/Controllers/NetworkController.cs new file mode 100644 index 0000000000..cffb43c33f --- /dev/null +++ b/src/registration/Registration.Service/Controllers/NetworkController.cs @@ -0,0 +1,68 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 BMW Group AG + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.BusinessLogic; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Controllers; + +[ApiController] +[Route("api/registration/[controller]")] +[Produces("application/json")] +[Consumes("application/json")] +public class NetworkController : ControllerBase +{ + private readonly INetworkBusinessLogic _logic; + + /// + /// Creates a new instance of + /// + /// The business logic for the registration + public NetworkController(INetworkBusinessLogic logic) + { + _logic = logic; + } + + /// + /// Submits the application + /// + /// The agreements for the companyRoles + /// Cancellation Token + /// NoContent + /// Example: POST: api/administration/registration/network/partnerRegistration/submit + /// Empty response on success. + /// No registration found for the externalId. + [HttpPost] + [Authorize(Roles = "submit_registration")] + [Authorize(Policy = PolicyTypes.CompanyUser)] + [Route("partnerRegistration/submit")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + public async Task Submit([FromBody] PartnerSubmitData data, CancellationToken cancellationToken) + { + await _logic.Submit(data, cancellationToken).ConfigureAwait(false); + return NoContent(); + } +} diff --git a/src/registration/Registration.Service/Model/PartnerSubmitData.cs b/src/registration/Registration.Service/Model/PartnerSubmitData.cs new file mode 100644 index 0000000000..b8d10b5d27 --- /dev/null +++ b/src/registration/Registration.Service/Model/PartnerSubmitData.cs @@ -0,0 +1,40 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; +using System.Text.Json.Serialization; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; + +public record PartnerSubmitData +( + IEnumerable CompanyRoles, + IEnumerable Agreements +); + +/// +/// +/// +/// +/// +/// +public record AgreementConsentData( + Guid AgreementId, + [property: JsonPropertyName("consentStatus")] ConsentStatusId ConsentStatusId +); diff --git a/src/registration/Registration.Service/Registration.Service.csproj b/src/registration/Registration.Service/Registration.Service.csproj index 6e83605fc3..8d1158a692 100644 --- a/src/registration/Registration.Service/Registration.Service.csproj +++ b/src/registration/Registration.Service/Registration.Service.csproj @@ -1,72 +1,73 @@ - - - - - - Org.Eclipse.TractusX.Portal.Backend.Registration.Service - net7.0 - enable - enable - 1557fa58-6743-480f-8f98-155d33f89c0a - Linux - ..\..\.. - True - CS1591 - Org.Eclipse.TractusX.Portal.Backend.Registration.Service - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - - - - Program.cs - - - - + + + + + + Org.Eclipse.TractusX.Portal.Backend.Registration.Service + Org.Eclipse.TractusX.Portal.Backend.Registration.Service + net7.0 + enable + enable + 1557fa58-6743-480f-8f98-155d33f89c0a + Linux + ..\..\.. + True + CS1591 + Org.Eclipse.TractusX.Portal.Backend.Registration.Service + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + + Program.cs + + + + diff --git a/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs b/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs index 0dffdb26cb..d449c14eed 100644 --- a/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs +++ b/tests/administration/Administration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -64,8 +63,6 @@ public class NetworkBusinessLogicTests private readonly IIdentityProviderRepository _identityProviderRepository; private readonly ICountryRepository _countryRepository; private readonly NetworkBusinessLogic _sut; - private readonly PartnerRegistrationSettings _settings; - private readonly IConsentRepository _consentRepository; public NetworkBusinessLogicTests() { @@ -87,19 +84,17 @@ public NetworkBusinessLogicTests() _networkRepository = A.Fake(); _identityProviderRepository = A.Fake(); _countryRepository = A.Fake(); - _consentRepository = A.Fake(); - _settings = new PartnerRegistrationSettings + var settings = new PartnerRegistrationSettings { InitialRoles = new[] { new UserRoleConfig("cl1", new[] { "Company Admin" }) } }; var options = A.Fake>(); - A.CallTo(() => options.Value).Returns(_settings); + A.CallTo(() => options.Value).Returns(settings); A.CallTo(() => _identityService.IdentityData).Returns(_identity); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_companyRepository); - A.CallTo(() => _portalRepositories.GetInstance()).Returns(_consentRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_companyRolesRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_processStepRepository); A.CallTo(() => _portalRepositories.GetInstance()).Returns(_applicationRepository); @@ -382,9 +377,6 @@ public async Task HandlePartnerRegistration_WithUserCreationThrowsException_Thro public async Task HandlePartnerRegistration_WithSingleIdpWithoutAlias_ThrowsServiceException() { // Arrange - var newCompanyId = Guid.NewGuid(); - var processId = Guid.NewGuid(); - var data = new PartnerRegistrationData( Guid.NewGuid(), "Test N2N", @@ -700,240 +692,6 @@ public async Task RetriggerSynchronizeUser_CallsExpected() #endregion - #region Submit - - [Fact] - public async Task Submit_WithNotExistingSubmitData_ThrowsNotFoundException() - { - // Arrange - var data = _fixture.Create(); - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns(new ValueTuple>, bool, IEnumerable>>, Guid?>()); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"Company {_identity.CompanyId} not found"); - } - - [Fact] - public async Task Submit_WithUserNotInRole_ThrowsForbiddenException() - { - // Arrange - var data = _fixture.Create(); - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Empty>(), false, Enumerable.Empty>>(), null)); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"User must be in role {string.Join(",", _settings.InitialRoles.SelectMany(x => x.UserRoleNames))}"); - } - - [Fact] - public async Task Submit_WithoutCompanyApplications_ThrowsConflictException() - { - // Arrange - var data = _fixture.Create(); - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Empty>(), true, Enumerable.Empty>>(), null)); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"Company {_identity.CompanyId} has no or more than one application"); - } - - [Fact] - public async Task Submit_WithMultipleCompanyApplications_ThrowsConflictException() - { - // Arrange - var data = _fixture.Create(); - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, _fixture.CreateMany>(2), true, Enumerable.Empty>>(), null)); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"Company {_identity.CompanyId} has no or more than one application"); - } - - [Fact] - public async Task Submit_WithWrongApplicationStatus_ThrowsConflictException() - { - // Arrange - var applicationId = Guid.NewGuid(); - var data = _fixture.Create(); - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.VERIFY, null), 1), true, Enumerable.Empty>>(), Guid.NewGuid())); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"Application {applicationId} is not in state CREATED"); - } - - [Fact] - public async Task Submit_WithOneMissingAgreement_ThrowsConflictException() - { - // Arrange - var applicationId = Guid.NewGuid(); - var agreementId = Guid.NewGuid(); - var notExistingAgreementId = Guid.NewGuid(); - var data = new PartnerSubmitData( - new[] { CompanyRoleId.APP_PROVIDER }, - new[] { new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE) }); - var companyRoleIds = new ValueTuple>[] - { - (CompanyRoleId.APP_PROVIDER, new [] {agreementId, notExistingAgreementId}) - }; - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, null), 1), true, companyRoleIds, Guid.NewGuid())); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"All Agreements for the company roles must be agreed to, missing agreementIds: {notExistingAgreementId} (Parameter 'Agreements')"); - } - - [Fact] - public async Task Submit_WithOneInactiveAgreement_ThrowsConflictException() - { - // Arrange - var applicationId = Guid.NewGuid(); - var agreementId = Guid.NewGuid(); - var inactiveAgreementId = Guid.NewGuid(); - var data = new PartnerSubmitData( - new[] { CompanyRoleId.APP_PROVIDER }, - new[] - { - new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), - new AgreementConsentData(inactiveAgreementId, ConsentStatusId.INACTIVE), - }); - var companyRoleIds = new ValueTuple>[] - { - (CompanyRoleId.APP_PROVIDER, new [] {agreementId, inactiveAgreementId}) - }; - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, null), 1), true, companyRoleIds, Guid.NewGuid())); - - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be($"All agreements must be agreed to. Agreements that are not active: {inactiveAgreementId} (Parameter 'Agreements')"); - } - - [Fact] - public async Task Submit_WithoutProcessId_ThrowsConflictException() - { - // Arrange - var applicationId = Guid.NewGuid(); - var agreementId = Guid.NewGuid(); - var agreementId1 = Guid.NewGuid(); - var data = new PartnerSubmitData( - new[] { CompanyRoleId.APP_PROVIDER }, - new[] - { - new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), - new AgreementConsentData(agreementId1, ConsentStatusId.ACTIVE), - }); - var companyRoleIds = new ValueTuple>[] - { - (CompanyRoleId.APP_PROVIDER, new [] {agreementId, agreementId1}) - }; - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, "https://callback.url"), 1), true, companyRoleIds, null)); - // Act - async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - var ex = await Assert.ThrowsAsync(Act); - ex.Message.Should().Be("There must be an process"); - } - - [Fact] - public async Task Submit_WithValidData_CallsExpected() - { - // Arrange - var applicationId = Guid.NewGuid(); - var agreementId = Guid.NewGuid(); - var agreementId1 = Guid.NewGuid(); - var processSteps = new List(); - var application = new CompanyApplication(applicationId, _identity.CompanyId, CompanyApplicationStatusId.CREATED, CompanyApplicationTypeId.EXTERNAL, DateTimeOffset.UtcNow); - - var data = new PartnerSubmitData( - new[] { CompanyRoleId.APP_PROVIDER }, - new[] - { - new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), - new AgreementConsentData(agreementId1, ConsentStatusId.ACTIVE), - }); - var companyRoleIds = new ValueTuple>[] - { - (CompanyRoleId.APP_PROVIDER, new [] {agreementId, agreementId1}) - }; - A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId, _identity.UserId, A>._)) - .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, "https://callback.url"), 1), true, companyRoleIds, Guid.NewGuid())); - A.CallTo(() => _applicationRepository.AttachAndModifyCompanyApplication(applicationId, A>._)) - .Invokes((Guid _, Action setOptionalFields) => - { - setOptionalFields.Invoke(application); - }); - A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>.That.Matches(x => - x.Count() == 1 && - x.Single().ProcessStepTypeId == ProcessStepTypeId.TRIGGER_CALLBACK_OSP_SUBMITTED))) - .Invokes((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> steps) => - { - processSteps.AddRange(steps.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow))); - }); - var consents = new List(); - var now = DateTimeOffset.UtcNow; - A.CallTo(() => _consentRepository.CreateConsents(A>._)) - .Invokes((IEnumerable<(Guid AgreementId, Guid CompanyId, Guid CompanyUserId, ConsentStatusId ConsentStatusId)> agreementConsents) => - { - foreach (var x in agreementConsents) - { - consents.Add(new Consent(Guid.NewGuid(), x.AgreementId, x.CompanyId, x.CompanyUserId, x.ConsentStatusId, now)); - } - }); - // Act - await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - application.ApplicationStatusId.Should().Be(CompanyApplicationStatusId.SUBMITTED); - A.CallTo(() => _consentRepository.CreateConsents(A>._)) - .MustHaveHappenedOnceExactly(); - consents.Should().HaveCount(2) - .And.AllSatisfy(x => x.Should().Match(x => - x.CompanyId == _identity.CompanyId && - x.CompanyUserId == _identity.UserId && - x.ConsentStatusId == ConsentStatusId.ACTIVE)) - .And.Satisfy( - x => x.AgreementId == agreementId, - x => x.AgreementId == agreementId1); - A.CallTo(() => _portalRepositories.SaveAsync()) - .MustHaveHappenedOnceExactly(); - processSteps.Should().ContainSingle().Which.Should().Match(x => - x.ProcessStepTypeId == ProcessStepTypeId.TRIGGER_CALLBACK_OSP_SUBMITTED && - x.ProcessStepStatusId == ProcessStepStatusId.TODO); - } - - #endregion - #region Setup private void SetupRepos() diff --git a/tests/administration/Administration.Service.Tests/Controllers/NetworkControllerTests.cs b/tests/administration/Administration.Service.Tests/Controllers/NetworkControllerTests.cs index 1e5a216545..e3984f2914 100644 --- a/tests/administration/Administration.Service.Tests/Controllers/NetworkControllerTests.cs +++ b/tests/administration/Administration.Service.Tests/Controllers/NetworkControllerTests.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -67,21 +66,6 @@ public async Task RetriggerSynchronizeUser_ReturnsExpected() .MustHaveHappenedOnceExactly(); } - [Fact] - public async Task Submit_ReturnsExpected() - { - // Arrange - var data = _fixture.Create(); - - // Act - var result = await this._controller.Submit(data, CancellationToken.None).ConfigureAwait(false); - - // Assert - result.StatusCode.Should().Be(204); - A.CallTo(() => _logic.Submit(A._, A._)) - .MustHaveHappenedOnceExactly(); - } - [Fact] public async Task RetriggerCallbackOspApprove_ReturnsExpected() { diff --git a/tests/portalbackend/PortalBackend.DBAccess.Tests/NetworkRepositoryTests.cs b/tests/portalbackend/PortalBackend.DBAccess.Tests/NetworkRepositoryTests.cs index 44d830d9ff..32da103345 100644 --- a/tests/portalbackend/PortalBackend.DBAccess.Tests/NetworkRepositoryTests.cs +++ b/tests/portalbackend/PortalBackend.DBAccess.Tests/NetworkRepositoryTests.cs @@ -162,16 +162,12 @@ public async Task GetSubmitData_WithoutNetworkRegistration_ReturnsExpected() var sut = await CreateSut().ConfigureAwait(false); // Act - var result = await sut.GetSubmitData(new Guid("729e0af2-6723-4a7f-85a1-833d84b39bdf"), new Guid("8b42e6de-7b59-4217-a63c-198e83d93776"), Enumerable.Repeat(new Guid("aabcdfeb-6669-4c74-89f0-19cda090873e"), 1)).ConfigureAwait(false); + var result = await sut.GetSubmitData(new Guid("3390c2d7-75c1-4169-aa27-6ce00e1f3cdd")).ConfigureAwait(false); // Assert - result.Should().NotBe(default); - result.Exists.Should().Be(true); - result.IsUserInRole.Should().Be(false); - result.CompanyRoleAgreementIds.Should().HaveCount(2).And.Satisfy( - x => x.CompanyRoleId == CompanyRoleId.APP_PROVIDER, - x => x.CompanyRoleId == CompanyRoleId.SERVICE_PROVIDER); - result.CompanyApplications.Should().HaveCount(1).And.Satisfy(x => x.CompanyApplicationStatusId == CompanyApplicationStatusId.CREATED); + result.Exists.Should().BeTrue(); + result.CompanyApplications.Should().BeEmpty(); + result.CompanyRoleAgreementIds.Should().Satisfy(x => x.CompanyRoleId == CompanyRoleId.SERVICE_PROVIDER && x.AgreementIds.Count() == 2); result.ProcessId.Should().BeNull(); } @@ -182,12 +178,11 @@ public async Task GetSubmitData_WithValid_ReturnsExpected() var sut = await CreateSut().ConfigureAwait(false); // Act - var result = await sut.GetSubmitData(new Guid("ac861325-bc54-4583-bcdc-9e9f2a38ff84"), new Guid("8b42e6de-7b59-4217-a63c-198e83d93776"), Enumerable.Repeat(new Guid("aabcdfeb-6669-4c74-89f0-19cda090873e"), 1)).ConfigureAwait(false); + var result = await sut.GetSubmitData(new Guid("ac861325-bc54-4583-bcdc-9e9f2a38ff84")).ConfigureAwait(false); // Assert result.Should().NotBe(default); - result.Exists.Should().Be(true); - result.IsUserInRole.Should().Be(true); + result.Exists.Should().BeTrue(); result.CompanyRoleAgreementIds.Should().HaveCount(2).And.Satisfy( x => x.CompanyRoleId == CompanyRoleId.ACTIVE_PARTICIPANT, x => x.CompanyRoleId == CompanyRoleId.APP_PROVIDER); diff --git a/tests/registration/Registration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs b/tests/registration/Registration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs new file mode 100644 index 0000000000..1743f86667 --- /dev/null +++ b/tests/registration/Registration.Service.Tests/BusinessLogic/NetworkBusinessLogicTests.cs @@ -0,0 +1,379 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using AutoFixture; +using AutoFixture.AutoFakeItEasy; +using FakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; +using Org.Eclipse.TractusX.Portal.Backend.Framework.Models.Configuration; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Models; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.DBAccess.Repositories; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Entities; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Enums; +using Org.Eclipse.TractusX.Portal.Backend.PortalBackend.PortalEntities.Identities; +using Org.Eclipse.TractusX.Portal.Backend.Processes.ApplicationChecklist.Library; +using Org.Eclipse.TractusX.Portal.Backend.Provisioning.Library.Service; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.BusinessLogic; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; +using Xunit; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Tests.BusinessLogic; + +public class NetworkBusinessLogicTests +{ + private const string Bpnl = "BPNL00000001TEST"; + private static readonly Guid ExistingExternalId = Guid.NewGuid(); + private static readonly Guid UserRoleId = Guid.NewGuid(); + private static readonly Guid MultiIdpCompanyId = Guid.NewGuid(); + private static readonly Guid NoIdpCompanyId = Guid.NewGuid(); + private static readonly Guid IdpId = Guid.NewGuid(); + private static readonly Guid NoAliasIdpCompanyId = Guid.NewGuid(); + + private readonly IFixture _fixture; + + private readonly IdentityData _identity = new(Guid.NewGuid().ToString(), Guid.NewGuid(), IdentityTypeId.COMPANY_USER, Guid.NewGuid()); + private readonly IUserProvisioningService _userProvisioningService; + private readonly IApplicationChecklistCreationService _checklistService; + + private readonly IPortalRepositories _portalRepositories; + private readonly ICompanyRepository _companyRepository; + private readonly IProcessStepRepository _processStepRepository; + private readonly IApplicationRepository _applicationRepository; + private readonly INetworkRepository _networkRepository; + private readonly IIdentityProviderRepository _identityProviderRepository; + private readonly ICountryRepository _countryRepository; + private readonly NetworkBusinessLogic _sut; + private readonly IConsentRepository _consentRepository; + + public NetworkBusinessLogicTests() + { + _fixture = new Fixture().Customize(new AutoFakeItEasyCustomization { ConfigureMembers = true }); + _fixture.Behaviors.OfType().ToList() + .ForEach(b => _fixture.Behaviors.Remove(b)); + _fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + + _userProvisioningService = A.Fake(); + _portalRepositories = A.Fake(); + _checklistService = A.Fake(); + + _companyRepository = A.Fake(); + _processStepRepository = A.Fake(); + _applicationRepository = A.Fake(); + _networkRepository = A.Fake(); + _identityProviderRepository = A.Fake(); + _countryRepository = A.Fake(); + _consentRepository = A.Fake(); + + var identityService = A.Fake(); + A.CallTo(() => identityService.IdentityData).Returns(_identity); + + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_companyRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_consentRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_processStepRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_applicationRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_networkRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_identityProviderRepository); + A.CallTo(() => _portalRepositories.GetInstance()).Returns(_countryRepository); + + _sut = new NetworkBusinessLogic(_portalRepositories, identityService, _checklistService); + + SetupRepos(); + } + + #region Submit + + [Fact] + public async Task Submit_WithNotExistingSubmitData_ThrowsNotFoundException() + { + // Arrange + var data = _fixture.Create(); + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns(new ValueTuple>, IEnumerable>>, Guid?>()); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"Company {_identity.CompanyId} not found"); + } + + [Fact] + public async Task Submit_WithoutCompanyApplications_ThrowsConflictException() + { + // Arrange + var data = _fixture.Create(); + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Empty>(), Enumerable.Empty>>(), null)); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"Company {_identity.CompanyId} has no or more than one application"); + } + + [Fact] + public async Task Submit_WithMultipleCompanyApplications_ThrowsConflictException() + { + // Arrange + var data = _fixture.Create(); + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, _fixture.CreateMany>(2), Enumerable.Empty>>(), null)); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"Company {_identity.CompanyId} has no or more than one application"); + } + + [Fact] + public async Task Submit_WithWrongApplicationStatus_ThrowsConflictException() + { + // Arrange + var applicationId = Guid.NewGuid(); + var data = _fixture.Create(); + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.VERIFY, null), 1), Enumerable.Empty>>(), Guid.NewGuid())); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"Application {applicationId} is not in state CREATED"); + } + + [Fact] + public async Task Submit_WithOneMissingAgreement_ThrowsConflictException() + { + // Arrange + var applicationId = Guid.NewGuid(); + var agreementId = Guid.NewGuid(); + var notExistingAgreementId = Guid.NewGuid(); + var data = new PartnerSubmitData( + new[] { CompanyRoleId.APP_PROVIDER }, + new[] { new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE) }); + var companyRoleIds = new ValueTuple>[] + { + (CompanyRoleId.APP_PROVIDER, new [] {agreementId, notExistingAgreementId}) + }; + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, null), 1), companyRoleIds, Guid.NewGuid())); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"All Agreements for the company roles must be agreed to, missing agreementIds: {notExistingAgreementId} (Parameter 'Agreements')"); + } + + [Fact] + public async Task Submit_WithOneInactiveAgreement_ThrowsConflictException() + { + // Arrange + var applicationId = Guid.NewGuid(); + var agreementId = Guid.NewGuid(); + var inactiveAgreementId = Guid.NewGuid(); + var data = new PartnerSubmitData( + new[] { CompanyRoleId.APP_PROVIDER }, + new[] + { + new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), + new AgreementConsentData(inactiveAgreementId, ConsentStatusId.INACTIVE), + }); + var companyRoleIds = new ValueTuple>[] + { + (CompanyRoleId.APP_PROVIDER, new [] {agreementId, inactiveAgreementId}) + }; + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, null), 1), companyRoleIds, Guid.NewGuid())); + + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be($"All agreements must be agreed to. Agreements that are not active: {inactiveAgreementId} (Parameter 'Agreements')"); + } + + [Fact] + public async Task Submit_WithoutProcessId_ThrowsConflictException() + { + // Arrange + var applicationId = Guid.NewGuid(); + var agreementId = Guid.NewGuid(); + var agreementId1 = Guid.NewGuid(); + var data = new PartnerSubmitData( + new[] { CompanyRoleId.APP_PROVIDER }, + new[] + { + new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), + new AgreementConsentData(agreementId1, ConsentStatusId.ACTIVE), + }); + var companyRoleIds = new ValueTuple>[] + { + (CompanyRoleId.APP_PROVIDER, new [] {agreementId, agreementId1}) + }; + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, "https://callback.url"), 1), companyRoleIds, null)); + // Act + async Task Act() => await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + var ex = await Assert.ThrowsAsync(Act); + ex.Message.Should().Be("There must be an process"); + } + + [Fact] + public async Task Submit_WithValidData_CallsExpected() + { + // Arrange + var applicationId = Guid.NewGuid(); + var agreementId = Guid.NewGuid(); + var agreementId1 = Guid.NewGuid(); + var processId = Guid.NewGuid(); + var submitProcessId = Guid.NewGuid(); + var processSteps = new List(); + var application = new CompanyApplication(applicationId, _identity.CompanyId, CompanyApplicationStatusId.CREATED, CompanyApplicationTypeId.EXTERNAL, DateTimeOffset.UtcNow); + + var data = new PartnerSubmitData( + new[] { CompanyRoleId.APP_PROVIDER }, + new[] + { + new AgreementConsentData(agreementId, ConsentStatusId.ACTIVE), + new AgreementConsentData(agreementId1, ConsentStatusId.ACTIVE), + }); + var companyRoleIds = new ValueTuple>[] + { + (CompanyRoleId.APP_PROVIDER, new [] {agreementId, agreementId1}) + }; + A.CallTo(() => _networkRepository.GetSubmitData(_identity.CompanyId)) + .Returns((true, Enumerable.Repeat>((applicationId, CompanyApplicationStatusId.CREATED, "https://callback.url"), 1), companyRoleIds, submitProcessId)); + A.CallTo(() => _applicationRepository.AttachAndModifyCompanyApplication(applicationId, A>._)) + .Invokes((Guid _, Action setOptionalFields) => + { + setOptionalFields.Invoke(application); + }); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .Invokes((IEnumerable<(ProcessStepTypeId ProcessStepTypeId, ProcessStepStatusId ProcessStepStatusId, Guid ProcessId)> steps) => + { + processSteps.AddRange(steps.Select(x => new ProcessStep(Guid.NewGuid(), x.ProcessStepTypeId, x.ProcessStepStatusId, x.ProcessId, DateTimeOffset.UtcNow))); + }); + var consents = new List(); + var now = DateTimeOffset.UtcNow; + A.CallTo(() => _consentRepository.CreateConsents(A>._)) + .Invokes((IEnumerable<(Guid AgreementId, Guid CompanyId, Guid CompanyUserId, ConsentStatusId ConsentStatusId)> agreementConsents) => + { + foreach (var x in agreementConsents) + { + consents.Add(new Consent(Guid.NewGuid(), x.AgreementId, x.CompanyId, x.CompanyUserId, x.ConsentStatusId, now)); + } + }); + A.CallTo(() => _processStepRepository.CreateProcess(ProcessTypeId.APPLICATION_CHECKLIST)) + .ReturnsLazily((ProcessTypeId processTypeId) => new Process(processId, processTypeId, Guid.NewGuid())); + A.CallTo(() => _checklistService.CreateInitialChecklistAsync(applicationId)) + .Returns(new[] + { + (ApplicationChecklistEntryTypeId.REGISTRATION_VERIFICATION, ApplicationChecklistEntryStatusId.TO_DO), + (ApplicationChecklistEntryTypeId.BUSINESS_PARTNER_NUMBER, ApplicationChecklistEntryStatusId.DONE), + (ApplicationChecklistEntryTypeId.IDENTITY_WALLET, ApplicationChecklistEntryStatusId.TO_DO), + (ApplicationChecklistEntryTypeId.SELF_DESCRIPTION_LP, ApplicationChecklistEntryStatusId.TO_DO), + (ApplicationChecklistEntryTypeId.CLEARING_HOUSE, ApplicationChecklistEntryStatusId.TO_DO), + (ApplicationChecklistEntryTypeId.APPLICATION_ACTIVATION, ApplicationChecklistEntryStatusId.TO_DO), + }); + A.CallTo(() => _checklistService.GetInitialProcessStepTypeIds(A>._)) + .Returns(new[] { ProcessStepTypeId.VERIFY_REGISTRATION, ProcessStepTypeId.DECLINE_APPLICATION }); + + // Act + await _sut.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + application.ApplicationStatusId.Should().Be(CompanyApplicationStatusId.SUBMITTED); + A.CallTo(() => _consentRepository.CreateConsents(A>._)) + .MustHaveHappenedOnceExactly(); + consents.Should().HaveCount(2) + .And.AllSatisfy(x => x.Should().Match(x => + x.CompanyId == _identity.CompanyId && + x.CompanyUserId == _identity.UserId && + x.ConsentStatusId == ConsentStatusId.ACTIVE)) + .And.Satisfy( + x => x.AgreementId == agreementId, + x => x.AgreementId == agreementId1); + A.CallTo(() => _portalRepositories.SaveAsync()) + .MustHaveHappenedOnceExactly(); + A.CallTo(() => _processStepRepository.CreateProcessStepRange(A>._)) + .MustHaveHappenedOnceExactly(); + processSteps.Should().Satisfy( + x => x.ProcessId == processId && x.ProcessStepTypeId == ProcessStepTypeId.VERIFY_REGISTRATION && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessId == processId && x.ProcessStepTypeId == ProcessStepTypeId.DECLINE_APPLICATION && x.ProcessStepStatusId == ProcessStepStatusId.TODO, + x => x.ProcessId == submitProcessId && x.ProcessStepTypeId == ProcessStepTypeId.TRIGGER_CALLBACK_OSP_SUBMITTED && x.ProcessStepStatusId == ProcessStepStatusId.TODO); + } + + #endregion + + #region Setup + + private void SetupRepos() + { + A.CallTo(() => _networkRepository.CheckExternalIdExists(ExistingExternalId, A.That.Matches(x => x == _identity.CompanyId || x == NoIdpCompanyId))) + .Returns(true); + A.CallTo(() => _networkRepository.CheckExternalIdExists(A.That.Not.Matches(x => x == ExistingExternalId), A.That.Matches(x => x == _identity.CompanyId || x == NoIdpCompanyId))) + .Returns(false); + + A.CallTo(() => _companyRepository.CheckBpnExists(Bpnl)).Returns(false); + A.CallTo(() => _companyRepository.CheckBpnExists(A.That.Not.Matches(x => x == Bpnl))).Returns(true); + + A.CallTo(() => _countryRepository.CheckCountryExistsByAlpha2CodeAsync("XX")) + .Returns(false); + A.CallTo(() => _countryRepository.CheckCountryExistsByAlpha2CodeAsync(A.That.Not.Matches(x => x == "XX"))) + .Returns(true); + + A.CallTo(() => _companyRepository.GetCompanyNameUntrackedAsync(A.That.Matches(x => x == _identity.CompanyId || x == NoIdpCompanyId))) + .Returns((true, "testCompany")); + A.CallTo(() => _companyRepository.GetCompanyNameUntrackedAsync(A.That.Not.Matches(x => x == _identity.CompanyId))) + .Returns((false, "")); + + A.CallTo(() => _identityProviderRepository.GetSingleManagedIdentityProviderAliasDataUntracked(_identity.CompanyId)) + .Returns((IdpId, (string?)"test-alias")); + + A.CallTo(() => _identityProviderRepository.GetSingleManagedIdentityProviderAliasDataUntracked(NoAliasIdpCompanyId)) + .Returns((IdpId, (string?)null)); + + A.CallTo(() => _identityProviderRepository.GetSingleManagedIdentityProviderAliasDataUntracked(NoIdpCompanyId)) + .Returns(((Guid, string?))default); + + A.CallTo(() => _identityProviderRepository.GetSingleManagedIdentityProviderAliasDataUntracked(MultiIdpCompanyId)) + .Throws(new InvalidOperationException("Sequence contains more than one element.")); + + A.CallTo(() => _identityProviderRepository.GetManagedIdentityProviderAliasDataUntracked(A.That.Matches(x => x == _identity.CompanyId || x == NoIdpCompanyId), A>._)) + .Returns(new[] { (IdpId, (string?)"test-alias") }.ToAsyncEnumerable()); + + A.CallTo(() => _userProvisioningService.GetRoleDatas(A>._)) + .Returns(new[] { new UserRoleData(UserRoleId, "cl1", "Company Admin") }.ToAsyncEnumerable()); + } + + #endregion +} diff --git a/tests/registration/Registration.Service.Tests/Controller/NetworkControllerTests.cs b/tests/registration/Registration.Service.Tests/Controller/NetworkControllerTests.cs new file mode 100644 index 0000000000..d9258726f4 --- /dev/null +++ b/tests/registration/Registration.Service.Tests/Controller/NetworkControllerTests.cs @@ -0,0 +1,57 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using AutoFixture; +using FakeItEasy; +using FluentAssertions; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.BusinessLogic; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Controllers; +using Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Model; +using Xunit; + +namespace Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Tests.Controller; + +public class NetworkControllerTests +{ + private readonly INetworkBusinessLogic _logic; + private readonly NetworkController _controller; + private readonly Fixture _fixture; + + public NetworkControllerTests() + { + _fixture = new Fixture(); + _logic = A.Fake(); + this._controller = new NetworkController(_logic); + } + + [Fact] + public async Task Submit_ReturnsExpected() + { + // Arrange + var data = _fixture.Create(); + + // Act + var result = await this._controller.Submit(data, CancellationToken.None).ConfigureAwait(false); + + // Assert + result.StatusCode.Should().Be(204); + A.CallTo(() => _logic.Submit(A._, A._)) + .MustHaveHappenedOnceExactly(); + } +} diff --git a/tests/registration/Registration.Service.Tests/Registration.Service.Tests.csproj b/tests/registration/Registration.Service.Tests/Registration.Service.Tests.csproj index 131544a1bc..8ae32c1fe9 100644 --- a/tests/registration/Registration.Service.Tests/Registration.Service.Tests.csproj +++ b/tests/registration/Registration.Service.Tests/Registration.Service.Tests.csproj @@ -1,55 +1,56 @@ - - - - - - Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Tests - net7.0 - enable - enable - false - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - + + + + + + Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Tests + Org.Eclipse.TractusX.Portal.Backend.Registration.Service.Tests + net7.0 + enable + enable + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + +