Skip to content

Commit

Permalink
feat: Add remove portal endpoint (#713)
Browse files Browse the repository at this point in the history
Co-authored-by: Jossilainen <[email protected]>
  • Loading branch information
Jossilainen and Jossilainen authored Sep 6, 2024
1 parent 1587844 commit 73b2e3f
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/pr-713-2027570183.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

---
"fusion-project-portal": patch
---
Added endpoint to delete a portal
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ public Handler(IReadWriteContext context)
public async Task Handle(RemoveOnboardedAppCommand command, CancellationToken cancellationToken)
{
var entity = await _context.Set<OnboardedApp>()
.Include(x => x.Apps)
.FirstOrDefaultAsync(onboardedApp => onboardedApp.AppKey == command.AppKey, cancellationToken);

if (entity == null)
{
throw new NotFoundException(nameof(OnboardedApp), command.AppKey);
}

if (entity.Apps.Any())
{
throw new InvalidOperationException("Cannot remove onboarded app. App is in use in a portal.");
}

_context.Set<OnboardedApp>().Remove(entity);

await _context.SaveChangesAsync(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Equinor.ProjectExecutionPortal.Domain.Common.Exceptions;
using Equinor.ProjectExecutionPortal.Domain.Entities;
using Equinor.ProjectExecutionPortal.Infrastructure;
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace Equinor.ProjectExecutionPortal.Application.Commands.Portals.RemovePortal
{
public class RemovePortalCommand : IRequest
{
public RemovePortalCommand(Guid id)
{
Id = id;
}

public Guid Id { get; }

public class Handler : IRequestHandler<RemovePortalCommand>
{
private readonly IReadWriteContext _context;

public Handler(IReadWriteContext context)
{
_context = context;
}

public async Task Handle(RemovePortalCommand command, CancellationToken cancellationToken)
{
var entity = await _context.Set<Portal>()
.Include(x => x.Apps)
.FirstOrDefaultAsync(portal => portal.Id == command.Id, cancellationToken);

if (entity == null)
{
throw new NotFoundException(nameof(Portal), command.Id);
}

if (entity.Apps.Any())
{
throw new InvalidOperationException("Cannot remove Portal, portal has onboarded apps");
}

_context.Set<Portal>().Remove(entity);

await _context.SaveChangesAsync(cancellationToken);

}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class OnboardedApp : AuditableEntityBase, ICreationAuditable, IModificati
{
public const int AppKeyLengthMax = 200;
private readonly List<ContextType> _contextTypes = new();
private readonly List<PortalApp> _apps = new();

public OnboardedApp(string appKey)
{
Expand All @@ -22,6 +23,7 @@ public OnboardedApp(string appKey)
public string AppKey { get; set; }

public IReadOnlyCollection<ContextType> ContextTypes => _contextTypes.AsReadOnly();
public IReadOnlyCollection<PortalApp> Apps => _apps.AsReadOnly();

public void AddContextTypes(IList<ContextType> contextTypes)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ public async Task<ActionResult> RemoveOnboardedApp([FromRoute] string appKey)
{
return FusionApiError.NotFound(appKey, ex.Message);
}
catch (InvalidOperationException ex)
{
return FusionApiError.InvalidOperation("400", ex.Message);
}
catch (Exception)
{
return FusionApiError.InvalidOperation("500", "An error occurred while removing the onboarded app");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task<ActionResult<ApiOnboardedContext>> OnboardedContext([FromRoute

return Ok(new ApiOnboardedContext(onboardedContext));
}

[HttpPost("")]
[Authorize(Policy = Policies.ProjectPortal.Admin)]
[Consumes(MediaTypeNames.Application.Json)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,36 @@ public async Task<ActionResult<Guid>> UpdatePortalConfiguration([FromRoute] Guid
return Ok();
}

[HttpDelete("{portalId:guid}")]
[Authorize(Policy = Policies.ProjectPortal.Admin)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(void), StatusCodes.Status400BadRequest)]
public async Task<ActionResult> RemovePortalApp([FromRoute] Guid portalId)
{
var request = new ApiRemovePortalRequest();

try
{
await Mediator.Send(request.ToCommand(portalId));
}
catch (NotFoundException ex)
{
return FusionApiError.NotFound(portalId, ex.Message);
}
catch (InvalidOperationException ex)
{
return FusionApiError.Forbidden(ex.Message);
}
catch (Exception)
{
return FusionApiError.InvalidOperation("500", "An error occurred while removing portal");
}

return Ok();
}

// Apps

[HttpGet("{portalId:guid}/apps")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Equinor.ProjectExecutionPortal.Application.Commands.Portals.RemovePortal;

namespace Equinor.ProjectExecutionPortal.WebApi.ViewModels.Portal
{
public class ApiRemovePortalRequest
{
public RemovePortalCommand ToCommand(Guid id)
{
return new RemovePortalCommand(id);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ namespace Equinor.ProjectExecutionPortal.WebApi.ViewModels.PortalApp
{
public class ApiPortalApp
{
public ApiPortalApp()
{

}
public ApiPortalApp(PortalAppDto portalAppDto)
{
Key = portalAppDto.OnboardedApp.AppKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ internal class FusionContextData
public class InitialSeedData
{
public static string JcaContextExternalId = "fc5ffcbc-392f-4d7e-bb14-79a006579337";
public static Guid JcaContextId = new Guid("94dd5f4d-17f1-4312-bf75-ad75f4d9572c");
public static string OgpContextExternalId = "91dd6653-a364-40c7-af26-7af516d66c42";
public static Guid OgpContextId = new Guid("ce31b83a-b6cd-4267-89f3-db308edf721e");
public static string InvalidContextExternalId = "11111111-1111-1111-1111-111111111111";
public static Guid InvalidContextId = new Guid("11111111-1111-1111-1111-111111111111");
public static string ContextType = "ProjectMaster";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,27 +279,28 @@ public async Task Get_OnlyGlobalAppsForPortal_WithoutContext_AsAuthenticatedUser
var portalToTest = portals?.FirstOrDefault();

// Act
var apps = await AssertGetAppsForPortal(portalToTest!.Id, null, null, UserType.Authenticated, HttpStatusCode.OK);
var apps = await AssertGetAppsForPortal(portalToTest!.Id, null, UserType.Authenticated, HttpStatusCode.OK);

// Assert
Assert.IsNotNull(apps);
Assert.AreEqual(apps.Count, 2);
}

[Ignore] // TODO: Need to perform clean up after each test
[Ignore]// TODO: Need to perform clean up after each test
[TestMethod] // Limitation: Invalid context not currently tested
public async Task Get_BothGlobalAndContextAppsForPortal_WithValidContext_AsAuthenticatedUser_ShouldReturnOk()
{
// Arrange
var portals = await AssertGetAllPortals(UserType.Authenticated, HttpStatusCode.OK);
var portalToTest = portals?.FirstOrDefault();
var portalToTest = portals?.SingleOrDefault(x => x.Key == PortalData.InitialSeedData.Portal2.Key);

// Act
var apps = await AssertGetAppsForPortal(portalToTest!.Id, FusionContextData.InitialSeedData.JcaContextExternalId, FusionContextData.InitialSeedData.ContextType, UserType.Authenticated, HttpStatusCode.OK);
var apps = await AssertGetAppsForPortal(portalToTest!.Id, FusionContextData.InitialSeedData.JcaContextId, UserType.Authenticated, HttpStatusCode.OK);

// Assert
Assert.IsNotNull(apps);
Assert.AreEqual(apps.Count, 3);
Assert.AreEqual(apps.Count, 6);

}

[Ignore]
Expand All @@ -311,7 +312,7 @@ public async Task Get_BothGlobalAndContextAppsForPortal_WithInvalidContext_AsAut
var portalToTest = portals?.FirstOrDefault();

// Act
await AssertGetAppsForPortal(portalToTest!.Id, FusionContextData.InitialSeedData.InvalidContextExternalId, FusionContextData.InitialSeedData.ContextType, UserType.Authenticated, HttpStatusCode.OK);
var apps = await AssertGetAppsForPortal(portalToTest!.Id, FusionContextData.InitialSeedData.InvalidContextId,UserType.Authenticated, HttpStatusCode.OK);

// Assert
// TODO Fusion 404 returned
Expand All @@ -321,7 +322,7 @@ public async Task Get_BothGlobalAndContextAppsForPortal_WithInvalidContext_AsAut
public async Task Get_AppsForNonExistentPortal_AsAuthenticatedUser_ShouldReturnNotFound()
{
// Act & Assert
var response = await GetAppsForPortal(Guid.NewGuid(), null, null, UserType.Authenticated);
var response = await GetAppsForPortal(Guid.NewGuid(), null, UserType.Authenticated);

Assert.AreEqual(HttpStatusCode.NotFound, response.StatusCode);
}
Expand All @@ -330,7 +331,7 @@ public async Task Get_AppsForNonExistentPortal_AsAuthenticatedUser_ShouldReturnN
public async Task Get_AppsForPortal_WithoutContext_AsAnonymousUser_ShouldReturnUnauthorized()
{
// Act
var apps = await AssertGetAppsForPortal(Guid.NewGuid(), null, null, UserType.Anonymous, HttpStatusCode.Unauthorized);
var apps = await AssertGetAppsForPortal(Guid.NewGuid(), null, UserType.Anonymous, HttpStatusCode.Unauthorized);

// Assert
Assert.IsNull(apps);
Expand All @@ -340,12 +341,65 @@ public async Task Get_AppsForPortal_WithoutContext_AsAnonymousUser_ShouldReturnU
public async Task Get_AppsForPortal_WithValidContext_AsAnonymousUser_ShouldReturnUnauthorized()
{
// Act
var apps = await AssertGetAppsForPortal(Guid.NewGuid(), FusionContextData.InitialSeedData.JcaContextExternalId, FusionContextData.InitialSeedData.ContextType, UserType.Anonymous, HttpStatusCode.Unauthorized);
var apps = await AssertGetAppsForPortal(Guid.NewGuid(), FusionContextData.InitialSeedData.JcaContextId, UserType.Anonymous, HttpStatusCode.Unauthorized);

// Assert
Assert.IsNull(apps);
}

[TestMethod]
public async Task Delete_Portal_AsAdministrator_ShouldReturnOk()
{
// Arrange
var payload = new ApiCreatePortalRequest
{
Name = "Portal to be deleted",
Description = "",
ShortName = "Created short name",
Subtext = "Created subtext",
Icon = "Created icon",
ContextTypes = new List<string> { "ProjectMaster" }
};

await CreatePortal(UserType.Administrator, payload);
var getAllAfterCreation = await AssertGetAllPortals(UserType.Administrator, HttpStatusCode.OK);
var theOneCreatedToBeDeleted = getAllAfterCreation!.Last();

// Act
var response = await DeletePortal(theOneCreatedToBeDeleted!.Id, UserType.Administrator);

// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);

// Verify the portal is actually deleted
var deletedPortal = await AssertGetPortal(theOneCreatedToBeDeleted.Id, UserType.Authenticated, HttpStatusCode.NotFound);
Assert.IsNull(deletedPortal);
}

[TestMethod]
public async Task Delete_PortalWithApps_AsAdministrator_ShouldReturnForbidden()
{
// Arrange
var portals = await AssertGetAllPortals(UserType.Administrator, HttpStatusCode.OK);
var portalToDelete = portals?.SingleOrDefault(x => x.Key == PortalData.InitialSeedData.Portal2.Key);

// Ensure the portal has apps
var apps = await AssertGetAppsForPortal(portalToDelete!.Id, FusionContextData.InitialSeedData.JcaContextId, UserType.Administrator, HttpStatusCode.OK);

Assert.IsNotNull(apps);
Assert.IsTrue(apps.Count > 0);

// Act
var response = await DeletePortal(portalToDelete.Id, UserType.Administrator);

// Assert
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);

// Verify the portal is not deleted
var deletedPortal = await AssertGetPortal(portalToDelete.Id, UserType.Administrator, HttpStatusCode.OK);
Assert.IsNotNull(deletedPortal);
}

#region Helpers

private static async Task<IList<ApiPortal>?> AssertGetAllPortals(UserType userType, HttpStatusCode expectedStatusCode)
Expand Down Expand Up @@ -387,7 +441,7 @@ public async Task Get_AppsForPortal_WithValidContext_AsAnonymousUser_ShouldRetur

if (response.StatusCode != HttpStatusCode.OK)
{
return portal;
return null;
}

Assert.IsNotNull(content);
Expand Down Expand Up @@ -453,11 +507,11 @@ private static async Task<HttpResponseMessage> UpdatePortalConfiguration(UserTyp

return response;
}

private static async Task<IList<ApiPortalApp>?> AssertGetAppsForPortal(Guid portalId, string? contextExternalId, string? contextType, UserType userType, HttpStatusCode expectedStatusCode)
private static async Task<IList<ApiPortalApp>?> AssertGetAppsForPortal(Guid portalId, Guid? contextId, UserType userType, HttpStatusCode expectedStatusCode)
{
// Act
var response = await GetAppsForPortal(portalId, contextExternalId, contextType, userType);
var response = await GetAppsForPortal(portalId, contextId, userType);
var content = await response.Content.ReadAsStringAsync();
var apps = JsonConvert.DeserializeObject<IList<ApiPortalApp>>(content);

Expand Down Expand Up @@ -504,15 +558,24 @@ private static async Task<HttpResponseMessage> GetPortalConfiguration(Guid porta
return response;
}

private static async Task<HttpResponseMessage> GetAppsForPortal(Guid portalId, string? contextExternalId, string? contextType, UserType userType)
private static async Task<HttpResponseMessage> GetAppsForPortal(Guid portalId, Guid? contextId, UserType userType)
{
var route = contextExternalId != null ? $"{Route}/{portalId}/contexts/{contextExternalId}/apps" : $"{Route}/{portalId}/apps";
var route = contextId != null ? $"{Route}/{portalId}/contexts/{contextId}/apps" : $"{Route}/{portalId}/apps";
var client = TestFactory.Instance.GetHttpClient(userType);
var response = await client.GetAsync(route);

return response;
}

private static async Task<HttpResponseMessage> DeletePortal(Guid portalId, UserType userType)
{
var route = $"{Route}/{portalId}";
var client = TestFactory.Instance.GetHttpClient(userType);
var response = await client.DeleteAsync(route);

return response;
}

#endregion Helpers
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Equinor.ProjectExecutionPortal.Tests.WebApi.Data;
using Equinor.ProjectExecutionPortal.WebApi.Misc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.DependencyInjection;

namespace Equinor.ProjectExecutionPortal.Tests.WebApi.Misc
Expand Down Expand Up @@ -68,8 +67,8 @@ private static void SeedPortal(DbContext dbContext)

dbContext.AddRange(jcaContext);
dbContext.SaveChanges();

// Add apps to work surface
// Add apps to portal

var globalMeetingsApp = new PortalApp(meetingsApp.Id, portalWithApps.Id);
var globalReviewsApp = new PortalApp(reviewsApp.Id, portalWithApps.Id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ private void SetupServiceMock()
{
return Task.FromResult(FusionContextData.ValidFusionContexts.FirstOrDefault(x => x.ExternalId == contextIdentifier.Identifier));
});

_fusionContextResolverMock.Setup(service => service.GetContextAsync(It.IsAny<Guid>())).Returns((Guid contextId) =>
{
return Task.FromResult(FusionContextData.ValidFusionContexts.First(x => x.Id == contextId));
});

}

private void SetupTestUsers()
Expand Down

0 comments on commit 73b2e3f

Please sign in to comment.