Skip to content

Commit

Permalink
Merge branch 'main' into feature/dp-875-Informal-consortium-add-addit…
Browse files Browse the repository at this point in the history
…ional-organisations
  • Loading branch information
dharmverma authored Jan 10, 2025
2 parents 80afae8 + e407fbf commit 9667c46
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Moq;
using System.Net;
using System.Net.Http.Json;
using static CO.CDP.Authentication.Constants;
using static System.Net.HttpStatusCode;

Expand All @@ -13,6 +14,7 @@ namespace CO.CDP.Organisation.WebApi.Tests.Api;
public class OrganisationPartiesEndpointsTests
{
private readonly Mock<IUseCase<Guid, OrganisationParties?>> _getOrganisationPartiesUseCase = new();
private readonly Mock<IUseCase<(Guid, AddOrganisationParty), bool>> _addOrganisationPartyUseCase = new();

[Theory]
[InlineData(OK, Channel.OneLogin, OrganisationPersonScope.Admin)]
Expand All @@ -38,4 +40,36 @@ public async Task GetOrganisationParties_Authorization_ReturnsExpectedStatusCode

response.StatusCode.Should().Be(expectedStatusCode);
}
}

[Theory]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Admin)]
[InlineData(NoContent, Channel.OneLogin, OrganisationPersonScope.Editor)]
[InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Responder)]
[InlineData(Forbidden, Channel.OneLogin, OrganisationPersonScope.Viewer)]
[InlineData(Forbidden, Channel.ServiceKey)]
[InlineData(Forbidden, Channel.OrganisationKey)]
[InlineData(Forbidden, "unknown_channel")]
public async Task AddOrganisationParty_Authorization_ReturnsExpectedStatusCode(
HttpStatusCode expectedStatusCode, string channel, string? scope = null)
{
var organisationId = Guid.NewGuid();
var organisationParty = new AddOrganisationParty
{
OrganisationPartyId = Guid.NewGuid(),
OrganisationRelationship = OrganisationRelationship.Consortium,
ShareCode = "Test"
};
var command = (organisationId, organisationParty);

_addOrganisationPartyUseCase.Setup(uc => uc.Execute(command))
.ReturnsAsync(true);

var factory = new TestAuthorizationWebApplicationFactory<Program>(
channel, organisationId, scope,
services => services.AddScoped(_ => _addOrganisationPartyUseCase.Object));

var response = await factory.CreateClient().PostAsJsonAsync($"/organisations/{organisationId}/add-party", organisationParty);

response.StatusCode.Should().Be(expectedStatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using CO.CDP.Organisation.WebApi.Model;
using CO.CDP.Organisation.WebApi.UseCase;
using CO.CDP.OrganisationInformation.Persistence;
using FluentAssertions;
using Moq;
using Persistence = CO.CDP.OrganisationInformation.Persistence;

namespace CO.CDP.Organisation.WebApi.Tests.UseCase;

public class AddOrganisationPartyUseCaseTests
{
private readonly Mock<IOrganisationRepository> _orgRepoMock = new();
private readonly Mock<IShareCodeRepository> _shareCodeRepoMock = new();
private readonly Mock<IOrganisationPartiesRepository> _orgPartiesRepoMock = new();
private AddOrganisationPartyUseCase UseCase => new(_orgRepoMock.Object, _shareCodeRepoMock.Object, _orgPartiesRepoMock.Object);

[Fact]
public async Task Execute_ShouldThrowException_WhenParentOrganisationNotFound()
{
var organisationId = Guid.NewGuid();
var addParty = new AddOrganisationParty
{
OrganisationPartyId = Guid.NewGuid(),
ShareCode = null,
OrganisationRelationship = Model.OrganisationRelationship.Consortium,
};

_orgRepoMock.Setup(repo => repo.Find(organisationId)).ReturnsAsync((Persistence.Organisation?)null);

Func<Task> action = async () => await UseCase.Execute((organisationId, addParty));

await action.Should().ThrowAsync<UnknownOrganisationException>()
.WithMessage($"Unknown organisation {organisationId}.");
}

[Fact]
public async Task Execute_ShouldThrowException_WhenChildOrganisationNotFound()
{
var organisationId = Guid.NewGuid();
var parentOrg = GivenOrganisation(organisationId);
var childOrganisationId = Guid.NewGuid();
var addParty = new AddOrganisationParty
{
OrganisationPartyId = childOrganisationId,
ShareCode = null,
OrganisationRelationship = Model.OrganisationRelationship.Consortium,
};

_orgRepoMock.Setup(repo => repo.Find(organisationId)).ReturnsAsync(parentOrg);
_orgRepoMock.Setup(repo => repo.Find(childOrganisationId)).ReturnsAsync((Persistence.Organisation?)null);

Func<Task> action = async () => await UseCase.Execute((organisationId, addParty));

await action.Should().ThrowAsync<UnknownOrganisationException>()
.WithMessage($"Unknown organisation {childOrganisationId}.");
}

[Fact]
public async Task Execute_ShouldThrowException_WhenShareCodeIsInvalid()
{
var organisationId = Guid.NewGuid();
var parentOrg = GivenOrganisation(organisationId);
var childOrganisationId = Guid.NewGuid();
var childOrg = GivenOrganisation(childOrganisationId);
var shareCode = "InvalidCode";
var addParty = new AddOrganisationParty
{
OrganisationPartyId = childOrganisationId,
ShareCode = shareCode,
OrganisationRelationship = Model.OrganisationRelationship.Consortium,
};

_orgRepoMock.Setup(repo => repo.Find(organisationId)).ReturnsAsync(parentOrg);
_orgRepoMock.Setup(repo => repo.Find(childOrganisationId)).ReturnsAsync(childOrg);
_shareCodeRepoMock.Setup(repo => repo.GetShareCodesAsync(childOrganisationId)).ReturnsAsync([]);

Func<Task> action = async () => await UseCase.Execute((organisationId, addParty));

await action.Should().ThrowAsync<OrganisationShareCodeInvalid>()
.WithMessage($"Invalid organisation share code: {shareCode}");
}

[Fact]
public async Task Execute_ShouldSaveOrganisationParty_WhenValidInputProvided()
{
var organisationId = Guid.NewGuid();
var parentOrg = GivenOrganisation(organisationId);
var childOrganisationId = Guid.NewGuid();
var childOrg = GivenOrganisation(childOrganisationId);
var shareCode = "ValidCode";
var sharedConsent = new Persistence.Forms.SharedConsent
{
Id = 1,
Guid = Guid.NewGuid(),
OrganisationId = childOrg.Id,
Organisation = childOrg,
FormId = 1,
Form = null!,
AnswerSets = [],
SubmissionState = Persistence.Forms.SubmissionState.Submitted,
SubmittedAt = null,
FormVersionId = string.Empty,
ShareCode = shareCode ?? "valid-sharecode",
CreatedOn = DateTimeOffset.UtcNow,
UpdatedOn = DateTimeOffset.UtcNow,
};

_orgRepoMock.Setup(repo => repo.Find(organisationId)).ReturnsAsync(parentOrg);
_orgRepoMock.Setup(repo => repo.Find(childOrganisationId)).ReturnsAsync(childOrg);
_shareCodeRepoMock.Setup(repo => repo.GetShareCodesAsync(childOrganisationId)).ReturnsAsync([sharedConsent]);

var result = await UseCase.Execute((organisationId, new AddOrganisationParty
{
OrganisationPartyId = childOrganisationId,
ShareCode = shareCode,
OrganisationRelationship = Model.OrganisationRelationship.Consortium,
}));

result.Should().BeTrue();

_orgPartiesRepoMock.Verify(repo => repo.Save(It.Is<Persistence.OrganisationParty>(party =>
party.ParentOrganisationId == parentOrg.Id &&
party.ChildOrganisationId == childOrg.Id &&
party.SharedConsentId == sharedConsent.Id &&
party.OrganisationRelationship == Persistence.OrganisationRelationship.Consortium)), Times.Once);
}

private static Persistence.Organisation GivenOrganisation(Guid guid) =>
new()
{
Guid = guid,
Name = "Test",
Type = OrganisationInformation.OrganisationType.Organisation,
Tenant = It.IsAny<Tenant>(),
ContactPoints = [new Persistence.Organisation.ContactPoint { Email = "test@test.com" }],
SupplierInfo = new Persistence.Organisation.SupplierInformation()
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public class GetOrganisationPartiesUseCaseTests(AutoMapperFixture mapperFixture)
private readonly Mock<IOrganisationPartiesRepository> _orgPartiesRepoMock = new();
private GetOrganisationPartiesUseCase UseCase => new(_orgPartiesRepoMock.Object, mapperFixture.Mapper);


[Fact]
public async Task Execute_ShouldReturnNull_WhenNoPartiesFound()
{
Expand Down Expand Up @@ -41,15 +40,15 @@ public async Task Execute_ShouldReturnMappedParties_WhenPartiesAreFound()
ParentOrganisationId = parentOrg.Id,
ChildOrganisationId = childOrg1.Id,
ChildOrganisation =childOrg1,
OrganisationRelationship = OrganisationRelationship.Consortium,
OrganisationRelationship = Persistence.OrganisationRelationship.Consortium,
},

new() {
Id = 1,
ParentOrganisationId = parentOrg.Id,
ChildOrganisationId = childOrg2.Id,
ChildOrganisation = childOrg2,
OrganisationRelationship = OrganisationRelationship.Consortium,
OrganisationRelationship = Persistence.OrganisationRelationship.Consortium,
},
};

Expand All @@ -63,7 +62,7 @@ public async Task Execute_ShouldReturnMappedParties_WhenPartiesAreFound()
_orgPartiesRepoMock.Verify(repo => repo.Find(organisationId), Times.Once);
}

private Persistence.Organisation GivenOrganisation(Guid guid) =>
private static Persistence.Organisation GivenOrganisation(Guid guid) =>
new()
{
Guid = guid,
Expand Down
27 changes: 27 additions & 0 deletions Services/CO.CDP.Organisation.WebApi/Api/Organisation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,33 @@ await useCase.Execute(organisationId)
return operation;
});

app.MapPost("/{organisationId}/add-party",
[OrganisationAuthorize(
[AuthenticationChannel.OneLogin],
[Constants.OrganisationPersonScope.Admin, Constants.OrganisationPersonScope.Editor],
OrganisationIdLocation.Path)]
async (Guid organisationId, AddOrganisationParty organisationParty, IUseCase<(Guid, AddOrganisationParty), bool> useCase) =>
await useCase.Execute((organisationId, organisationParty))
.AndThen(_ => Results.NoContent())
)
.Produces(StatusCodes.Status204NoContent)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.ProducesProblem(StatusCodes.Status400BadRequest)
.ProducesProblem(StatusCodes.Status401Unauthorized)
.ProducesProblem(StatusCodes.Status404NotFound)
.WithOpenApi(operation =>
{
operation.OperationId = "AddOrganisationParty";
operation.Description = "Add organisation party";
operation.Summary = "Add organisation party";
operation.Responses["204"].Description = "Organisation party added successfully.";
operation.Responses["400"].Description = "Bad request.";
operation.Responses["401"].Description = "Valid authentication credentials are missing in the request.";
operation.Responses["404"].Description = "Organisation parties not found.";
operation.Responses["500"].Description = "Internal server error.";
return operation;
});

return app;
}
}
Expand Down
1 change: 1 addition & 0 deletions Services/CO.CDP.Organisation.WebApi/ErrorCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ public static class ErrorCodes
{ typeof(MissingIdentifierNumber), (StatusCodes.Status400BadRequest, "ORGANISATION_MISSING_IDENTIFIER_NUMBER") },
{ typeof(IdentiferNumberAlreadyExists), (StatusCodes.Status400BadRequest, "ORGANISATION_IDENTIFIER_NUMBER_ALREADY_EXISTS") },
{ typeof(UnknownMouException), (StatusCodes.Status404NotFound, "MOU_DOES_NOT_EXIST") },
{ typeof(OrganisationShareCodeInvalid), (StatusCodes.Status400BadRequest, "ORGANISATION_SHARE_CODE_INVALID") }
};
}
15 changes: 15 additions & 0 deletions Services/CO.CDP.Organisation.WebApi/Model/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,19 @@ public static ContactPoint AsView(this OrganisationContactPoint? command) =>
Telephone = command.Telephone,
Url = command.Url != null ? new Uri(command.Url) : null
} : new ContactPoint();
}

public record AddOrganisationParty
{
public required Guid OrganisationPartyId { get; init; }

public required OrganisationRelationship OrganisationRelationship { get; init; }

public string? ShareCode { get; init; }
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OrganisationRelationship
{
Consortium
}
3 changes: 2 additions & 1 deletion Services/CO.CDP.Organisation.WebApi/Model/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ public class DuplicateEmailWithinOrganisationException(string message, Exception
public class DuplicateInviteEmailForOrganisationException(string message, Exception? cause = null) : Exception(message, cause);
public class PersonAlreadyAddedToOrganisationException(string message, Exception? cause = null) : Exception(message, cause);
public class PersonAlreadyInvitedToOrganisationException(string message, Exception? cause = null) : Exception(message, cause);
public class UnknownOrganisationJoinRequestException(string message, Exception? cause = null) : Exception(message, cause);
public class UnknownOrganisationJoinRequestException(string message, Exception? cause = null) : Exception(message, cause);
public class OrganisationShareCodeInvalid(string shareCode) : Exception($"Invalid organisation share code: {shareCode}");
6 changes: 4 additions & 2 deletions Services/CO.CDP.Organisation.WebApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
using CO.CDP.OrganisationInformation.Persistence;
using CO.CDP.WebApi.Foundation;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Npgsql;
using System.Reflection;
using ConnectedEntity = CO.CDP.Organisation.WebApi.Model.ConnectedEntity;
Expand Down Expand Up @@ -69,6 +68,8 @@
builder.Services.AddScoped<IPersonInviteRepository, DatabasePersonInviteRepository>();
builder.Services.AddScoped<IAuthenticationKeyRepository, DatabaseAuthenticationKeyRepository>();
builder.Services.AddScoped<IOrganisationJoinRequestRepository, DatabaseOrganisationJoinRequestRepository>();
builder.Services.AddScoped<IShareCodeRepository, DatabaseShareCodeRepository>();

builder.Services.AddScoped<IUseCase<AssignOrganisationIdentifier, bool>, AssignIdentifierUseCase>();
builder.Services.AddScoped<IUseCase<RegisterOrganisation, Organisation>, RegisterOrganisationUseCase>();
builder.Services.AddScoped<IUseCase<Guid, Organisation?>, GetOrganisationUseCase>();
Expand All @@ -93,7 +94,6 @@
builder.Services.AddScoped<IUseCase<Guid, IEnumerable<PersonInviteModel>>, GetPersonInvitesUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, Guid), bool>, RemovePersonInviteFromOrganisationUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, SupportUpdateOrganisation), bool>, SupportUpdateOrganisationUseCase>();
builder.Services.AddGovUKNotifyApiClient(builder.Configuration);
builder.Services.AddScoped<IUseCase<Guid, IEnumerable<CO.CDP.Organisation.WebApi.Model.AuthenticationKey>>, GetAuthenticationKeyUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, RegisterAuthenticationKey), bool>, RegisterAuthenticationKeyUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, string), bool>, RevokeAuthenticationKeyUseCase>();
Expand All @@ -108,7 +108,9 @@
builder.Services.AddScoped<IUseCase<(Guid, Guid), MouSignature>, GetOrganisationMouSignatureUseCase>();
builder.Services.AddScoped<IUseCase<Guid, MouSignatureLatest>, GetOrganisationMouSignatureLatestUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, SignMouRequest),bool>, SignOrganisationMouUseCase>();
builder.Services.AddScoped<IUseCase<(Guid, AddOrganisationParty), bool>, AddOrganisationPartyUseCase>();

builder.Services.AddGovUKNotifyApiClient(builder.Configuration);
builder.Services.AddProblemDetails();

builder.Services.AddJwtBearerAndApiKeyAuthentication(builder.Configuration, builder.Environment);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using CO.CDP.Organisation.WebApi.Model;
using CO.CDP.OrganisationInformation.Persistence;

namespace CO.CDP.Organisation.WebApi.UseCase;

public class AddOrganisationPartyUseCase(
IOrganisationRepository orgRepo,
IShareCodeRepository shareCodeRepo,
IOrganisationPartiesRepository orgPartiesRepo
) : IUseCase<(Guid, AddOrganisationParty), bool>
{
public async Task<bool> Execute((Guid, AddOrganisationParty) command)
{
(Guid organisationId, AddOrganisationParty addParty) = command;

var parentOrganisation = await orgRepo.Find(organisationId)
?? throw new UnknownOrganisationException($"Unknown organisation {organisationId}.");

var childOrganisation = await orgRepo.Find(addParty.OrganisationPartyId)
?? throw new UnknownOrganisationException($"Unknown organisation {addParty.OrganisationPartyId}.");

int? sharedConsentId = null;

if (!string.IsNullOrWhiteSpace(addParty.ShareCode))
{
var sharedConsents = await shareCodeRepo.GetShareCodesAsync(addParty.OrganisationPartyId);

var sharedConsent = sharedConsents.FirstOrDefault(s =>
string.Equals(s.ShareCode, addParty.ShareCode, StringComparison.InvariantCultureIgnoreCase));

if (sharedConsent == null
|| sharedConsent.SubmissionState != OrganisationInformation.Persistence.Forms.SubmissionState.Submitted
|| sharedConsent.OrganisationId != childOrganisation.Id)
{
throw new OrganisationShareCodeInvalid(addParty.ShareCode);
}

sharedConsentId = sharedConsent.Id;
}

await orgPartiesRepo.Save(
new OrganisationInformation.Persistence.OrganisationParty
{
ParentOrganisationId = parentOrganisation.Id,
ChildOrganisationId = childOrganisation.Id,
OrganisationRelationship = (OrganisationInformation.Persistence.OrganisationRelationship)addParty.OrganisationRelationship,
SharedConsentId = sharedConsentId,
});

return true;
}
}

0 comments on commit 9667c46

Please sign in to comment.