diff --git a/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs index 14d16f234e..48c9d81eef 100644 --- a/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/ConnectorsBusinessLogic.cs @@ -97,6 +97,7 @@ private async Task CreateConnectorInternalAsync(ConnectorInputModel connec { var companyId = _identityData.CompanyId; var (name, connectorUrl, location, technicalUserId) = connectorInputModel; + await CheckDuplicateConnector(name, connectorUrl).ConfigureAwait(ConfigureAwaitOptions.None); await CheckLocationExists(location); var result = await portalRepositories @@ -125,6 +126,7 @@ private async Task CreateManagedConnectorInternalAsync(ManagedConnectorInp { var companyId = _identityData.CompanyId; var (name, connectorUrl, location, subscriptionId, technicalUserId) = connectorInputModel; + await CheckDuplicateConnector(name, connectorUrl).ConfigureAwait(ConfigureAwaitOptions.None); await CheckLocationExists(location).ConfigureAwait(ConfigureAwaitOptions.None); var result = await portalRepositories.GetInstance() @@ -178,6 +180,15 @@ private async Task CheckLocationExists(string location) } } + private async Task CheckDuplicateConnector(string name, string connectorUrl) + { + if (await portalRepositories.GetInstance() + .CheckConnectorExists(name, connectorUrl).ConfigureAwait(ConfigureAwaitOptions.None)) + { + throw ConflictException.Create(AdministrationConnectorErrors.CONNECTOR_DUPLICATE, [new("name", name), new("connectorUrl", connectorUrl)]); + } + } + private async Task ValidateTechnicalUser(Guid? technicalUserId, Guid companyId) { if (technicalUserId == null) diff --git a/src/administration/Administration.Service/ErrorHandling/AdministrationConnectorErrorMessageContainer.cs b/src/administration/Administration.Service/ErrorHandling/AdministrationConnectorErrorMessageContainer.cs index 4380fd2687..aac8287306 100644 --- a/src/administration/Administration.Service/ErrorHandling/AdministrationConnectorErrorMessageContainer.cs +++ b/src/administration/Administration.Service/ErrorHandling/AdministrationConnectorErrorMessageContainer.cs @@ -26,24 +26,25 @@ public class AdministrationConnectorErrorMessageContainer : IErrorMessageContain { private static readonly IReadOnlyDictionary _messageContainer = new Dictionary { { AdministrationConnectorErrors.CONNECTOR_NOT_FOUND, "connector {connectorId} does not exist" }, - { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY,"company {companyId} is not provider of connector {connectorId}"}, + { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY,"company {companyId} is not provider of connector {connectorId}" }, { AdministrationConnectorErrors.CONNECTOR_UNEXPECTED_NO_BPN_ASSIGNED, "provider company {companyId} has no businessPartnerNumber assigned" }, - { AdministrationConnectorErrors.CONNECTOR_NOT_OFFERSUBSCRIPTION_EXIST,"OfferSubscription {subscriptionId} does not exist"}, - { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY_OFFER,"Company is not the provider of the offer"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_OFFERSUBSCRIPTION_LINKED,"OfferSubscription is already linked to a connector"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_STATUS_ACTIVE_OR_PENDING,"The offer subscription must be either {offerSubscriptionStatusIdActive} or {offerSubscriptionStatusIdPending}"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_NO_DESCRIPTION,"provider company {CompanyId} has no self description document"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_SET_BPN,"The bpn of company {companyId} must be set"}, - { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_LOCATION_NOT_EXIST,"Location {location} does not exist"}, - { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_TECH_USER_NOT_ACTIVE,"Technical User {technicalUserId} is not assigned to company {companyId} or is not active"}, - { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY_NOR_HOST,"company {companyId} is neither provider nor host-company of connector {connectorId}"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_DELETION_DECLINED,"Connector status does not match a deletion scenario. Deletion declined"}, - { AdministrationConnectorErrors.CONNECTOR_DELETION_FAILED_OFFER_SUBSCRIPTION,"Deletion Failed. Connector {connectorId} connected to an active offer subscription, {activeConnectorOfferSubscription}"}, - { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_INCORRECT_BPN,"Incorrect BPN {bpns} attribute value"}, - { AdministrationConnectorErrors.CONNECTOR_NOT_EXIST,"Connector {externalId} does not exist"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_ALREADY_ASSIGNED,"Connector {externalId} already has a document assigned"}, - { AdministrationConnectorErrors.CONNECTOR_NOT_HOST_COMPANY,"Company {companyId} is not the connectors host company"}, - { AdministrationConnectorErrors.CONNECTOR_CONFLICT_INACTIVE_STATE,"Connector {connectorId} is in state {connectorStatusId}"} + { AdministrationConnectorErrors.CONNECTOR_NOT_OFFERSUBSCRIPTION_EXIST,"OfferSubscription {subscriptionId} does not exist" }, + { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY_OFFER,"Company is not the provider of the offer" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_OFFERSUBSCRIPTION_LINKED,"OfferSubscription is already linked to a connector" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_STATUS_ACTIVE_OR_PENDING,"The offer subscription must be either {offerSubscriptionStatusIdActive} or {offerSubscriptionStatusIdPending}" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_NO_DESCRIPTION,"provider company {CompanyId} has no self description document" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_SET_BPN,"The bpn of company {companyId} must be set" }, + { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_LOCATION_NOT_EXIST,"Location {location} does not exist" }, + { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_TECH_USER_NOT_ACTIVE,"Technical User {technicalUserId} is not assigned to company {companyId} or is not active" }, + { AdministrationConnectorErrors.CONNECTOR_NOT_PROVIDER_COMPANY_NOR_HOST,"company {companyId} is neither provider nor host-company of connector {connectorId}" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_DELETION_DECLINED,"Connector status does not match a deletion scenario. Deletion declined" }, + { AdministrationConnectorErrors.CONNECTOR_DELETION_FAILED_OFFER_SUBSCRIPTION,"Deletion Failed. Connector {connectorId} connected to an active offer subscription, {activeConnectorOfferSubscription}" }, + { AdministrationConnectorErrors.CONNECTOR_ARGUMENT_INCORRECT_BPN,"Incorrect BPN {bpns} attribute value" }, + { AdministrationConnectorErrors.CONNECTOR_NOT_EXIST,"Connector {externalId} does not exist" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_ALREADY_ASSIGNED,"Connector {externalId} already has a document assigned" }, + { AdministrationConnectorErrors.CONNECTOR_NOT_HOST_COMPANY,"Company {companyId} is not the connectors host company" }, + { AdministrationConnectorErrors.CONNECTOR_CONFLICT_INACTIVE_STATE,"Connector {connectorId} is in state {connectorStatusId}" }, + { AdministrationConnectorErrors.CONNECTOR_DUPLICATE,"Connector {name} does already exists for url {connectorUrl}" } }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); public Type Type { get => typeof(AdministrationConnectorErrors); } @@ -70,5 +71,6 @@ public enum AdministrationConnectorErrors CONNECTOR_NOT_EXIST, CONNECTOR_CONFLICT_ALREADY_ASSIGNED, CONNECTOR_NOT_HOST_COMPANY, - CONNECTOR_CONFLICT_INACTIVE_STATE + CONNECTOR_CONFLICT_INACTIVE_STATE, + CONNECTOR_DUPLICATE } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/ConnectorsRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/ConnectorsRepository.cs index 267a6a3b85..f9dae24ce1 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/ConnectorsRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/ConnectorsRepository.cs @@ -232,4 +232,9 @@ public IAsyncEnumerable GetConnectorIdsWithMissingSelfDescription() => .Where(c => c.SdCreationProcessId == processId) .Select(c => new ValueTuple(c.Id, c.Provider!.BusinessPartnerNumber, c.Provider.SelfDescriptionDocumentId!.Value)) .SingleOrDefaultAsync(); + + public Task CheckConnectorExists(string name, string connectorUrl) => + dbContext.Connectors.AnyAsync(x => + x.Name == name && + x.ConnectorUrl == connectorUrl); } diff --git a/src/portalbackend/PortalBackend.DBAccess/Repositories/IConnectorsRepository.cs b/src/portalbackend/PortalBackend.DBAccess/Repositories/IConnectorsRepository.cs index 07da1a7c26..d8180de151 100644 --- a/src/portalbackend/PortalBackend.DBAccess/Repositories/IConnectorsRepository.cs +++ b/src/portalbackend/PortalBackend.DBAccess/Repositories/IConnectorsRepository.cs @@ -109,4 +109,5 @@ public interface IConnectorsRepository Func?>> GetConnectorsWithMissingSdDocument(); IAsyncEnumerable GetConnectorIdsWithMissingSelfDescription(); Task<(Guid Id, string? BusinessPartnerNumber, Guid SelfDescriptionDocumentId)> GetConnectorForProcessId(Guid processId); + Task CheckConnectorExists(string name, string connectorUrl); } diff --git a/tests/administration/Administration.Service.Tests/BusinessLogic/ConnectorsBusinessLogicTests.cs b/tests/administration/Administration.Service.Tests/BusinessLogic/ConnectorsBusinessLogicTests.cs index fcd1fc8056..a43fe65653 100644 --- a/tests/administration/Administration.Service.Tests/BusinessLogic/ConnectorsBusinessLogicTests.cs +++ b/tests/administration/Administration.Service.Tests/BusinessLogic/ConnectorsBusinessLogicTests.cs @@ -192,6 +192,25 @@ public async Task CreateConnectorAsync_WithValidInput_ReturnsCreatedConnectorDat A.CallTo(() => _sdFactoryBusinessLogic.RegisterConnectorAsync(A._, A._, A._, A._)).MustHaveHappened(clearingHouseDisabled ? 0 : 1, Times.Exactly); } + [Fact] + public async Task CreateConnectorAsync_WithExistingConnector_ThrowsConflictException() + { + // Arrange + A.CallTo(() => _connectorsRepository.CheckConnectorExists(A._, A._)) + .Returns(true); + var connectorInput = new ConnectorInputModel("connectorName", "https://test.de", "de", ServiceAccountUserId); + Task Act() => _logic.CreateConnectorAsync(connectorInput, CancellationToken.None); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("CONNECTOR_DUPLICATE"); + _connectors.Should().BeEmpty(); + A.CallTo(() => _connectorsRepository.CreateConnectorAssignedSubscriptions(A._, A._)).MustNotHaveHappened(); + A.CallTo(() => _sdFactoryBusinessLogic.RegisterConnectorAsync(A._, A._, A._, A._)).MustNotHaveHappened(); + } + [Fact] public async Task CreateConnectorAsync_WithInvalidTechnicalUser_ThrowsControllerArgumentException() { @@ -383,6 +402,27 @@ public async Task CreateManagedConnectorAsync_WithTechnicalUser_ReturnsCreatedCo A.CallTo(() => _sdFactoryBusinessLogic.RegisterConnectorAsync(A._, A._, A._, A._)).MustHaveHappened(clearingHouseDisabled ? 0 : 1, Times.Exactly); } + [Fact] + public async Task CreateManagedConnectorAsync_WithExistingConnector_ThrowsConflictException() + { + // Arrange + A.CallTo(() => _connectorsRepository.CheckConnectorExists(A._, A._)) + .Returns(true); + var connectorInput = new ManagedConnectorInputModel("connectorName", "https://test.de", "de", _validOfferSubscriptionId, ServiceAccountUserId); + Task Act() => _logic.CreateManagedConnectorAsync(connectorInput, CancellationToken.None); + + // Act + var ex = await Assert.ThrowsAsync(Act); + + // Assert + ex.Message.Should().Be("CONNECTOR_DUPLICATE"); + _connectors.Should().BeEmpty(); + A.CallTo(() => _connectorsRepository.CreateConnectorAssignedSubscriptions(A._, _validOfferSubscriptionId)) + .MustNotHaveHappened(); + A.CallTo(() => _sdFactoryBusinessLogic.RegisterConnectorAsync(A._, A._, A._, A._)) + .MustNotHaveHappened(); + } + [Fact] public async Task CreateManagedConnectorAsync_WithInvalidLocation_ThrowsControllerArgumentException() { diff --git a/tests/portalbackend/PortalBackend.DBAccess.Tests/ConnectorRepositoryTests.cs b/tests/portalbackend/PortalBackend.DBAccess.Tests/ConnectorRepositoryTests.cs index ad9b630289..c7e4e42c7c 100644 --- a/tests/portalbackend/PortalBackend.DBAccess.Tests/ConnectorRepositoryTests.cs +++ b/tests/portalbackend/PortalBackend.DBAccess.Tests/ConnectorRepositoryTests.cs @@ -596,6 +596,36 @@ public async Task GetConnectorsWithMissingSelfDescription_ReturnsExpectedConnect #endregion + #region CheckConnectorExists + + [Fact] + public async Task CheckConnectorExists_WithExisting_ReturnsTrue() + { + // Arrange + var (sut, _) = await CreateSut(); + + // Act + var result = await sut.CheckConnectorExists("Test Connector 6", "www.google.de").ConfigureAwait(false); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public async Task CheckConnectorExists_WithoutExisting_ReturnsFalse() + { + // Arrange + var (sut, _) = await CreateSut(); + + // Act + var result = await sut.CheckConnectorExists("not existing", "www.google.de").ConfigureAwait(false); + + // Assert + result.Should().BeFalse(); + } + + #endregion + private async Task<(ConnectorsRepository, PortalDbContext)> CreateSut() { var context = await _dbTestDbFixture.GetPortalDbContext();