diff --git a/.nvmrc b/.nvmrc index 603606bc911..87ec8842b15 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.17.0 +18.18.2 diff --git a/azure-pipelines.dfe.yml b/azure-pipelines.dfe.yml index 95b4673970e..efb4dd5d185 100644 --- a/azure-pipelines.dfe.yml +++ b/azure-pipelines.dfe.yml @@ -10,7 +10,7 @@ variables: BuildConfiguration: 'Release' IsBranchDeployable: ${{ containsValue(parameters.DeployBranches, variables['Build.SourceBranchName']) }} CI: true - NODE_VERSION: 18.17.0 + NODE_VERSION: 18.18.2 DOTNET_VERSION: 6.0.x trigger: @@ -20,7 +20,7 @@ trigger: - dev paths: exclude: - - infrastructure/ + - infrastructure/ pr: - master - dev diff --git a/docker/public-frontend/Dockerfile b/docker/public-frontend/Dockerfile index bb29c5f3e1d..53a6dfa28f8 100644 --- a/docker/public-frontend/Dockerfile +++ b/docker/public-frontend/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18.17.0-alpine AS base +FROM node:18.18.2-alpine AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" diff --git a/infrastructure/templates/template.json b/infrastructure/templates/template.json index 51ccef60552..91b5cf0d193 100644 --- a/infrastructure/templates/template.json +++ b/infrastructure/templates/template.json @@ -2078,7 +2078,7 @@ "NEXT_CONFIG_MODE": "server", "NODE_ENV": "production", "PUBLIC_URL": "[concat(variables('publicAppUrl'), '/')]", - "WEBSITE_NODE_DEFAULT_VERSION": "18.17.0", + "WEBSITE_NODE_DEFAULT_VERSION": "18.18.2", "WEBSITES_PORT": 3000 } }, diff --git a/package.json b/package.json index d25ea472a8f..8acb0fa66e6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "packageManager": "pnpm@8.8.0", "engines": { - "node": "18.17.0", + "node": "18.18.2", "pnpm": ">=8.8.0" }, "devDependencies": { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandlerTests.cs index 6269ac75ad7..e47d27b0d9d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandlerTests.cs @@ -11,6 +11,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static Moq.MockBehavior; @@ -98,9 +99,11 @@ private static AdoptMethodologyForSpecificPublicationAuthorizationHandler SetupH ) { return new AdoptMethodologyForSpecificPublicationAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - userPublicationRoleRepository ?? Mock.Of(Strict))); + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), + Mock.Of(Strict), + userPublicationRoleRepository ?? Mock.Of(Strict), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlerTests.cs index 46e3443e46a..0891fc5be14 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlerTests.cs @@ -3,12 +3,16 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; +using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { @@ -22,8 +26,8 @@ public async Task AssignPrereleaseContactsToSpecificReleaseAuthorizationHandler_ { // Assert that users with the "UpdateAllReleases" claim can assign pre release contacts to a release // that's unapproved - await AssertReleaseHandlerSucceedsWithCorrectClaims - ( + await AssertHandlerSucceedsWithCorrectClaims + ( CreateHandler, new Release { @@ -37,8 +41,8 @@ public async Task AssignPrereleaseContactsToSpecificReleaseAuthorizationHandler_ { // Assert that users with the "UpdateAllReleases" claim can assign pre release contacts to a release // that's approved - await AssertReleaseHandlerSucceedsWithCorrectClaims - ( + await AssertHandlerSucceedsWithCorrectClaims + ( CreateHandler, new Release { @@ -117,6 +121,7 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = ReleaseApprovalStatus.Draft }, ReleaseRole.Approver, ReleaseRole.Contributor, ReleaseRole.Lead); @@ -132,6 +137,7 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = ReleaseApprovalStatus.Approved }, ReleaseRole.Approver, ReleaseRole.Contributor, ReleaseRole.Lead); @@ -141,9 +147,11 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< private static IAuthorizationHandler CreateHandler(ContentDbContext contentDbContext) { return new AssignPrereleaseContactsToSpecificReleaseAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + contentDbContext, new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext)) + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict)) ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlerTests.cs index 0c7f57c3bd9..10fcb1cf300 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlerTests.cs @@ -178,7 +178,7 @@ public async Task UserCannotCreateMethodologyForPublicationWithoutPublicationOwn await handler.HandleAsync(authContext); VerifyAllMocks(userPublicationRoleRepository); - // Verify that the user can't create a Methodology for this Publication because they don't have + // Verify that the user can't create a Methodology for this Publication because they don't have // Publication Owner role on it Assert.False(authContext.HasSucceeded); } @@ -241,15 +241,17 @@ private static AuthorizationHandlerContext CreateAuthContext(ClaimsPrincipal use private static (CreateMethodologyForSpecificPublicationAuthorizationHandler, Mock) - CreateHandlerAndDependencies(ContentDbContext context) + CreateHandlerAndDependencies(ContentDbContext contentDbContext) { var userPublicationRoleRepository = new Mock(Strict); var handler = new CreateMethodologyForSpecificPublicationAuthorizationHandler( - context, - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - userPublicationRoleRepository.Object)); + contentDbContext, + new AuthorizationHandlerService( + contentDbContext, + Mock.Of(Strict), + userPublicationRoleRepository.Object, + Mock.Of(Strict))); return (handler, userPublicationRoleRepository); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlerTests.cs index 2de3c9f7c91..2fb2653348c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlerTests.cs @@ -14,6 +14,8 @@ using Microsoft.AspNetCore.Authorization; using Moq; using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers; @@ -138,13 +140,15 @@ private static Mock) CreateHandlerAndDependencies() { - var userReleaseRoleRepository = new Mock(MockBehavior.Strict); - var userPublicationRoleRepository = new Mock(MockBehavior.Strict); + var userReleaseRoleRepository = new Mock(Strict); + var userPublicationRoleRepository = new Mock(Strict); var handler = new CreateReleaseForSpecificPublicationAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return (handler, userPublicationRoleRepository); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs index 446c358f144..a20408de5b2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs @@ -4,7 +4,6 @@ using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; -using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Moq; using Xunit; @@ -46,10 +45,11 @@ await AssertHandlerOnlySucceedsWithReleaseRoles contentDbContext.Add(release), contentDbContext => new DeleteSpecificCommentAuthorizationHandler( contentDbContext, - new ReleasePublishingStatusRepository(Mock.Of()), - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + contentDbContext, new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext))), + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))), ReleaseRole.Approver, ReleaseRole.Contributor, ReleaseRole.Lead); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlerTests.cs index 5f78f2ae776..0539594c727 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlerTests.cs @@ -14,6 +14,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.EnumUtil; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; @@ -81,7 +82,7 @@ await ForEachSecurityClaimAsync(async claim => ) = CreateHandlerAndDependencies(); // If the Claim given to the handler isn't enough to make the handler succeed, it'll go on to check - // the user's Publication Roles. + // the user's Publication Roles. if (!expectClaimToSucceed) { methodologyRepository.Setup(s => @@ -141,7 +142,7 @@ await ForEachSecurityClaimAsync(async claim => ) = CreateHandlerAndDependencies(); // If the Claim given to the handler isn't enough to make the handler succeed, it'll go on to check - // the user's Publication Roles. + // the user's Publication Roles. if (!expectClaimToSucceed) { methodologyRepository.Setup(s => @@ -342,9 +343,11 @@ private static ( var handler = new DeleteSpecificMethodologyAuthorizationHandler( methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - userPublicationRoleRepository.Object)); + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), + Mock.Of(Strict), + userPublicationRoleRepository.Object, + Mock.Of(Strict))); return (handler, methodologyRepository, userPublicationRoleRepository); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlerTests.cs index 80a993eddce..f8c04a4f514 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlerTests.cs @@ -3,12 +3,16 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { @@ -21,7 +25,7 @@ public class ClaimsTests public async Task DeleteSpecificReleaseAuthorizationHandler_NotAmendment() { // Assert that no users can delete a non-amendment release - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { @@ -34,7 +38,7 @@ await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { @@ -48,7 +52,7 @@ public async Task DeleteSpecificReleaseAuthorizationHandler_UnapprovedAmendment( { // Assert that users with the "DeleteAllReleaseAmendments" claim can delete an amendment release that is not // yet approved - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { @@ -183,9 +187,11 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandlerTests.cs index e4fc23d2466..50c480cc834 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandlerTests.cs @@ -10,6 +10,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; @@ -182,9 +183,11 @@ private static DropMethodologyLinkAuthorizationHandler SetupHandler( ) { return new( - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), Mock.Of(Strict), - userPublicationRoleRepository ?? Mock.Of(Strict))); + userPublicationRoleRepository ?? Mock.Of(Strict), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlersTests.cs index 54c6da4ff90..1242d42470d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlersTests.cs @@ -43,9 +43,11 @@ await AssertPublicationHandlerSucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } @@ -80,9 +82,11 @@ await AssertHandlerOnlySucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } @@ -117,9 +121,11 @@ await AssertHandlerOnlySucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } @@ -154,9 +160,11 @@ await AssertHandlerOnlySucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfReleaseAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfReleaseAuthorizationHandlersTests.cs index bf129be4d3e..4d1de462335 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfReleaseAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfReleaseAuthorizationHandlersTests.cs @@ -9,6 +9,7 @@ using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; @@ -32,7 +33,7 @@ public async Task MakeAmendmentOfSpecificReleaseAuthorizationHandler_OnlyVersion }; // Assert that no users can amend a draft Release that is the only version - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( contentDbContext => { contentDbContext.Add(release); @@ -54,7 +55,7 @@ public async Task MakeAmendmentOfSpecificReleaseAuthorizationHandler_OnlyVersion }; // Assert that users with the "MakeAmendmentOfAllReleases" claim can amend a published Release that is the only version - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( contentDbContext => { contentDbContext.Add(release); @@ -87,7 +88,7 @@ public async Task MakeAmendmentOfSpecificReleaseAuthorizationHandler_DraftVersio }; // Assert that no users can amend an amendment Release if it is not yet approved - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( contentDbContext => { contentDbContext.Add(publication); @@ -120,7 +121,7 @@ public async Task MakeAmendmentOfSpecificReleaseAuthorizationHandler_NotLatestVe }; // Assert that no users can amend an amendment Release if it is not the latest version - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( contentDbContext => { contentDbContext.Add(publication); @@ -153,7 +154,7 @@ public async Task MakeAmendmentOfSpecificReleaseAuthorizationHandler_LatestVersi }; // Assert that users with the "MakeAmendmentOfAllReleases" claim can amend a published Release that is the latest version - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( contentDbContext => { contentDbContext.Add(publication); @@ -496,9 +497,11 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new AuthorizationHandlerService( + contentDbContext, + Mock.Of(Strict), + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlerTests.cs index 820dbc26f64..221656a527e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlerTests.cs @@ -14,6 +14,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.EnumUtil; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; @@ -235,9 +236,11 @@ private static (MakeAmendmentOfSpecificMethodologyAuthorizationHandler, var handler = new MakeAmendmentOfSpecificMethodologyAuthorizationHandler( methodologyVersionRepository.Object, methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), Mock.Of(Strict), - userPublicationRoleRepository.Object)); + userPublicationRoleRepository.Object, + Mock.Of(Strict))); return (handler, methodologyRepository, methodologyVersionRepository, userPublicationRoleRepository); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlerTests.cs index 198b7f18a3d..a88327ce3e8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlerTests.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services; using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Microsoft.AspNetCore.Authorization; @@ -101,7 +102,7 @@ public async Task UserCannotManageExternalMethodologyForPublicationWithoutPublic await handler.HandleAsync(authContext); VerifyAllMocks(userPublicationRoleRepository); - // Verify that the user can't create a Methodology for this Publication because they don't have + // Verify that the user can't create a Methodology for this Publication because they don't have // Publication Owner role on it Assert.False(authContext.HasSucceeded); } @@ -121,9 +122,11 @@ private static (ManageExternalMethodologyForSpecificPublicationAuthorizationHand var userPublicationRoleRepository = new Mock(Strict); var handler = new ManageExternalMethodologyForSpecificPublicationAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - userPublicationRoleRepository.Object)); + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), + Mock.Of(Strict), + userPublicationRoleRepository.Object, + Mock.Of(Strict))); return (handler, userPublicationRoleRepository); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandlerTests.cs index 8aa0811f36a..d96427cf8a8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandlerTests.cs @@ -11,6 +11,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; @@ -353,9 +354,11 @@ private static var handler = new MarkMethodologyAsApprovedAuthorizationHandler( methodologyVersionRepository.Object, methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return ( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandlerTests.cs index 40661cafcdd..4adde0f91ad 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandlerTests.cs @@ -8,10 +8,11 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Moq; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; @@ -408,9 +409,11 @@ private static ( var handler = new MarkMethodologyAsDraftAuthorizationHandler( methodologyVersionRepository.Object, methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return ( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsHigherReviewAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsHigherReviewAuthorizationHandlerTests.cs index ee05688b5a6..64b4f25dad5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsHigherReviewAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/MarkMethodologyAsHigherReviewAuthorizationHandlerTests.cs @@ -8,10 +8,11 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Moq; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; @@ -418,9 +419,11 @@ private static ( var handler = new MarkMethodologyAsHigherLevelReviewAuthorizationHandler( methodologyVersionRepository.Object, methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return ( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandlerTests.cs index 500e035d6d4..ab0c8c28367 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandlerTests.cs @@ -1,14 +1,19 @@ #nullable enable +using System; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseRole; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { @@ -18,26 +23,28 @@ public class PublishSpecificReleaseAuthorizationHandlerTests public class ClaimTests { [Fact] - public async Task PublishSpecificReleaseAuthorizationHandler_FailsWhenDraft() + public async Task FailsWhenDraft() { // Assert that no claims will allow a draft Release to be published - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = Draft } ); } [Fact] - public async Task PublishSpecificReleaseAuthorizationHandler_SucceedsWhenApproved() + public async Task SucceedsWhenApproved() { // Assert that the PublishAllReleases claim will allow an approved Release to be published - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = Approved }, PublishAllReleases @@ -48,26 +55,28 @@ await AssertReleaseHandlerSucceedsWithCorrectClaims( CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = Draft } ); } [Fact] - public async Task PublishSpecificReleaseAuthorizationHandler_SucceedsWhenApproved() + public async Task SucceedsWhenApproved() { // Assert that only the Approver User Release role will allow an approved Release to be published await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( CreateHandler, new Release { + Id = Guid.NewGuid(), ApprovalStatus = Approved }, Approver); @@ -77,9 +86,11 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlersTests.cs index a6137cf9eda..d450600a140 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlersTests.cs @@ -13,7 +13,9 @@ using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.EnumUtil; using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static Moq.MockBehavior; @@ -204,7 +206,7 @@ await AssertAllRolesFailWhenReleasePublished( } private static MarkReleaseAsDraftAuthorizationHandler CreateHandler( - Mock releaseStatusRepository, + Mock releaseStatusRepository, ContentDbContext context) { return BuildMarkReleaseAsDraftHandler( @@ -353,7 +355,7 @@ await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< return CreateHandler(releaseStatusRepository, context); }, release, - Owner, + Owner, Approver ); } @@ -395,7 +397,7 @@ await AssertAllRolesFailWhenReleasePublished releaseStatusRepository, + Mock releaseStatusRepository, ContentDbContext context) { return BuildMarkReleaseAsHigherLevelReviewHandler( @@ -541,7 +543,7 @@ await AssertAllRolesFailWhenReleasePublished( } private static MarkReleaseAsApprovedAuthorizationHandler CreateHandler( - Mock releaseStatusRepository, + Mock releaseStatusRepository, ContentDbContext context) { return BuildMarkReleaseAsApprovedHandler( @@ -577,7 +579,7 @@ await GetEnumValues() // Assert that users with the specified claims can update the // Release status if it has not started publishing - await AssertReleaseHandlerSucceedsWithCorrectClaims(context => + await AssertHandlerSucceedsWithCorrectClaims(context => { context.Add(release); context.SaveChanges(); @@ -630,7 +632,7 @@ await GetEnumValues() ); // Assert that no users can update a Release status once it has started publishing - await AssertReleaseHandlerSucceedsWithCorrectClaims(context => + await AssertHandlerSucceedsWithCorrectClaims(context => { context.Add(release); context.SaveChanges(); @@ -677,7 +679,7 @@ await GetEnumValues() .ReturnsAsync(new List()); // Assert that no users can update a Release status once it has been published - await AssertReleaseHandlerSucceedsWithCorrectClaims(context => + await AssertHandlerSucceedsWithCorrectClaims(context => { context.Add(release); context.SaveChanges(); @@ -822,22 +824,26 @@ private static MarkReleaseAsDraftAuthorizationHandler BuildMarkReleaseAsDraftHan IUserReleaseRoleRepository userReleaseRoleRepository) { return new MarkReleaseAsDraftAuthorizationHandler( - releasePublishingStatusRepository, - new AuthorizationHandlerResourceRoleService( + releasePublishingStatusRepository, + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository, - userPublicationRoleRepository)); + userPublicationRoleRepository, + Mock.Of(Strict))); } - + private static MarkReleaseAsHigherLevelReviewAuthorizationHandler BuildMarkReleaseAsHigherLevelReviewHandler( IReleasePublishingStatusRepository releasePublishingStatusRepository, IUserPublicationRoleRepository userPublicationRoleRepository, IUserReleaseRoleRepository userReleaseRoleRepository) { return new MarkReleaseAsHigherLevelReviewAuthorizationHandler( - releasePublishingStatusRepository, - new AuthorizationHandlerResourceRoleService( + releasePublishingStatusRepository, + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository, - userPublicationRoleRepository)); + userPublicationRoleRepository, + Mock.Of(Strict))); } private static MarkReleaseAsApprovedAuthorizationHandler BuildMarkReleaseAsApprovedHandler( @@ -846,10 +852,12 @@ private static MarkReleaseAsApprovedAuthorizationHandler BuildMarkReleaseAsAppro IUserReleaseRoleRepository userReleaseRoleRepository) { return new MarkReleaseAsApprovedAuthorizationHandler( - releasePublishingStatusRepository, - new AuthorizationHandlerResourceRoleService( + releasePublishingStatusRepository, + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository, - userPublicationRoleRepository)); + userPublicationRoleRepository, + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateContactAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateContactAuthorizationHandlerTests.cs index fc32b6fd177..c39d68839e9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateContactAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateContactAuthorizationHandlerTests.cs @@ -36,9 +36,11 @@ await AssertPublicationHandlerSucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new AuthorizationHandlerService( + contentDbContext, + Mock.Of(Strict), + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandlerTests.cs index 07c0df08908..f17c4d837e1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandlerTests.cs @@ -36,9 +36,11 @@ await AssertPublicationHandlerSucceedsWithPublicationRoles(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new AuthorizationHandlerService( + contentDbContext, + Mock.Of(Strict), + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandlerTests.cs index bd696e11fe7..2aa9b078c80 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandlerTests.cs @@ -61,9 +61,11 @@ await AssertHandlerOnlySucceedsWithPublicationRoles private static UpdateReleaseRoleAuthorizationHandler CreateHandler(ContentDbContext contentDbContext) { return new UpdateReleaseRoleAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - new UserPublicationRoleRepository(contentDbContext))); + new AuthorizationHandlerService( + contentDbContext, + Mock.Of(Strict), + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandlerTests.cs index e9c967b2e19..02783e7ba6a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandlerTests.cs @@ -10,6 +10,7 @@ using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; @@ -209,9 +210,11 @@ private static ( var handler = new UpdateSpecificMethodologyAuthorizationHandler( methodologyRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return ( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandlerTests.cs index fe4025af3db..e7afdc346c9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandlerTests.cs @@ -12,11 +12,12 @@ using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using Moq; using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Common.Services.EnumUtil; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; using static GovUk.Education.ExploreEducationStatistics.Publisher.Model.ReleasePublishingStatusOverallStage; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { @@ -25,87 +26,54 @@ public class UpdateSpecificReleaseAuthorizationHandlerTests { public class ClaimTests { + // Test that Releases that are in any approval state other than Approved can be updated + // if the current user has the "UpdateAllReleases" Claim. [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishingNotStarted() + public async Task UpdateAllReleases_ClaimSucceedsIfNotApproved() { - // Assert that only users with the "UpdateAllReleases" claim can - // update an arbitrary Release if it has not started publishing await GetEnumValues() + .Where(releaseStatus => releaseStatus != Approved) .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => + .ForEachAwaitAsync(async status => + { + var release = new Release { - var release = new Release - { - Id = Guid.NewGuid(), - ApprovalStatus = status - }; - - await AssertReleaseHandlerSucceedsWithCorrectClaims( - HandlerSupplier(release), - release, - SecurityClaimTypes.UpdateAllReleases - ); - } - ); + Id = Guid.NewGuid(), + ApprovalStatus = status + }; + + await AssertHandlerSucceedsWithCorrectClaims( + HandlerSupplier(release), + release, + SecurityClaimTypes.UpdateAllReleases + ); + }); } + // Test that Releases that are Approved cannot be updated by a user having any Claim. [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishing() + public async Task AllClaimsFailIfApproved() { - // Assert that no users can update an arbitrary Release - // if it has started publishing - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - ApprovalStatus = status, - }; - - await AssertReleaseHandlerSucceedsWithCorrectClaims( - HandlerSupplierWhenPublishing(release), - release - ); - } - ); - } - - [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublished() - { - // Assert that no users can update an arbitrary - // Release if it has been published - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - ApprovalStatus = status, - Published = DateTime.UtcNow - }; - - await AssertReleaseHandlerSucceedsWithCorrectClaims( - HandlerSupplier(release), - release - ); - } - ); + var release = new Release + { + Id = Guid.NewGuid(), + ApprovalStatus = Approved + }; + + await AssertHandlerSucceedsWithCorrectClaims( + HandlerSupplier(release), + release, + claimsExpectedToSucceed: Array.Empty()); } } public class PublicationRoleTests { [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishingNotStarted() + public async Task PublicationOwnerAndApproversCanUpdateUnapprovedRelease() { await GetEnumValues() + .Where(releaseStatus => releaseStatus != Approved) .ToAsyncEnumerable() .ForEachAwaitAsync( async status => @@ -121,97 +89,51 @@ await GetEnumValues() ApprovalStatus = status }; - // Assert that a User who has the Publication Owner or Approver role - // can update it if it is not Approved - if (status != Approved) - { - await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< - UpdateSpecificReleaseRequirement>( - HandlerSupplier(release), - release, - Owner, Approver - ); - } - else - { - // Assert that a User who has the Publication Approver role - // can update it even if it is Approved - await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< - UpdateSpecificReleaseRequirement>( - HandlerSupplier(release), - release, - Approver - ); - } - } - ); - } - - [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishing() - { - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - Publication = new Publication - { - Id = Guid.NewGuid() - }, - Published = null, - ApprovalStatus = status - }; - - // Assert that no User Publication roles will allow updating a Release once it has started publishing + // Assert that a Publication Owner or Approver can update the Release in any approval + // state other than Admin await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< UpdateSpecificReleaseRequirement>( - HandlerSupplierWhenPublishing(release), - release - ); + HandlerSupplier(release), + release, + rolesExpectedToSucceed: new [] + { + PublicationRole.Owner, + PublicationRole.Approver + }); } ); } [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublished() + public async Task NoRolesCanUpdateApprovedRelease() { - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - Publication = new Publication - { - Id = Guid.NewGuid() - }, - ApprovalStatus = status, - Published = DateTime.UtcNow - }; - - // Assert that no User Publication roles will allow updating a Release once it has been published - await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< - UpdateSpecificReleaseRequirement>( - HandlerSupplier(release), - release - ); - } - ); + var release = new Release + { + Id = Guid.NewGuid(), + Publication = new Publication + { + Id = Guid.NewGuid() + }, + Published = null, + ApprovalStatus = Approved + }; + + // Assert that no Publication Role can update the Release if it is Approved. + await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles< + UpdateSpecificReleaseRequirement>( + HandlerSupplier(release), + release, + rolesExpectedToSucceed: Array.Empty()); } } public class ReleaseRoleTests { [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishingNotStarted() + public async Task EditorsCanUpdateUnapprovedRelease() { await GetEnumValues() + .Where(releaseStatus => releaseStatus != Approved) .ToAsyncEnumerable() .ForEachAwaitAsync( async status => @@ -227,102 +149,45 @@ await GetEnumValues() ApprovalStatus = status }; - // Assert that a User who has the "Contributor", "Lead" or "Approver" role on a - // Release can update it if it is not Approved - if (status != Approved) - { - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< - UpdateSpecificReleaseRequirement>( - HandlerSupplier(release), - release, + // Assert that a Release Editor (Contributor, Lead, Approver) can update the Release + // in any approval state other than Approved. + await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< + UpdateSpecificReleaseRequirement>( + HandlerSupplier(release), + release, + rolesExpectedToSucceed: new[] + { ReleaseRole.Contributor, ReleaseRole.Lead, ReleaseRole.Approver - ); - } - else - { - // Assert that a User who has the "Approver" role on a - // Release can update it if it is Approved - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< - UpdateSpecificReleaseRequirement>( - HandlerSupplier(release), - release, - ReleaseRole.Approver - ); - } + }); } ); } [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublishing() + public async Task NoRolesCanUpdateApprovedRelease() { - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - Publication = new Publication - { - Id = Guid.NewGuid() - }, - Published = null, - ApprovalStatus = status - }; - - // Assert that no User Release roles will allow updating a Release once it has started publishing - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( - HandlerSupplierWhenPublishing(release), - release - ); - } - ); - } - - [Fact] - public async Task UpdateSpecificReleaseAuthorizationHandler_ReleasePublished() - { - await GetEnumValues() - .ToAsyncEnumerable() - .ForEachAwaitAsync( - async status => - { - var release = new Release - { - Id = Guid.NewGuid(), - Publication = new Publication - { - Id = Guid.NewGuid() - }, - ApprovalStatus = status, - Published = DateTime.UtcNow - }; - - // Assert that no User Release roles will allow updating a Release once it has been published - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( - HandlerSupplier(release), - release - ); - } - ); + var release = new Release + { + Id = Guid.NewGuid(), + Publication = new Publication + { + Id = Guid.NewGuid() + }, + Published = null, + ApprovalStatus = Approved + }; + + // Assert that no Publication Role can update the Release if it is Approved. + await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles< + UpdateSpecificReleaseRequirement>( + HandlerSupplier(release), + release, + rolesExpectedToSucceed: Array.Empty()); } } - private static Func HandlerSupplierWhenPublishing( - Release release) - { - var statusListWhenPublishing = new List - { - new() - }; - - return HandlerSupplier(release, statusListWhenPublishing); - } - private static Func HandlerSupplier( Release release, List? publishingStatuses = null) @@ -344,10 +209,11 @@ private static Func contentDbContext.SaveChanges(); return new UpdateSpecificReleaseAuthorizationHandler( - releaseStatusRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + contentDbContext, new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext))); + new UserPublicationRoleRepository(contentDbContext), + Mock.Of(Strict))); }; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/AuthorizationHandlersTestUtil.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/AuthorizationHandlersTestUtil.cs index b0140931212..ce5102576b8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/AuthorizationHandlersTestUtil.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/AuthorizationHandlersTestUtil.cs @@ -34,18 +34,6 @@ await scenarios scenario => AssertHandlerHandlesScenarioSuccessfully(handler, scenario)); } - public static async Task AssertHandlerSucceedsWithCorrectClaims( - IAuthorizationHandler handler, - params SecurityClaimTypes[] claimsExpectedToSucceed) - where TRequirement : IAuthorizationRequirement - { - var scenarios = GetClaimTestScenarios(null, claimsExpectedToSucceed); - await scenarios - .ToAsyncEnumerable() - .ForEachAwaitAsync(scenario => - AssertHandlerHandlesScenarioSuccessfully(handler, scenario)); - } - public static async Task AssertHandlerSucceedsWithCorrectClaims( Func handlerSupplier, TEntity entity, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/PublicationAuthorizationHandlersTestUtil.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/PublicationAuthorizationHandlersTestUtil.cs index 9018a4f10d8..1c2eeae590b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/PublicationAuthorizationHandlersTestUtil.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/PublicationAuthorizationHandlersTestUtil.cs @@ -88,45 +88,6 @@ await AssertPublicationHandlerHandlesScenarioSuccessfully(handlerS }); } - public static async Task AssertPublicationHandlerFailsRegardlessOfPublicationOwner( - Func handlerSupplier, - Publication publication) - where TRequirement : IAuthorizationRequirement - { - var user = CreateClaimsPrincipal(Guid.NewGuid()); - - // Test the handler fails with the Owner role on the Publication for the User - await AssertPublicationHandlerHandlesScenarioSuccessfully(handlerSupplier, - new PublicationHandlerTestScenario - { - User = user, - Entity = publication, - UserPublicationRoles = new List - { - // Setup a UserPublicationRole for this Publication and User - new UserPublicationRole - { - PublicationId = publication.Id, - UserId = user.GetUserId(), - Role = Owner - } - }, - ExpectedToPass = false, - UnexpectedPassMessage = "Expected handler to fail despite having Publication Owner role on the Publication" - }); - - // Test the handler fails without the Owner role on the Publication for the User - await AssertPublicationHandlerHandlesScenarioSuccessfully(handlerSupplier, - new PublicationHandlerTestScenario - { - User = user, - Entity = publication, - UserPublicationRoles = new List(), - ExpectedToPass = false, - UnexpectedPassMessage = "Expected handler to fail with no roles on the Publication" - }); - } - public class PublicationHandlerTestScenario : HandlerTestScenario { public List? UserPublicationRoles { get; set; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/ReleaseAuthorizationHandlersTestUtil.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/ReleaseAuthorizationHandlersTestUtil.cs index 91bade0f6b5..26fab59869f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/ReleaseAuthorizationHandlersTestUtil.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/Utils/ReleaseAuthorizationHandlersTestUtil.cs @@ -16,66 +16,6 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Author { public static class ReleaseAuthorizationHandlersTestUtil { - /** - * Assert that the given handler succeeds when a user has any of the "claimsExpectedToSucceed" and is tested - * against an arbitrary Release, and fails otherwise - */ - public static async Task AssertReleaseHandlerSucceedsWithCorrectClaims( - IAuthorizationHandler handler, - params SecurityClaimTypes[] claimsExpectedToSucceed) - where TRequirement : IAuthorizationRequirement - { - var release = new Release - { - Id = Guid.NewGuid() - }; - - await AssertHandlerSucceedsWithCorrectClaims(handler, release, - claimsExpectedToSucceed); - } - - public static async Task AssertReleaseHandlerSucceedsWithCorrectClaims( - Func handlerSupplier, - params SecurityClaimTypes[] claimsExpectedToSucceed) - where TRequirement : IAuthorizationRequirement - { - var release = new Release - { - Id = Guid.NewGuid() - }; - - await AssertHandlerSucceedsWithCorrectClaims(handlerSupplier, release, - claimsExpectedToSucceed); - } - - public static async Task AssertReleaseHandlerSucceedsWithCorrectClaims( - Func handlerSupplier, - Release release, - params SecurityClaimTypes[] claimsExpectedToSucceed) - where TRequirement : IAuthorizationRequirement - { - await AssertHandlerSucceedsWithCorrectClaims(handlerSupplier, release, - claimsExpectedToSucceed); - } - - /** - * Assert that the given handler succeeds when a user has any of the "rolesExpectedToSucceed" on an arbitrary - * Release, and fails otherwise - */ - public static async Task AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( - Func handlerSupplier, - params ReleaseRole[] rolesExpectedToSucceed) - where TRequirement : IAuthorizationRequirement - { - var release = new Release - { - Id = Guid.NewGuid() - }; - - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles(handlerSupplier, release, - rolesExpectedToSucceed); - } - /** * Assert that the given handler succeeds when a user has any of the "rolesExpectedToSucceed" on the supplied * Release, and fails otherwise @@ -322,7 +262,7 @@ public static async Task AssertReleaseHandlerHandlesScenarioSuccessfully(handler, scenario); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandlerTests.cs index 0615b14068e..2f348abf4a7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandlerTests.cs @@ -2,12 +2,16 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils. ReleaseAuthorizationHandlersTestUtil; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { @@ -19,14 +23,14 @@ public class ClaimTests [Fact] public async Task ViewReleaseStatusHistoryAuthorizationHandler_ReleaseRoles() { - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, new Release(), AccessAllReleases ); } - } - + } + public class ReleaseRoleTests { [Fact] @@ -41,8 +45,8 @@ await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlerTests.cs index 633fae5f14c..0b5a8d02aec 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlerTests.cs @@ -12,6 +12,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; @@ -406,9 +407,11 @@ private static ( userReleaseRoleRepository.Object, preReleaseService.Object, publicationRepository.Object, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), userReleaseRoleRepository.Object, - userPublicationRoleRepository.Object) + userPublicationRoleRepository.Object, + Mock.Of(Strict)) ); return ( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandlersTests.cs index 02693d77fd8..3dec56e09f2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandlersTests.cs @@ -3,11 +3,15 @@ using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; +using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers; @@ -18,8 +22,12 @@ public async Task ViewSpecificPreReleaseSummary_SucceedsWithAccessAllReleasesCla { // Assert that any users with the "AccessAllReleases" claim can view an arbitrary PreRelease Summary // (and no other claim allows this) - await AssertReleaseHandlerSucceedsWithCorrectClaims( + await AssertHandlerSucceedsWithCorrectClaims( CreateHandler, + new Release + { + Id = Guid.NewGuid() + }, AccessAllReleases); } @@ -29,6 +37,10 @@ public async Task ViewSpecificPreReleaseSummary_SucceedsWithReleaseRoles() // Assert that a User who has any unrestricted viewer role on a Release can view the PreRelease Summary await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( CreateHandler, + new Release + { + Id = Guid.NewGuid() + }, ReleaseRole.Viewer, ReleaseRole.Lead, ReleaseRole.Contributor, ReleaseRole.Approver, ReleaseRole.PrereleaseViewer); } @@ -52,8 +64,10 @@ await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles(Strict))); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlersTests.cs index edae2d7f099..14794bc9a4d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlersTests.cs @@ -1,19 +1,20 @@ +#nullable enable using System; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Admin.Services; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services; +using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; using Moq; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.ViewSpecificPublicationAuthorizationHandler; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.PublicationAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers @@ -22,34 +23,30 @@ public class ViewSpecificPublicationAuthorizationHandlersTests { private readonly Guid _userId = Guid.NewGuid(); - private readonly Publication _publication = new Publication + private readonly Publication _publication = new() { Id = Guid.NewGuid() }; - + [Fact] - public async Task CanSeeAllPublicationsAuthorizationHandler() + public async Task SucceedsWithAccessAllPublicationsClaim() { - // Assert that any users with the "AccessAllReleases" claim can view an arbitrary Publication + // Assert that any users with the "AccessAllPublications" claim can view an arbitrary Publication // (and no other claim allows this) - // - // Note that we're deliberately using the "All RELEASES" claim rather than having to have a separate - // "All PUBLICATIONS" claim, as they're effectively the same - await AssertHandlerSucceedsWithCorrectClaims( - new CanSeeAllPublicationsAuthorizationHandler(), - AccessAllReleases); + await AssertHandlerSucceedsWithCorrectClaims( + context => CreateHandler(context), + _publication, + AccessAllPublications); } [Fact] public async Task HasOwnerOrApproverRoleOnPublicationAuthorizationHandler_Succeeds() { - await AssertPublicationHandlerSucceedsWithPublicationRoles< - ViewSpecificPublicationRequirement>(contentDbContext => - new HasOwnerOrApproverRoleOnPublicationAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - new UserPublicationRoleRepository(contentDbContext))), - Owner, Approver); + await AssertPublicationHandlerSucceedsWithPublicationRoles( + contentDbContext => CreateHandler( + contentDbContext, + userPublicationRoleRepository: new UserPublicationRoleRepository(contentDbContext)), + EnumUtil.GetEnumValuesAsArray()); } [Fact] @@ -60,19 +57,19 @@ public async Task HasRoleOnAnyChildReleaseAuthorizationHandler_NoReleasesOnThisP Id = Guid.NewGuid(), PublicationId = Guid.NewGuid() }; - + var releaseOnThisPublication = new Release { Id = Guid.NewGuid(), PublicationId = _publication.Id }; - + var releaseRoleForDifferentPublication = new UserReleaseRole { UserId = _userId, Release = releaseOnAnotherPublication }; - + var releaseRoleForDifferentUser = new UserReleaseRole { UserId = Guid.NewGuid(), @@ -84,7 +81,7 @@ await AssertHasRoleOnAnyChildReleaseHandlesOk( releaseRoleForDifferentPublication, releaseRoleForDifferentUser); } - + [Fact] public async Task HasRoleOnAnyChildReleaseAuthorizationHandler_HasRoleOnAReleaseOfThisPublication() { @@ -102,7 +99,7 @@ public async Task HasRoleOnAnyChildReleaseAuthorizationHandler_HasRoleOnARelease await AssertHasRoleOnAnyChildReleaseHandlesOk(true, roleOnThisPublication); } - + private async Task AssertHasRoleOnAnyChildReleaseHandlesOk(bool expectedToSucceed, params UserReleaseRole[] releaseRoles) { await using (var context = DbUtils.InMemoryApplicationDbContext()) @@ -112,18 +109,35 @@ private async Task AssertHasRoleOnAnyChildReleaseHandlesOk(bool expectedToSuccee var handler = new ViewSpecificPublicationAuthorizationHandler( context, - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + context, Mock.Of(Strict), - new UserPublicationRoleRepository(context))); - + new UserPublicationRoleRepository(context), + Mock.Of(Strict))); + var authContext = new AuthorizationHandlerContext( new IAuthorizationRequirement[] {new ViewSpecificPublicationRequirement()}, CreateClaimsPrincipal(_userId), _publication); await handler.HandleAsync(authContext); - + Assert.Equal(expectedToSucceed, authContext.HasSucceeded); } } + + private ViewSpecificPublicationAuthorizationHandler CreateHandler( + ContentDbContext context, + IUserReleaseRoleRepository? userReleaseRoleRepository = null, + IUserPublicationRoleRepository? userPublicationRoleRepository = null, + IPreReleaseService? preReleaseService = null) + { + return new ViewSpecificPublicationAuthorizationHandler( + context, + new AuthorizationHandlerService( + context, + userReleaseRoleRepository ?? new UserReleaseRoleRepository(context), + userPublicationRoleRepository ?? new UserPublicationRoleRepository(context), + preReleaseService ?? Mock.Of(Strict))); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandlersTests.cs index d144ba3d3f3..297974bd1da 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandlersTests.cs @@ -10,6 +10,7 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; @@ -24,7 +25,7 @@ public class ViewSpecificPublicationReleaseTeamAccessAuthorizationHandlersTests { Id = Guid.NewGuid() }; - + [Fact] public async Task ViewSpecificPublicationReleaseTeamAccess_SucceedsWithAccessAllPublicationsClaim() { @@ -42,17 +43,17 @@ await ForEachSecurityClaimAsync(async claim => .Setup(s => s.GetAllRolesByUserAndPublication(UserId, Publication.Id)) .ReturnsAsync(new List()); } - + var handler = CreateHandler(userPublicationRoleRepository.Object); var user = CreateClaimsPrincipal(UserId, claim); - + var authContext = CreateAuthorizationHandlerContext (user, Publication); await handler.HandleAsync(authContext); - + VerifyAllMocks(userPublicationRoleRepository); Assert.Equal(expectedToPassByClaimAlone, authContext.HasSucceeded); @@ -63,13 +64,13 @@ await ForEachSecurityClaimAsync(async claim => public async Task ViewSpecificPublicationReleaseTeamAccess_SucceedsWithPublicationRoles() { await ForEachPublicationRoleAsync(async role => - { + { var userPublicationRoleRepository = new Mock(Strict); userPublicationRoleRepository .Setup(s => s.GetAllRolesByUserAndPublication(UserId, Publication.Id)) .ReturnsAsync(ListOf(role)); - + var handler = CreateHandler(userPublicationRoleRepository.Object); var user = CreateClaimsPrincipal(UserId); @@ -79,11 +80,11 @@ await ForEachPublicationRoleAsync(async role => (user, Publication); await handler.HandleAsync(authContext); - + VerifyAllMocks(userPublicationRoleRepository); Assert.Equal( - ListOf(PublicationRole.Owner, PublicationRole.Approver).Contains(role), + ListOf(PublicationRole.Owner, PublicationRole.Approver).Contains(role), authContext.HasSucceeded); }); } @@ -92,8 +93,10 @@ private static ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler Crea IUserPublicationRoleRepository? userPublicationRoleRepository = null) { return new ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( + new AuthorizationHandlerService( + InMemoryApplicationDbContext(), Mock.Of(Strict), - userPublicationRoleRepository ?? Mock.Of(Strict))); + userPublicationRoleRepository ?? Mock.Of(Strict), + Mock.Of(Strict))); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlersTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlersTests.cs index ef208a137f3..30a197753dc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlersTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlersTests.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -8,142 +9,206 @@ using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Security.AuthorizationHandlers; +using Microsoft.Extensions.Options; using Moq; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.ViewSpecificReleaseAuthorizationHandler; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.AuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers.Utils.ReleaseAuthorizationHandlersTestUtil; using static GovUk.Education.ExploreEducationStatistics.Common.Services.EnumUtil; +using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.AuthorizationHandlers { + // ReSharper disable once ClassNeverInstantiated.Global public class ViewSpecificReleaseAuthorizationHandlersTests { - [Fact] - public async Task CanSeeAllReleasesAuthorizationHandler() + private static readonly Release Release = new() { - // Assert that any users with the "AccessAllReleases" claim can view an arbitrary Release - // (and no other claim allows this) - await AssertReleaseHandlerSucceedsWithCorrectClaims( - new CanSeeAllReleasesAuthorizationHandler(), AccessAllReleases); - } + Id = Guid.NewGuid(), + Publication = new Publication + { + Id = Guid.NewGuid() + } + }; - [Fact] - public async Task HasUnrestrictedViewerRoleOnReleaseAuthorizationHandler() + public class ClaimsTests { - // Assert that a User who has any unrestricted viewer role on a Release can view the Release - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( - contentDbContext => - new HasUnrestrictedViewerRoleOnReleaseAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext))), - ReleaseRole.Viewer, ReleaseRole.Lead, ReleaseRole.Contributor, ReleaseRole.Approver); + [Fact] + public async Task HasAccessAllReleasesClaim() + { + // Assert that any users with the "AccessAllReleases" claim can view an arbitrary Release + // (and no other claim allows this) + await AssertHandlerSucceedsWithCorrectClaims( + contentDbContext => + { + contentDbContext.Attach(Release); + return CreateHandler(contentDbContext); + }, + Release, + claimsExpectedToSucceed: AccessAllReleases); + } } - [Fact] - public async Task HasOwnerOrApproverRoleOnParentPublicationAuthorizationHandler() + public class PublicationRoleTests { - var publication = new Publication + [Fact] + public async Task HasOwnerOrApproverRoleOnParentPublication() { - Id = Guid.NewGuid() - }; - await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles( - contentDbContext => new HasOwnerOrApproverRoleOnParentPublicationAuthorizationHandler( - new AuthorizationHandlerResourceRoleService( - Mock.Of(Strict), - new UserPublicationRoleRepository(contentDbContext))), - new Release - { - PublicationId = publication.Id, - Publication = publication - }, - PublicationRole.Owner, PublicationRole.Approver); + await AssertReleaseHandlerSucceedsWithCorrectPublicationRoles( + contentDbContext => + { + contentDbContext.Attach(Release); + return CreateHandler(contentDbContext); + }, + Release, + rolesExpectedToSucceed: new [] { + PublicationRole.Owner, + PublicationRole.Approver + }); + } } - [Fact] - public async Task HasPreReleaseRoleWithinAccessWindowAuthorizationHandler() + public class ReleaseRoleTests { - var release = new Release(); + [Fact] + public async Task UnrestrictedViewerRoleOnRelease() + { + // Assert that a User who has any unrestricted viewer role on a Release can view the Release + await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( + contentDbContext => + { + contentDbContext.Attach(Release); + return CreateHandler(contentDbContext); + }, + Release, + rolesExpectedToSucceed: new[] + { + ReleaseRole.Viewer, + ReleaseRole.Lead, + ReleaseRole.Contributor, + ReleaseRole.Approver + }); + } - var preReleaseService = new Mock(); + [Fact] + public async Task PreReleaseUser_WithinPreReleaseAccessWindow() + { + var userId = Guid.NewGuid(); - preReleaseService - .Setup(s => s.GetPreReleaseWindowStatus(release, It.IsAny())) - .Returns(new PreReleaseWindowStatus + var successScenario = new ReleaseHandlerTestScenario { - Access = PreReleaseAccess.Within - }); - - // Assert that a User who specifically has the Pre Release role will cause this handler to pass - // IF the Pre Release window is open - await AssertReleaseHandlerSucceedsWithCorrectReleaseRoles( - contentDbContext => - new HasPreReleaseRoleWithinAccessWindowAuthorizationHandler( - preReleaseService.Object, - new AuthorizationHandlerResourceRoleService( - new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext))), - release, - ReleaseRole.PrereleaseViewer); - } + Entity = Release, + User = ClaimsPrincipalUtils.CreateClaimsPrincipal(userId), + UserReleaseRoles = new List + { + new() + { + ReleaseId = Release.Id, + UserId = userId, + Role = ReleaseRole.PrereleaseViewer + } + }, + ExpectedToPass = true, + UnexpectedFailMessage = + "Expected the test to succeed because the Pre Release window is currently open" + }; - [Fact] - public async Task HasPreReleaseRoleWithinAccessWindowAuthorizationHandler_PreReleaseWindowNotOpen() - { - var release = new Release - { - Id = Guid.NewGuid() - }; + var preReleaseService = new Mock(Strict); + + preReleaseService + .Setup(s => s.GetPreReleaseWindowStatus(Release, It.IsAny())) + .Returns(new PreReleaseWindowStatus + { + Access = PreReleaseAccess.Within + }); - var preReleaseService = new Mock(); + // Assert that a User who specifically has the Pre Release role will cause this handler to succeed + // if the Pre Release window is currently open. + await AssertReleaseHandlerHandlesScenarioSuccessfully( + contentDbContext => CreateHandler(contentDbContext, preReleaseService.Object), + successScenario); - var userId = Guid.NewGuid(); + VerifyAllMocks(preReleaseService); + } - var failureScenario = new ReleaseHandlerTestScenario + [Fact] + public async Task PreReleaseUser_OutsidePreReleaseAccessWindow() { - Entity = release, - User = ClaimsPrincipalUtils.CreateClaimsPrincipal(userId), - UserReleaseRoles = new List + var userId = Guid.NewGuid(); + + var failureScenario = new ReleaseHandlerTestScenario { - new UserReleaseRole + Entity = Release, + User = ClaimsPrincipalUtils.CreateClaimsPrincipal(userId), + UserReleaseRoles = new List { - ReleaseId = release.Id, - UserId = userId, - Role = ReleaseRole.PrereleaseViewer - } - }, - ExpectedToPass = false, - UnexpectedPassMessage = "Expected the test to fail because the Pre Release window is not open at the " + - "current time" - }; - - await GetEnumValues() - .Where(value => value != PreReleaseAccess.Within) - .ToList() - .ToAsyncEnumerable() - .ForEachAwaitAsync(async access => - { - preReleaseService - .Setup(s => s.GetPreReleaseWindowStatus(release, It.IsAny())) - .Returns(new PreReleaseWindowStatus + new() + { + ReleaseId = Release.Id, + UserId = userId, + Role = ReleaseRole.PrereleaseViewer + } + }, + ExpectedToPass = false, + UnexpectedPassMessage = + "Expected the test to fail because the Pre Release window is not open at the " + + "current time" + }; + + await GetEnumValues() + .Where(value => value != PreReleaseAccess.Within) + .ToList() + .ToAsyncEnumerable() + .ForEachAwaitAsync(async access => + { + var preReleaseService = new Mock(Strict); + + preReleaseService + .Setup(s => s.GetPreReleaseWindowStatus(Release, It.IsAny())) + .Returns(new PreReleaseWindowStatus + { + Access = access + }); + + // Assert that a User who specifically has the Pre Release role will cause this handler to fail + // IF the Pre Release window is NOT open + await AssertReleaseHandlerHandlesScenarioSuccessfully( + contentDbContext => + { + contentDbContext.Attach(Release); + return CreateHandler(contentDbContext, preReleaseService.Object); + }, + failureScenario); + + VerifyAllMocks(preReleaseService); + }); + } + } + + private static ViewSpecificReleaseAuthorizationHandler CreateHandler( + ContentDbContext contentDbContext, + IPreReleaseService? preReleaseService = null) + { + return new ViewSpecificReleaseAuthorizationHandler( + new AuthorizationHandlerService( + contentDbContext, + new UserReleaseRoleRepository(contentDbContext), + new UserPublicationRoleRepository(contentDbContext), + preReleaseService ?? new PreReleaseService(Options.Create(new PreReleaseOptions + { + PreReleaseAccess = new PreReleaseAccessOptions { - Access = access - }); - - // Assert that a User who specifically has the Pre Release role will cause this handler to fail - // IF the Pre Release window is NOT open - await AssertReleaseHandlerHandlesScenarioSuccessfully( - contentDbContext => - new HasPreReleaseRoleWithinAccessWindowAuthorizationHandler( - preReleaseService.Object, - new AuthorizationHandlerResourceRoleService( - new UserReleaseRoleRepository(contentDbContext), - new UserPublicationRoleRepository(contentDbContext))), - failureScenario); - }); + AccessWindow = new AccessWindowOptions + { + MinutesBeforeReleaseTimeEnd = 100, + MinutesBeforeReleaseTimeStart = 200, + } + } + })))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyAmendmentServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyAmendmentServiceTests.cs index 44dfc092541..83edfea9169 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyAmendmentServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyAmendmentServiceTests.cs @@ -32,11 +32,8 @@ public async Task CreateMethodologyAmendment() Id = Guid.NewGuid(), Status = Approved, Published = DateTime.Today, - Methodology = new Methodology - { - Slug = "methodology-slug", - OwningPublicationTitle = "Owning Publication Title" - }, + Methodology = new Methodology(), + Version = 0, MethodologyContent = new MethodologyVersionContent { Content = new List { @@ -104,7 +101,10 @@ public async Task CreateMethodologyAmendment() .Include(m => m.MethodologyContent) .SingleAsync(m => m.Id == amendmentId); + Assert.Equal(originalVersion.MethodologyId, amendment.MethodologyId); + Assert.Equal(originalVersion.Id, amendment.PreviousVersionId); + Assert.Equal(originalVersion.Version + 1, amendment.Version); var contentSection = Assert.Single(amendment.MethodologyContent.Content); Assert.NotNull(contentSection); @@ -129,8 +129,8 @@ public async Task CreateMethodologyAmendmentWithMethodologyFiles() Published = DateTime.Today, Methodology = new Methodology { - Slug = "methodology-slug", - OwningPublicationTitle = "Owning Publication Title" + OwningPublicationTitle = "Owning Publication Title", + OwningPublicationSlug = "owning-publication-slug", }, MethodologyContent = new MethodologyVersionContent { Content = AsList(new ContentSection diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServicePermissionTests.cs index f4490491fec..7222cf00d50 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServicePermissionTests.cs @@ -98,7 +98,8 @@ private MethodologyApprovalService SetupService( IUserService? userService = null, IUserReleaseRoleService? userReleaseRoleService = null, IMethodologyCacheService? methodologyCacheService = null, - IEmailTemplateService? emailTemplateService = null) + IEmailTemplateService? emailTemplateService = null, + IRedirectsCacheService? redirectsCacheService = null) { return new( persistenceHelper ?? DefaultPersistenceHelperMock().Object, @@ -111,7 +112,9 @@ private MethodologyApprovalService SetupService( userService ?? Mock.Of(), userReleaseRoleService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict), - emailTemplateService ?? Mock.Of(Strict)); + emailTemplateService ?? Mock.Of(Strict), + redirectsCacheService ?? Mock.Of(Strict)); + } private Mock> DefaultPersistenceHelperMock() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServiceTests.cs index d09580fce53..6b4c4402418 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyApprovalServiceTests.cs @@ -656,6 +656,7 @@ public async Task UpdateApprovalStatus_ApprovingUsingImmediateStrategy() var methodologyVersionRepository = new Mock(Strict); var publishingService = new Mock(Strict); var methodologyCacheService = new Mock(Strict); + var redirectsCacheService = new Mock(Strict); contentService.Setup(mock => mock.GetContentBlocks(methodologyVersion.Id)) @@ -674,13 +675,17 @@ public async Task UpdateApprovalStatus_ApprovingUsingImmediateStrategy() new Either>( new List())); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupService(contentDbContext: context, methodologyContentService: contentService.Object, methodologyVersionRepository: methodologyVersionRepository.Object, publishingService: publishingService.Object, - methodologyCacheService: methodologyCacheService.Object); + methodologyCacheService: methodologyCacheService.Object, + redirectsCacheService: redirectsCacheService.Object); var updatedMethodologyVersion = (await service.UpdateApprovalStatus(methodologyVersion.Id, request)).AssertRight(); @@ -688,7 +693,8 @@ public async Task UpdateApprovalStatus_ApprovingUsingImmediateStrategy() contentService, methodologyVersionRepository, publishingService, - methodologyCacheService); + methodologyCacheService, + redirectsCacheService); Assert.Equal(methodologyVersion.Id, updatedMethodologyVersion.Id); updatedMethodologyVersion.Published.AssertUtcNow(); @@ -1322,7 +1328,8 @@ private static MethodologyApprovalService SetupService( IUserService? userService = null, IUserReleaseRoleService? userReleaseRoleService = null, IMethodologyCacheService? methodologyCacheService = null, - IEmailTemplateService? emailTemplateService = null) + IEmailTemplateService? emailTemplateService = null, + IRedirectsCacheService? redirectsCacheService = null) { return new( @@ -1336,7 +1343,8 @@ private static MethodologyApprovalService SetupService( userService ?? AlwaysTrueUserService(UserId).Object, userReleaseRoleService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict), - emailTemplateService ?? Mock.Of(Strict)); + emailTemplateService ?? Mock.Of(Strict), + redirectsCacheService ?? Mock.Of(Strict)); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyContentServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyContentServiceTests.cs index b6826af5620..84cc6222c29 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyContentServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyContentServiceTests.cs @@ -27,8 +27,8 @@ public async Task GetContent() { var methodology = new Methodology { - Slug = "methodology-slug", - OwningPublicationTitle = "Methodology title", + OwningPublicationTitle = "Publication title", + OwningPublicationSlug = "publication-slug", Versions = new List { new() @@ -59,7 +59,7 @@ public async Task GetContent() var result = (await service.GetContent(methodologyVersion.Id)).AssertRight(); Assert.Equal(methodologyVersion.Id, result.Id); - Assert.Equal(methodology.Slug, result.Slug); + Assert.Equal(methodologyVersion.Slug, result.Slug); Assert.Equal(methodologyVersion.Title, result.Title); Assert.Equal(methodologyVersion.Status, result.Status); Assert.Equal(methodologyVersion.Published, result.Published); @@ -74,8 +74,8 @@ public async Task GetContent_TestContentSections() { var methodology = new Methodology { - Slug = "methodology-slug", OwningPublicationTitle = "Methodology title", + OwningPublicationSlug = "methodology-title", Versions = new List { new() @@ -191,8 +191,8 @@ public async Task GetContent_TestNotes() { var methodology = new Methodology { - Slug = "methodology-slug", OwningPublicationTitle = "Methodology title", + OwningPublicationSlug = "methodology-title", Versions = new List { new() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServicePermissionTests.cs index 1da533fe80e..fdc2b30116a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServicePermissionTests.cs @@ -46,6 +46,7 @@ public class MethodologyServicePermissionTests { Id = Guid.NewGuid(), AlternativeTitle = "Title", + AlternativeSlug = "title", Status = Draft }; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs index ec0c14c879b..14f3f34e02e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -321,7 +321,8 @@ public async Task CreateMethodology() { var publication = new Publication { - Title = "Test publication" + Title = "Test publication", + Slug = "test-publication", }; var contentDbContextId = Guid.NewGuid().ToString(); @@ -346,8 +347,8 @@ public async Task CreateMethodology() Methodology = new Methodology { Id = Guid.NewGuid(), - Slug = "test-publication", OwningPublicationTitle = publication.Title, + OwningPublicationSlug = publication.Slug, Publications = new List { new() @@ -394,7 +395,8 @@ public async Task CreateMethodology_SlugNotUnique() { var publication = new Publication { - Title = "Test publication" + Title = "Test publication", + Slug = "test-publication", }; var contentDbContextId = Guid.NewGuid().ToString(); @@ -421,8 +423,8 @@ public async Task CreateMethodology_SlugNotUnique() Methodology = new Methodology { Id = Guid.NewGuid(), - Slug = "test-publication", OwningPublicationTitle = publication.Title, + OwningPublicationSlug = publication.Slug, Publications = new List { new() @@ -632,12 +634,14 @@ public async Task GetAdoptableMethodologies() var methodology = new Methodology { LatestPublishedVersionId = Guid.NewGuid(), - Slug = "test-publication", + OwningPublicationTitle = "Owning publication", + OwningPublicationSlug = "owning-publication", }; var publication = new Publication { Title = "Owning publication", + Slug = "owning-publication", Methodologies = new List { new() @@ -654,7 +658,8 @@ public async Task GetAdoptableMethodologies() Published = null, PublishingStrategy = MethodologyPublishingStrategy.Immediately, Status = MethodologyApprovalStatus.Draft, - AlternativeTitle = "Alternative title" + AlternativeTitle = "Alternative title", + AlternativeSlug = "alternative-slug", }; var methodologyStatus = new MethodologyStatus @@ -703,7 +708,6 @@ public async Task GetAdoptableMethodologies() var viewModel = result[0]; Assert.Equal(methodologyVersion.Id, viewModel.Id); - Assert.Equal("test-publication", viewModel.Slug); Assert.False(viewModel.Amendment); Assert.Equal("Test approval", viewModel.InternalReleaseNote); Assert.Equal(methodologyVersion.MethodologyId, viewModel.MethodologyId); @@ -711,6 +715,7 @@ public async Task GetAdoptableMethodologies() Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); Assert.Equal(MethodologyApprovalStatus.Draft, viewModel.Status); Assert.Equal("Alternative title", viewModel.Title); + Assert.Equal("alternative-slug", viewModel.Slug); Assert.Equal(publication.Id, viewModel.OwningPublication.Id); Assert.Equal("Owning publication", viewModel.OwningPublication.Title); @@ -724,7 +729,7 @@ public async Task GetAdoptableMethodologies_NoUnpublishedMethodologies() var methodology = new Methodology { LatestPublishedVersionId = null, // methodology is unpublished - Slug = "test-publication", + OwningPublicationSlug = "test-publication", Versions = new List { new() @@ -819,12 +824,14 @@ public async Task GetMethodology() { var methodology = new Methodology { - Slug = "test-publication" + OwningPublicationTitle = "Owning publication", + OwningPublicationSlug = "owning-publication", }; var owningPublication = new Publication { Title = "Owning publication", + Slug = "owning-publication", Methodologies = new List { new() @@ -838,6 +845,7 @@ public async Task GetMethodology() var adoptingPublication1 = new Publication { Title = "Adopting publication 1", + Slug = "adopting-publication-1", Methodologies = ListOf( new PublicationMethodology { @@ -850,6 +858,7 @@ public async Task GetMethodology() var adoptingPublication2 = new Publication { Title = "Adopting publication 2", + Slug = "adopting-publication-2", Methodologies = ListOf( new PublicationMethodology { @@ -871,7 +880,8 @@ public async Task GetMethodology() ReleaseName = "2021" }, Status = MethodologyApprovalStatus.Approved, - AlternativeTitle = "Alternative title" + AlternativeTitle = "Alternative title", + AlternativeSlug = "alternative-title", }; var methodologyStatus = new MethodologyStatus @@ -899,14 +909,14 @@ public async Task GetMethodology() var viewModel = (await service.GetMethodology(methodologyVersion.Id)).AssertRight(); Assert.Equal(methodologyVersion.Id, viewModel.Id); - Assert.Equal("test-publication", viewModel.Slug); + Assert.Equal("Alternative title", viewModel.Title); + Assert.Equal("alternative-title", viewModel.Slug); Assert.False(viewModel.Amendment); Assert.Equal("Test approval", viewModel.InternalReleaseNote); Assert.Equal(methodologyVersion.MethodologyId, viewModel.MethodologyId); Assert.Equal(new DateTime(2020, 5, 25), viewModel.Published); Assert.Equal(MethodologyPublishingStrategy.WithRelease, viewModel.PublishingStrategy); Assert.Equal(MethodologyApprovalStatus.Approved, viewModel.Status); - Assert.Equal("Alternative title", viewModel.Title); Assert.Equal(owningPublication.Id, viewModel.OwningPublication.Id); Assert.Equal("Owning publication", viewModel.OwningPublication.Title); @@ -1606,8 +1616,8 @@ public async Task UpdateMethodology() Methodology = new Methodology { Id = Guid.NewGuid(), - Slug = "test-publication", OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -1636,7 +1646,8 @@ public async Task UpdateMethodology() { var methodologyVersionRepository = new Mock(Strict); methodologyVersionRepository - .Setup(mock => mock.GetLatestPublishedVersionBySlug("updated-methodology-title")) + .Setup(mock => + mock.GetLatestPublishedVersionBySlug("updated-methodology-title")) .ReturnsAsync((MethodologyVersion?)null); var service = SetupMethodologyService(context, @@ -1644,6 +1655,8 @@ public async Task UpdateMethodology() var viewModel = (await service.UpdateMethodology(methodologyVersion.Id, request)).AssertRight(); + VerifyAllMocks(methodologyVersionRepository); + Assert.Equal(methodologyVersion.Id, viewModel.Id); Assert.Equal("updated-methodology-title", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); @@ -1670,13 +1683,13 @@ public async Task UpdateMethodology() Assert.Equal("Updated Methodology Title", updatedMethodology.Title); Assert.Equal("Updated Methodology Title", updatedMethodology.AlternativeTitle); Assert.Equal("updated-methodology-title", updatedMethodology.Slug); - Assert.Equal("updated-methodology-title", updatedMethodology.Methodology.Slug); + Assert.True(updatedMethodology.Updated.HasValue); updatedMethodology.Updated.AssertUtcNow(); } } [Fact] - public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() + public async Task UpdateMethodology_UpdatingAmendmentSlugChangesAndCreatesRedirect() { var publication = new Publication { @@ -1691,8 +1704,8 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Slug = "test-publication", OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -1730,8 +1743,11 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() var viewModel = (await service.UpdateMethodology(methodologyVersion.Id, request)).AssertRight(); + VerifyAllMocks(methodologyVersionRepository); + Assert.Equal(methodologyVersion.Id, viewModel.Id); - Assert.Equal("test-publication", viewModel.Slug); + Assert.Equal("Updated Methodology Title", viewModel.Title); + Assert.Equal("updated-methodology-title", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); @@ -1745,24 +1761,31 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - var updatedMethodology = await context + var updatedMethodologyVersion = await context .MethodologyVersions .Include(m => m.Methodology) .SingleAsync(m => m.Id == methodologyVersion.Id); - Assert.Null(updatedMethodology.Published); - Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodology.Status); - Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodology.PublishingStrategy); - Assert.Equal("Updated Methodology Title", updatedMethodology.Title); - Assert.Equal("Updated Methodology Title", updatedMethodology.AlternativeTitle); - Assert.Equal("test-publication", updatedMethodology.Slug); - Assert.Equal("test-publication", updatedMethodology.Methodology.Slug); - updatedMethodology.Updated.AssertUtcNow(); + Assert.Null(updatedMethodologyVersion.Published); + Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodologyVersion.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodologyVersion.PublishingStrategy); + Assert.Equal("Updated Methodology Title", updatedMethodologyVersion.Title); + Assert.Equal("Updated Methodology Title", updatedMethodologyVersion.AlternativeTitle); + Assert.Equal("updated-methodology-title", updatedMethodologyVersion.Slug); + Assert.Equal("updated-methodology-title", updatedMethodologyVersion.AlternativeSlug); + Assert.Equal("Test publication", updatedMethodologyVersion.Methodology.OwningPublicationTitle); + Assert.Equal("test-publication", updatedMethodologyVersion.Methodology.OwningPublicationSlug); + Assert.True(updatedMethodologyVersion.Updated.HasValue); + updatedMethodologyVersion.Updated.AssertUtcNow(); + + var methodologyRedirect = await context.MethodologyRedirects + .SingleAsync(mr => mr.MethodologyVersionId == methodologyVersion.Id); + Assert.Equal("test-publication", methodologyRedirect.Slug); } } [Fact] - public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAlternativeTitle() + public async Task UpdateMethodology_UpdatingTitleSlugToMatchPublicationUnsetsAlternativeTitleSlug() { var publication = new Publication { @@ -1774,12 +1797,13 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl { Id = Guid.NewGuid(), AlternativeTitle = "Alternative Methodology Title", + AlternativeSlug = "alternative-methodology-title", PublishingStrategy = MethodologyPublishingStrategy.Immediately, Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Slug = "test-publication", OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -1816,6 +1840,8 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl var viewModel = (await service.UpdateMethodology(methodologyVersion.Id, request)).AssertRight(); + VerifyAllMocks(methodologyVersionRepository); + Assert.Equal(methodologyVersion.Id, viewModel.Id); Assert.Equal("test-publication", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); @@ -1831,27 +1857,28 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - var updatedMethodology = await context + var updatedMethodologyVersion = await context .MethodologyVersions .Include(m => m.Methodology) .SingleAsync(m => m.Id == methodologyVersion.Id); - Assert.Null(updatedMethodology.Published); - Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodology.Status); - Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodology.PublishingStrategy); - Assert.Equal(publication.Title, updatedMethodology.Title); + Assert.Null(updatedMethodologyVersion.Published); + Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodologyVersion.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, + updatedMethodologyVersion.PublishingStrategy); + Assert.Equal(publication.Title, updatedMethodologyVersion.Title); // Test explicitly that AlternativeTitle has been unset. - Assert.Null(updatedMethodology.AlternativeTitle); - - Assert.Equal("test-publication", updatedMethodology.Slug); - Assert.Equal("test-publication", updatedMethodology.Methodology.Slug); - updatedMethodology.Updated.AssertUtcNow(); + Assert.Null(updatedMethodologyVersion.AlternativeTitle); + Assert.Null(updatedMethodologyVersion.AlternativeSlug); + Assert.Equal("test-publication", updatedMethodologyVersion.Slug); + Assert.True(updatedMethodologyVersion.Updated.HasValue); + updatedMethodologyVersion.Updated.AssertUtcNow(); } } [Fact] - public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnsetsAlternativeTitle() + public async Task UpdateMethodology_UpdatingAmendmentUnsetsAlternativeTitleAndSlugAndCreatesRedirect() { var publication = new Publication { @@ -1863,12 +1890,13 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse { Id = Guid.NewGuid(), AlternativeTitle = "Alternative Methodology Title", + AlternativeSlug = "alternative-methodology-title", PublishingStrategy = MethodologyPublishingStrategy.Immediately, Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Slug = "alternative-methodology-title", OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -1906,8 +1934,10 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse var viewModel = (await service.UpdateMethodology(methodologyVersion.Id, request)).AssertRight(); + VerifyAllMocks(methodologyVersionRepository); + Assert.Equal(methodologyVersion.Id, viewModel.Id); - Assert.Equal("alternative-methodology-title", viewModel.Slug); + Assert.Equal("test-publication", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); @@ -1921,22 +1951,28 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - var updatedMethodology = await context + var updatedMethodologyVersion = await context .MethodologyVersions .Include(m => m.Methodology) .SingleAsync(m => m.Id == methodologyVersion.Id); - Assert.Null(updatedMethodology.Published); - Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodology.Status); - Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodology.PublishingStrategy); - Assert.Equal(publication.Title, updatedMethodology.Title); + Assert.Null(updatedMethodologyVersion.Published); + Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodologyVersion.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodologyVersion.PublishingStrategy); + Assert.Equal(publication.Title, updatedMethodologyVersion.Title); // Test that the AlternativeTitle has explicitly be set to null. - Assert.Null(updatedMethodology.AlternativeTitle); + Assert.Null(updatedMethodologyVersion.AlternativeTitle); + Assert.Null(updatedMethodologyVersion.AlternativeSlug); - Assert.Equal("alternative-methodology-title", updatedMethodology.Slug); - Assert.Equal("alternative-methodology-title", updatedMethodology.Methodology.Slug); - updatedMethodology.Updated.AssertUtcNow(); + Assert.Equal("test-publication", updatedMethodologyVersion.Slug); + Assert.True(updatedMethodologyVersion.Updated.HasValue); + updatedMethodologyVersion.Updated.AssertUtcNow(); + + var methodologyRedirect = await context + .MethodologyRedirects + .SingleAsync(mr => mr.MethodologyVersionId == methodologyVersion.Id); + Assert.Equal("alternative-methodology-title", methodologyRedirect.Slug); } } @@ -1956,8 +1992,8 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Slug = "test-publication", OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -1973,8 +2009,8 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Slug = "updated-methodology-title", - OwningPublicationTitle = "Test publication 2" + OwningPublicationTitle = "Test publication 2", + OwningPublicationSlug = "test-publication-2", } }; @@ -2019,15 +2055,17 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() Assert.Equal(MethodologyApprovalStatus.Draft, notUpdatedMethodology.Status); Assert.Equal(MethodologyPublishingStrategy.Immediately, notUpdatedMethodology.PublishingStrategy); Assert.Equal("Test publication", notUpdatedMethodology.Title); + Assert.Equal("Test publication", notUpdatedMethodology.Methodology.OwningPublicationTitle); Assert.Null(notUpdatedMethodology.AlternativeTitle); Assert.Equal("test-publication", notUpdatedMethodology.Slug); - Assert.Equal("test-publication", notUpdatedMethodology.Methodology.Slug); + Assert.Equal("test-publication", notUpdatedMethodology.Methodology.OwningPublicationSlug); + Assert.Null(notUpdatedMethodology.AlternativeSlug); Assert.False(notUpdatedMethodology.Updated.HasValue); } } [Fact] - public async Task UpdateMethodology_StatusUpdate() + public async Task UpdateMethodology_RedirectForNewSlugAlreadyExists() { var publication = new Publication { @@ -2042,9 +2080,9 @@ public async Task UpdateMethodology_StatusUpdate() Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { - Id = Guid.NewGuid(), - Slug = "test-publication", + LatestPublishedVersionId = null, OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", Publications = ListOf(new PublicationMethodology { Owner = true, @@ -2053,11 +2091,19 @@ public async Task UpdateMethodology_StatusUpdate() } }; + var versionWithRedirect = new MethodologyVersion + { + Methodology = new Methodology(), + }; + + var methodologyRedirect = new MethodologyRedirect + { + MethodologyVersion = versionWithRedirect, + Slug = "updated-methodology-title", + }; + var request = new MethodologyUpdateRequest { - LatestInternalReleaseNote = "Approved", - PublishingStrategy = MethodologyPublishingStrategy.Immediately, - Status = MethodologyApprovalStatus.Approved, Title = "Updated Methodology Title" }; @@ -2065,31 +2111,193 @@ public async Task UpdateMethodology_StatusUpdate() await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - await context.MethodologyVersions.AddAsync(methodologyVersion); + await context.MethodologyVersions.AddRangeAsync(methodologyVersion, versionWithRedirect); + await context.MethodologyRedirects.AddAsync(methodologyRedirect); await context.SaveChangesAsync(); } await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - var methodologyApprovalService = new Mock(); - methodologyApprovalService - .Setup(s => s.UpdateApprovalStatus(methodologyVersion.Id, request)) - .ReturnsAsync(methodologyVersion); + var methodologyVersionRepository = new Mock(Strict); + methodologyVersionRepository + .Setup(mock => mock.GetLatestPublishedVersionBySlug("updated-methodology-title")) + .ReturnsAsync((MethodologyVersion?)null); + + var service = SetupMethodologyService(context, + methodologyVersionRepository: methodologyVersionRepository.Object); + + var result = await service.UpdateMethodology(methodologyVersion.Id, request); + result.AssertBadRequest(SlugUsedByRedirect); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var notUpdatedMethodology = await context + .MethodologyVersions + .Include(m => m.Methodology) + .SingleAsync(m => m.Id == methodologyVersion.Id); + + Assert.Equal("Test publication", notUpdatedMethodology.Title); + Assert.Equal("Test publication", notUpdatedMethodology.Methodology.OwningPublicationTitle); + Assert.Null(notUpdatedMethodology.AlternativeTitle); + Assert.Equal("test-publication", notUpdatedMethodology.Slug); + Assert.Equal("test-publication", notUpdatedMethodology.Methodology.OwningPublicationSlug); + Assert.Null(notUpdatedMethodology.AlternativeSlug); + + Assert.False(notUpdatedMethodology.Updated.HasValue); + } + } + + [Fact] + public async Task UpdateMethodology_RedirectForNewSlugAlreadyExistsButForSameMethodology() + { + var publication = new Publication + { + Title = "Test publication", + Slug = "test-publication" + }; + + var versionWithRedirectId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = versionWithRedirectId, + OwningPublicationTitle = "Test publication", + OwningPublicationSlug = "test-publication", + Publications = ListOf(new PublicationMethodology + { + Owner = true, + Publication = publication + }) + }; + var versionWithRedirect = new MethodologyVersion + { + Id = versionWithRedirectId, + Methodology = methodology, + Status = MethodologyApprovalStatus.Approved, + }; + + var methodologyVersion = new MethodologyVersion + { + Id = Guid.NewGuid(), + Methodology = methodology, + Status = MethodologyApprovalStatus.Draft, + PreviousVersionId = versionWithRedirectId, + }; + + var methodologyRedirect = new MethodologyRedirect + { + MethodologyVersion = versionWithRedirect, + Slug = "updated-methodology-title", + }; + + var request = new MethodologyUpdateRequest + { + Title = "Updated Methodology Title", + Status = MethodologyApprovalStatus.Draft, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.MethodologyVersions.AddRangeAsync(methodologyVersion, versionWithRedirect); + await context.MethodologyRedirects.AddAsync(methodologyRedirect); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { var methodologyVersionRepository = new Mock(Strict); methodologyVersionRepository .Setup(mock => mock.GetLatestPublishedVersionBySlug("updated-methodology-title")) .ReturnsAsync((MethodologyVersion?)null); + var service = SetupMethodologyService(context, + methodologyVersionRepository: methodologyVersionRepository.Object); + + var result = await service.UpdateMethodology(methodologyVersion.Id, request); + var methodologyVersionViewModel = result.AssertRight(); + + Assert.Equal(methodologyVersion.Id, methodologyVersionViewModel.Id); + Assert.Equal("Updated Methodology Title", methodologyVersionViewModel.Title); + Assert.Equal("updated-methodology-title", methodologyVersionViewModel.Slug); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var updatedVersion = await context + .MethodologyVersions + .Include(m => m.Methodology) + .SingleAsync(m => m.Id == methodologyVersion.Id); + + Assert.Equal("Updated Methodology Title", updatedVersion.Title); + Assert.Equal("Updated Methodology Title", updatedVersion.AlternativeTitle); + Assert.Equal("Test publication", updatedVersion.Methodology.OwningPublicationTitle); + + Assert.Equal("updated-methodology-title", updatedVersion.Slug); + Assert.Equal("updated-methodology-title", updatedVersion.AlternativeSlug); + Assert.Equal("test-publication", updatedVersion.Methodology.OwningPublicationSlug); + + Assert.True(updatedVersion.Updated.HasValue); + } + } + + [Fact] + public async Task UpdateMethodology_StatusUpdate() + { + var publication = new Publication + { + Title = "Test publication", + Slug = "test-publication" + }; + + var methodologyVersion = new MethodologyVersion + { + Id = Guid.NewGuid(), + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, + Methodology = new Methodology + { + Id = Guid.NewGuid(), + OwningPublicationTitle = publication.Title, + OwningPublicationSlug = publication.Slug, + Publications = ListOf(new PublicationMethodology + { + Owner = true, + Publication = publication + }) + } + }; + + var request = new MethodologyUpdateRequest + { + Title = "Updated Methodology Title", + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.MethodologyVersions.AddAsync(methodologyVersion); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var methodologyVersionRepository = new Mock(Strict); + methodologyVersionRepository + .Setup(mock => mock.GetLatestPublishedVersionBySlug( + "updated-methodology-title")) + .ReturnsAsync((MethodologyVersion?)null); + var service = SetupMethodologyService( context, - methodologyApprovalService: methodologyApprovalService.Object, methodologyVersionRepository: methodologyVersionRepository.Object); await service.UpdateMethodology(methodologyVersion.Id, request); - // Verify that the call to update the approval status happened. - VerifyAllMocks(methodologyApprovalService); + VerifyAllMocks(methodologyVersionRepository); } await using (var context = InMemoryApplicationDbContext(contentDbContextId)) @@ -2104,7 +2312,8 @@ public async Task UpdateMethodology_StatusUpdate() Assert.Equal("Updated Methodology Title", updatedMethodology.Title); Assert.Equal("Updated Methodology Title", updatedMethodology.AlternativeTitle); Assert.Equal("updated-methodology-title", updatedMethodology.Slug); - Assert.Equal("updated-methodology-title", updatedMethodology.Methodology.Slug); + Assert.Equal("updated-methodology-title", updatedMethodology.AlternativeSlug); + Assert.True(updatedMethodology.Updated.HasValue); updatedMethodology.Updated.AssertUtcNow(); } } @@ -2218,8 +2427,8 @@ public async Task DeleteMethodologyVersion() Methodology = new Methodology { Id = methodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Publications = ListOf(new PublicationMethodology { MethodologyId = methodologyId, @@ -2288,8 +2497,8 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() var methodology = new Methodology { Id = methodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), @@ -2363,8 +2572,8 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() var methodology = new Methodology { Id = methodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), @@ -2376,8 +2585,8 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() var unrelatedMethodology = new Methodology { Id = unrelatedMethodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), @@ -2437,8 +2646,8 @@ public async Task GetMethodologyStatuses() var methodology = new Methodology { Id = methodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf( new MethodologyVersion { @@ -2458,8 +2667,8 @@ public async Task GetMethodologyStatuses() var unrelatedMethodology = new Methodology { Id = unrelatedMethodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), @@ -2561,8 +2770,8 @@ public async Task GetMethodologyStatuses_NoStatuses() var methodology = new Methodology { Id = methodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf( new MethodologyVersion { @@ -2580,8 +2789,8 @@ public async Task GetMethodologyStatuses_NoStatuses() var unrelatedMethodology = new Methodology { Id = unrelatedMethodologyId, - Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", + OwningPublicationSlug = "pupil-absence-statistics-methodology", Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), @@ -2900,7 +3109,7 @@ public async Task UserIsApproverOnOwningPublicationOldRelease_Included() var releases = _fixture .DefaultRelease() .WithApprovalStatuses(ListOf( - ReleaseApprovalStatus.Approved, + ReleaseApprovalStatus.Approved, ReleaseApprovalStatus.Draft)) .GenerateList(); @@ -2939,7 +3148,7 @@ public async Task UserIsApproverOnOwningPublicationOldRelease_Included() var result = await service.ListUsersMethodologyVersionsForApproval(); var methodologyVersionsForApproval = result.AssertRight(); - + // The user should have access to approve the Methodology if they have Approver permissions // on ANY of the Publication's Releases, not just the latest one. var methodologyForApproval = Assert.Single(methodologyVersionsForApproval); @@ -3187,6 +3396,7 @@ private static MethodologyService SetupMethodologyService( IMethodologyApprovalService? methodologyApprovalService = null, IMethodologyCacheService? methodologyCacheService = null, IUserService? userService = null) + { return new( persistenceHelper ?? new PersistenceHelper(contentDbContext), diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs index 74b0d7cf6ae..2c8de7d3ef0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServicePermissionTests.cs @@ -13,6 +13,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using Moq; using Xunit; @@ -564,7 +565,7 @@ private static PublicationService BuildPublicationService( ContentDbContext? context = null, IUserService? userService = null, IPublicationRepository? publicationRepository = null, - IMethodologyVersionRepository? methodologyVersionRepository = null, + IMethodologyService? methodologyService = null, IPublicationCacheService? publicationCacheService = null, IMethodologyCacheService? methodologyCacheService = null) { @@ -576,7 +577,7 @@ private static PublicationService BuildPublicationService( new PersistenceHelper(context), userService ?? AlwaysTrueUserService().Object, publicationRepository ?? Mock.Of(Strict), - methodologyVersionRepository ?? Mock.Of(Strict), + methodologyService ?? Mock.Of(Strict), publicationCacheService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict)); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs index 659f517b656..999da649c4f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/PublicationServiceTests.cs @@ -13,6 +13,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; using Microsoft.AspNetCore.Mvc; @@ -1022,13 +1023,13 @@ public async Task UpdatePublication_NotPublished() var newSupersededById = Guid.NewGuid(); await using (var context = InMemoryApplicationDbContext(contextId)) { - var methodologyVersionRepository = new Mock(Strict); + var methodologyService = new Mock(Strict); var publicationService = BuildPublicationService(context, - methodologyVersionRepository: methodologyVersionRepository.Object); + methodologyService: methodologyService.Object); - methodologyVersionRepository - .Setup(s => s.PublicationTitleChanged( + methodologyService + .Setup(s => s.PublicationTitleOrSlugChanged( publication.Id, publication.Slug, "New title", @@ -1047,7 +1048,7 @@ public async Task UpdatePublication_NotPublished() } ); - VerifyAllMocks(methodologyVersionRepository); + VerifyAllMocks(methodologyService); var viewModel = result.AssertRight(); @@ -1134,7 +1135,7 @@ public async Task UpdatePublication_AlreadyPublished() await using (var context = InMemoryApplicationDbContext(contextId)) { - var methodologyVersionRepository = new Mock(Strict); + var methodologyService = new Mock(Strict); var methodologyCacheService = new Mock(Strict); var publicationCacheService = new Mock(Strict); @@ -1152,13 +1153,13 @@ public async Task UpdatePublication_AlreadyPublished() new List())); var publicationService = BuildPublicationService(context, - methodologyVersionRepository: methodologyVersionRepository.Object, + methodologyService: methodologyService.Object, publicationCacheService: publicationCacheService.Object, methodologyCacheService: methodologyCacheService.Object); // Expect the title to change but not the slug, as the Publication is already published. - methodologyVersionRepository - .Setup(s => s.PublicationTitleChanged( + methodologyService + .Setup(s => s.PublicationTitleOrSlugChanged( publication.Id, publication.Slug, "New title", @@ -1177,7 +1178,7 @@ public async Task UpdatePublication_AlreadyPublished() } ); - VerifyAllMocks(methodologyVersionRepository, + VerifyAllMocks(methodologyService, methodologyCacheService, publicationCacheService); @@ -1218,6 +1219,119 @@ public async Task UpdatePublication_AlreadyPublished() } } + [Fact] + public async Task UpdatePublication_TitleChangeLeavesPubAndMethodologySlugsUnchanged() + { + var publication = new Publication + { + Slug = "old-title", + Title = "Old title", + Topic = new Topic + { + Theme = new Theme(), + }, + Contact = new Contact + { + ContactName = "Old name", + ContactTelNo = "0987654321", + TeamName = "Old team", + TeamEmail = "old.smith@test.com", + }, + LatestPublishedRelease = new Release(), + }; + + var methodologyVersionId = Guid.NewGuid(); + var publicationMethodology = new PublicationMethodology + { + Publication = publication, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = methodologyVersionId, + Versions = ListOf(new MethodologyVersion + { + Id = methodologyVersionId, + }), + OwningPublicationTitle = "Old title", + OwningPublicationSlug = "old-title", + } + }; + + var contextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contextId)) + { + context.PublicationMethodologies.AddRange(publicationMethodology); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var methodologyService = new Mock(Strict); + var methodologyCacheService = new Mock(Strict); + var publicationCacheService = new Mock(Strict); + + publicationCacheService.Setup(mock => mock.UpdatePublication(publication.Slug)) + .ReturnsAsync(new PublicationCacheViewModel()); + + publicationCacheService.Setup(mock => mock.UpdatePublicationTree()) + .ReturnsAsync(new List()); + + methodologyCacheService.Setup(mock => mock.UpdateSummariesTree()) + .ReturnsAsync(new Either>( + new List())); + + var publicationService = BuildPublicationService(context, + methodologyService: methodologyService.Object, + publicationCacheService: publicationCacheService.Object, + methodologyCacheService: methodologyCacheService.Object); + + methodologyService + .Setup(s => s.PublicationTitleOrSlugChanged( + publication.Id, + publication.Slug, + "New title", + publication.Slug)) // methodology slug isn't changing + .Returns(Task.CompletedTask); + + var result = await publicationService.UpdatePublication( + publication.Id, + new PublicationSaveRequest + { + TopicId = publication.TopicId, + Title = "New title", + Summary = "New summary", + } + ); + + VerifyAllMocks(methodologyService, + methodologyCacheService, + publicationCacheService); + + var viewModel = result.AssertRight(); + + Assert.Equal("New title", viewModel.Title); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var updatedPublication = await context.Publications + .Include(p => p.Contact) + .Include(p => p.Topic) + .SingleAsync(p => p.Title == "New title"); + + Assert.True(updatedPublication.Live); + Assert.True(updatedPublication.Updated.HasValue); + Assert.InRange(DateTime.UtcNow.Subtract(updatedPublication.Updated!.Value).Milliseconds, 0, 1500); + + // Slug remains unchanged + Assert.Equal("old-title", updatedPublication.Slug); + Assert.Equal("New title", updatedPublication.Title); + + // We don't check whether methodology titles/slugs have changed, because this is done by + // PublicationTitleOrSlugChanged, which has been mocked. + } + } + [Fact] public async void UpdatePublication_NoTitleOrSupersededByChange() { @@ -1231,7 +1345,7 @@ public async void UpdatePublication_NoTitleOrSupersededByChange() { Title = "Old title", Summary = "Old summary", - Slug = "old-slug", + Slug = "old-title", Topic = new Topic { Title = "Old topic" @@ -1258,13 +1372,12 @@ public async void UpdatePublication_NoTitleOrSupersededByChange() await using (var context = InMemoryApplicationDbContext(contextId)) { - // Expect no calls to be made on this Mock as the Publication's Title hasn't changed. - var methodologyVersionRepository = new Mock(Strict); + // Expect no calls to be made on this Mock as the Publication's Title and Slug haven't changed. + var methodologyService = new Mock(Strict); var publicationService = BuildPublicationService(context, - methodologyVersionRepository: methodologyVersionRepository.Object); + methodologyService: methodologyService.Object); - // Service method under test var result = await publicationService.UpdatePublication( publication.Id, new PublicationSaveRequest @@ -1276,7 +1389,7 @@ public async void UpdatePublication_NoTitleOrSupersededByChange() } ); - VerifyAllMocks(methodologyVersionRepository); + VerifyAllMocks(methodologyService); var viewModel = result.AssertRight(); @@ -2444,7 +2557,7 @@ private static PublicationService BuildPublicationService( ContentDbContext context, IUserService? userService = null, IPublicationRepository? publicationRepository = null, - IMethodologyVersionRepository? methodologyVersionRepository = null, + IMethodologyService? methodologyService = null, IPublicationCacheService? publicationCacheService = null, IMethodologyCacheService? methodologyCacheService = null) { @@ -2454,7 +2567,7 @@ private static PublicationService BuildPublicationService( new PersistenceHelper(context), userService ?? AlwaysTrueUserService().Object, publicationRepository ?? new PublicationRepository(context), - methodologyVersionRepository ?? Mock.Of(Strict), + methodologyService ?? Mock.Of(Strict), publicationCacheService ?? Mock.Of(Strict), methodologyCacheService ?? Mock.Of(Strict)); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseApprovalServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseApprovalServicePermissionTests.cs index abf879172d8..d65603b9dc8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseApprovalServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseApprovalServicePermissionTests.cs @@ -56,7 +56,6 @@ await PolicyCheckBuilder() public async Task UpdateReleaseStatus_Draft() { await PolicyCheckBuilder() - .SetupResourceCheck(_release, CanUpdateSpecificRelease) .SetupResourceCheckToFail(_release, CanMarkSpecificReleaseAsDraft) .AssertForbidden( userService => @@ -77,7 +76,6 @@ await PolicyCheckBuilder() public async Task UpdateReleaseStatus_HigherLevelReview() { await PolicyCheckBuilder() - .SetupResourceCheck(_release, CanUpdateSpecificRelease) .SetupResourceCheckToFail(_release, CanSubmitSpecificReleaseToHigherReview) .AssertForbidden( userService => @@ -98,7 +96,6 @@ await PolicyCheckBuilder() public async Task UpdateReleaseStatus_Approve() { await PolicyCheckBuilder() - .SetupResourceCheck(_release, CanUpdateSpecificRelease) .SetupResourceCheckToFail(_release, CanApproveSpecificRelease) .AssertForbidden( userService => @@ -129,7 +126,7 @@ private ReleaseApprovalService BuildService(IUserService userService) Mock.Of(), Mock.Of(), Mock.Of(), - Options.Create(new ReleaseApprovalOptions()), + Options.Create(new ReleaseApprovalOptions()), Mock.Of(), Mock.Of() ); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Bau/MethodologyMigrationController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Bau/MethodologyMigrationController.cs index ad9e970b066..d9e6f123403 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Bau/MethodologyMigrationController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Bau/MethodologyMigrationController.cs @@ -11,10 +11,6 @@ using Microsoft.EntityFrameworkCore; using static GovUk.Education.ExploreEducationStatistics.Admin.Models.GlobalRoles; -// !!!!! -// TODO: Move `MethodologyVersionRepository#IsToBePublished` `MethodologyApprovalService` and change to be private after removing this controller -// !!!!! - namespace GovUk.Education.ExploreEducationStatistics.Admin.Controllers.Api.Bau { [Route("api")] @@ -42,6 +38,7 @@ public class MethodologyMigrationResult public Guid? LatestMethodologyVersionId { get; set; } } + // TODO: Remove EES-4627 [HttpPatch("migration/set-methodology-latest-published-version-ids")] public async Task>> MigrateMethodologyLatestPublishedVersionIds( [FromQuery] bool dryRun = true) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.Designer.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.Designer.cs new file mode 100644 index 00000000000..1af59008c8d --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.Designer.cs @@ -0,0 +1,2053 @@ +// +using System; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + [DbContext(typeof(ContentDbContext))] + [Migration("20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects")] + partial class EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.21") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContentBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("LegacyCreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Resolved") + .HasColumnType("datetime2"); + + b.Property("ResolvedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContentBlockId"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ResolvedById"); + + b.ToTable("Comment"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ContactTelNo") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TeamEmail") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TeamName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentSectionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Locked") + .HasColumnType("datetime2"); + + b.Property("LockedById") + .IsConcurrencyToken() + .HasColumnType("uniqueidentifier"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContentSectionId"); + + b.HasIndex("LockedById"); + + b.HasIndex("Type"); + + b.ToTable("ContentBlock", (string)null); + + b.HasDiscriminator("Type").HasValue("ContentBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Caption") + .HasColumnType("nvarchar(max)"); + + b.Property("Heading") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("Id"); + + b.HasIndex("Type"); + + b.ToTable("ContentSections"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("ExpectedImportedRows") + .HasColumnType("int"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("GeographicLevels") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ImportedRows") + .HasColumnType("int"); + + b.Property("LastProcessedRowIndex") + .HasColumnType("int"); + + b.Property("MetaFileId") + .HasColumnType("uniqueidentifier"); + + b.Property("StagePercentageComplete") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("ZipFileId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FileId") + .IsUnique(); + + b.HasIndex("MetaFileId") + .IsUnique(); + + b.HasIndex("ZipFileId") + .IsUnique() + .HasFilter("[ZipFileId] IS NOT NULL"); + + b.ToTable("DataImports"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImportError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("DataImportId") + .HasColumnType("uniqueidentifier"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("DataImportId"); + + b.ToTable("DataImportErrors"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("EmbedBlocks"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DataBlockId") + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DataBlockId") + .IsUnique(); + + b.HasIndex("ReleaseId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("FeaturedTables"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentLength") + .HasColumnType("bigint"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Filename") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReplacedById") + .HasColumnType("uniqueidentifier"); + + b.Property("ReplacingId") + .HasColumnType("uniqueidentifier"); + + b.Property("RootPath") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceId") + .HasColumnType("uniqueidentifier"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReplacedById") + .IsUnique() + .HasFilter("[ReplacedById] IS NOT NULL"); + + b.HasIndex("ReplacingId") + .IsUnique() + .HasFilter("[ReplacingId] IS NOT NULL"); + + b.HasIndex("SourceId"); + + b.HasIndex("Type"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FreeTextRank", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("Rank") + .HasColumnType("int"); + + b.ToTable((string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.GlossaryEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Body") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.ToTable("GlossaryEntries"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("GuidanceText") + .HasColumnType("nvarchar(max)"); + + b.Property("GuidanceTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Trend") + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("KeyStatistics"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.LegacyRelease", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PublicationId"); + + b.ToTable("LegacyReleases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("LatestPublishedVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("OwningPublicationSlug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OwningPublicationTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("LatestPublishedVersionId") + .IsUnique() + .HasFilter("[LatestPublishedVersionId] IS NOT NULL"); + + b.ToTable("Methodologies"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("MethodologyVersionId"); + + b.ToTable("MethodologyFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DisplayDate") + .HasColumnType("datetime2"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("UpdatedById") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyVersionId"); + + b.HasIndex("UpdatedById"); + + b.ToTable("MethodologyNotes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("MethodologyVersionId", "Slug"); + + b.ToTable("MethodologyRedirects"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("InternalReleaseNote") + .HasColumnType("nvarchar(max)"); + + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyVersionId"); + + b.ToTable("MethodologyStatus"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AlternativeSlug") + .HasColumnType("nvarchar(max)"); + + b.Property("AlternativeTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("InternalReleaseNote") + .HasColumnType("nvarchar(max)"); + + b.Property("MethodologyId") + .HasColumnType("uniqueidentifier"); + + b.Property("PreviousVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("PublishingStrategy") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduledWithReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("MethodologyId"); + + b.HasIndex("PreviousVersionId"); + + b.HasIndex("ScheduledWithReleaseId"); + + b.ToTable("MethodologyVersions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", b => + { + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Annexes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("MethodologyVersionId"); + + b.ToTable("MethodologyVersions", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Permalink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("DataSetTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MigratedFromLegacy") + .HasColumnType("bit"); + + b.Property("PublicationTitle") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("SubjectId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("ReleaseId"); + + b.HasIndex("SubjectId"); + + b.ToTable("Permalinks"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContactId") + .HasColumnType("uniqueidentifier"); + + b.Property("LatestPublishedReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("nvarchar(160)"); + + b.Property("SupersededById") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TopicId") + .HasColumnType("uniqueidentifier"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.HasIndex("LatestPublishedReleaseId") + .IsUnique() + .HasFilter("[LatestPublishedReleaseId] IS NOT NULL"); + + b.HasIndex("SupersededById"); + + b.HasIndex("TopicId"); + + b.ToTable("Publications"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationMethodology", b => + { + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("MethodologyId") + .HasColumnType("uniqueidentifier"); + + b.Property("Owner") + .HasColumnType("bit"); + + b.HasKey("PublicationId", "MethodologyId"); + + b.HasIndex("MethodologyId"); + + b.ToTable("PublicationMethodologies"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("DataGuidance") + .HasColumnType("nvarchar(max)"); + + b.Property("NextReleaseDate") + .HasColumnType("nvarchar(max)"); + + b.Property("NotifiedOn") + .HasColumnType("datetime2"); + + b.Property("NotifySubscribers") + .HasColumnType("bit"); + + b.Property("PreReleaseAccessList") + .HasColumnType("nvarchar(max)"); + + b.Property("PreviousVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublishScheduled") + .HasColumnType("datetime2"); + + b.Property("Published") + .HasColumnType("datetime2"); + + b.Property("RelatedInformation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseName") + .HasColumnType("nvarchar(max)"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("TimePeriodCoverage") + .IsRequired() + .HasMaxLength(6) + .HasColumnType("nvarchar(6)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UpdatePublishedDate") + .HasColumnType("bit"); + + b.Property("Version") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("PublicationId"); + + b.HasIndex("Type"); + + b.HasIndex("PreviousVersionId", "Version"); + + b.ToTable("Releases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseContentBlock", b => + { + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContentBlockId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("ReleaseId", "ContentBlockId"); + + b.HasIndex("ContentBlockId"); + + b.ToTable("ReleaseContentBlocks"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseContentSection", b => + { + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("ContentSectionId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("ReleaseId", "ContentSectionId"); + + b.HasIndex("ContentSectionId") + .IsUnique(); + + b.ToTable("ReleaseContentSections"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseFile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("FileId") + .HasColumnType("uniqueidentifier"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("Order") + .HasColumnType("int"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FileId"); + + b.HasIndex("ReleaseId"); + + b.ToTable("ReleaseFiles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ApprovalStatus") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("InternalReleaseNote") + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseId"); + + b.ToTable("ReleaseStatus"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("Summary") + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(max)"); + + b.Property("ThemeId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ThemeId"); + + b.ToTable("Topics"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Update", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("On") + .HasColumnType("datetime2"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseId"); + + b.ToTable("Update"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("PublicationId"); + + b.ToTable("UserPublicationInvites"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("datetime2"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("PublicationId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPublicationRoles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EmailSent") + .HasColumnType("bit"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("Updated") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("ReleaseId"); + + b.ToTable("UserReleaseInvites"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.Property("CreatedById") + .HasColumnType("uniqueidentifier"); + + b.Property("Deleted") + .HasColumnType("datetime2"); + + b.Property("DeletedById") + .HasColumnType("uniqueidentifier"); + + b.Property("ReleaseId") + .HasColumnType("uniqueidentifier"); + + b.Property("Role") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SoftDeleted") + .HasColumnType("bit"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("ReleaseId"); + + b.HasIndex("UserId"); + + b.ToTable("UserReleaseRoles"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Charts") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Charts"); + + b.Property("Heading") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Heading"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Query") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Query"); + + b.Property("Source") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Table") + .IsRequired() + .HasColumnType("nvarchar(max)") + .HasColumnName("DataBlock_Table"); + + b.HasDiscriminator().HasValue("DataBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("EmbedBlockId") + .HasColumnType("uniqueidentifier") + .HasColumnName("EmbedBlockId"); + + b.HasIndex("EmbedBlockId") + .IsUnique() + .HasFilter("[EmbedBlockId] IS NOT NULL"); + + b.HasDiscriminator().HasValue("EmbedBlockLink"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.HtmlBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Body") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("nvarchar(max)") + .HasColumnName("Body"); + + b.HasDiscriminator().HasValue("HtmlBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic"); + + b.Property("DataBlockId") + .HasColumnType("uniqueidentifier"); + + b.HasIndex("DataBlockId"); + + b.ToTable("KeyStatisticsDataBlock", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic"); + + b.Property("Statistic") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.ToTable("KeyStatisticsText", (string)null); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MarkDownBlock", b => + { + b.HasBaseType("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock"); + + b.Property("Body") + .IsRequired() + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("nvarchar(max)") + .HasColumnName("Body"); + + b.HasDiscriminator().HasValue("MarkDownBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Comment", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", "ContentBlock") + .WithMany("Comments") + .HasForeignKey("ContentBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "ResolvedBy") + .WithMany() + .HasForeignKey("ResolvedById"); + + b.Navigation("ContentBlock"); + + b.Navigation("CreatedBy"); + + b.Navigation("ResolvedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", "ContentSection") + .WithMany("Content") + .HasForeignKey("ContentSectionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "LockedBy") + .WithMany() + .HasForeignKey("LockedById"); + + b.Navigation("ContentSection"); + + b.Navigation("LockedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "FileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "MetaFile") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "MetaFileId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ZipFile") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "ZipFileId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("File"); + + b.Navigation("MetaFile"); + + b.Navigation("ZipFile"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImportError", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", "DataImport") + .WithMany("Errors") + .HasForeignKey("DataImportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DataImport"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", "DataBlock") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.FeaturedTable", "DataBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("FeaturedTables") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("DataBlock"); + + b.Navigation("Release"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.File", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacedBy") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "Replacing") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "ReplacingId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "Source") + .WithMany() + .HasForeignKey("SourceId"); + + b.Navigation("CreatedBy"); + + b.Navigation("ReplacedBy"); + + b.Navigation("Replacing"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.GlossaryEntry", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("KeyStatistics") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById"); + + b.Navigation("CreatedBy"); + + b.Navigation("Release"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.LegacyRelease", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("LegacyReleases") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "LatestPublishedVersion") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "LatestPublishedVersionId"); + + b.Navigation("LatestPublishedVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyFile", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyNote", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany("Notes") + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "UpdatedBy") + .WithMany() + .HasForeignKey("UpdatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("CreatedBy"); + + b.Navigation("MethodologyVersion"); + + b.Navigation("UpdatedBy"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("MethodologyVersion"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "Methodology") + .WithMany("Versions") + .HasForeignKey("MethodologyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "PreviousVersion") + .WithMany() + .HasForeignKey("PreviousVersionId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "ScheduledWithRelease") + .WithMany() + .HasForeignKey("ScheduledWithReleaseId"); + + b.Navigation("CreatedBy"); + + b.Navigation("Methodology"); + + b.Navigation("PreviousVersion"); + + b.Navigation("ScheduledWithRelease"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", null) + .WithOne("MethodologyContent") + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersionContent", "MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "LatestPublishedRelease") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "LatestPublishedReleaseId"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "SupersededBy") + .WithMany() + .HasForeignKey("SupersededById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", "Topic") + .WithMany("Publications") + .HasForeignKey("TopicId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ExternalMethodology", "ExternalMethodology", b1 => + { + b1.Property("PublicationId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Title") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.Property("Url") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b1.HasKey("PublicationId"); + + b1.ToTable("ExternalMethodology", (string)null); + + b1.WithOwner() + .HasForeignKey("PublicationId"); + }); + + b.Navigation("Contact"); + + b.Navigation("ExternalMethodology"); + + b.Navigation("LatestPublishedRelease"); + + b.Navigation("SupersededBy"); + + b.Navigation("Topic"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationMethodology", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", "Methodology") + .WithMany("Publications") + .HasForeignKey("MethodologyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("Methodologies") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Methodology"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "PreviousVersion") + .WithMany() + .HasForeignKey("PreviousVersionId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany("Releases") + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("PreviousVersion"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseContentBlock", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", "ContentBlock") + .WithMany() + .HasForeignKey("ContentBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("ContentBlocks") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ContentBlock"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseContentSection", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", "ContentSection") + .WithOne("Release") + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseContentSection", "ContentSectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("Content") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ContentSection"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseFile", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.File", "File") + .WithMany() + .HasForeignKey("FileId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany() + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("File"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseStatus", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("ReleaseStatuses") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", "Theme") + .WithMany("Topics") + .HasForeignKey("ThemeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Theme"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Update", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany("Updates") + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationInvite", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany() + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Publication"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserPublicationRole", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", "Publication") + .WithMany() + .HasForeignKey("PublicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Publication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseInvite", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany() + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.UserReleaseRole", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "DeletedBy") + .WithMany() + .HasForeignKey("DeletedById"); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", "Release") + .WithMany() + .HasForeignKey("ReleaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("Release"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlock", "EmbedBlock") + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.EmbedBlockLink", "EmbedBlockId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmbedBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.DataBlock", "DataBlock") + .WithMany() + .HasForeignKey("DataBlockId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", null) + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticDataBlock", "Id") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + + b.Navigation("DataBlock"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatistic", null) + .WithOne() + .HasForeignKey("GovUk.Education.ExploreEducationStatistics.Content.Model.KeyStatisticText", "Id") + .OnDelete(DeleteBehavior.ClientCascade) + .IsRequired(); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentBlock", b => + { + b.Navigation("Comments"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.ContentSection", b => + { + b.Navigation("Content"); + + b.Navigation("Release"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.DataImport", b => + { + b.Navigation("Errors"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Methodology", b => + { + b.Navigation("Publications"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", b => + { + b.Navigation("MethodologyContent") + .IsRequired(); + + b.Navigation("Notes"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Publication", b => + { + b.Navigation("LegacyReleases"); + + b.Navigation("Methodologies"); + + b.Navigation("Releases"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Release", b => + { + b.Navigation("Content"); + + b.Navigation("ContentBlocks"); + + b.Navigation("FeaturedTables"); + + b.Navigation("KeyStatistics"); + + b.Navigation("ReleaseStatuses"); + + b.Navigation("Updates"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Theme", b => + { + b.Navigation("Topics"); + }); + + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.Topic", b => + { + b.Navigation("Publications"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.cs new file mode 100644 index 00000000000..97fe8c11ba8 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects.cs @@ -0,0 +1,69 @@ +using System; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Migrations.ContentMigrations +{ + public partial class EES4482_CreateAlternativeSlugAndOwningPublicationSlugForMethodologiesAndRedirects : Migration + { + private const string MigrationId = "20230912081830"; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AlternativeSlug", + table: "MethodologyVersions", + type: "nvarchar(max)", + nullable: true); + + migrationBuilder.AddColumn( + name: "OwningPublicationSlug", + table: "Methodologies", + type: "nvarchar(max)", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateTable( + name: "MethodologyRedirects", + columns: table => new + { + Slug = table.Column(type: "nvarchar(450)", nullable: false), + MethodologyVersionId = table.Column(type: "uniqueidentifier", nullable: false), + Created = table.Column(type: "datetime2", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MethodologyRedirects", x => new { x.MethodologyVersionId, x.Slug }); + table.ForeignKey( + name: "FK_MethodologyRedirects_MethodologyVersions_MethodologyVersionId", + column: x => x.MethodologyVersionId, + principalTable: "MethodologyVersions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.Sql("GRANT SELECT ON dbo.MethodologyRedirects TO [content]"); + migrationBuilder.Sql("GRANT SELECT ON dbo.MethodologyRedirects TO [publisher]"); + + migrationBuilder.SqlFromFile( + MigrationConstants.ContentMigrationsPath, + $"{MigrationId}_EES4482_MigrateMethodologyOwningPublicationSlug.sql"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "MethodologyRedirects"); + + migrationBuilder.DropColumn( + name: "AlternativeSlug", + table: "MethodologyVersions"); + + migrationBuilder.DropColumn( + name: "OwningPublicationSlug", + table: "Methodologies"); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_MigrateMethodologyOwningPublicationSlug.sql b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_MigrateMethodologyOwningPublicationSlug.sql new file mode 100644 index 00000000000..62b53e4a58d --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/20230912081830_EES4482_MigrateMethodologyOwningPublicationSlug.sql @@ -0,0 +1,16 @@ +-- Update OwningPublicationSlug so it's aligned with the owning publication's Slug +UPDATE [dbo].[Methodologies] +SET OwningPublicationSlug = P.Slug +FROM [dbo].[MethodologyVersions] MV + JOIN [dbo].[Methodologies] M ON MV.MethodologyId = M.Id + JOIN [dbo].[PublicationMethodologies] PM ON PM.MethodologyId = M.Id + JOIN [dbo].[Publications] P ON P.Id = PM.PublicationId +WHERE PM.Owner = 1; + +-- If Methodology.Slug is different to the owning publication's slug, +-- it is an AlternativeSlug +UPDATE [dbo].[MethodologyVersions] +SET AlternativeSlug = M.Slug +FROM [dbo].[MethodologyVersions] MV + JOIN [dbo].[Methodologies] M ON MV.MethodologyId = M.Id +WHERE M.Slug != M.OwningPublicationSlug; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs index 6890dd0bc73..17ab500a9ac 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Migrations/ContentMigrations/ContentDbContextModelSnapshot.cs @@ -511,6 +511,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LatestPublishedVersionId") .HasColumnType("uniqueidentifier"); + b.Property("OwningPublicationSlug") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("OwningPublicationTitle") .IsRequired() .HasColumnType("nvarchar(max)"); @@ -588,6 +592,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("MethodologyNotes"); }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.Property("MethodologyVersionId") + .HasColumnType("uniqueidentifier"); + + b.Property("Slug") + .HasColumnType("nvarchar(450)"); + + b.Property("Created") + .HasColumnType("datetime2"); + + b.HasKey("MethodologyVersionId", "Slug"); + + b.ToTable("MethodologyRedirects"); + }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => { b.Property("Id") @@ -625,6 +645,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); + b.Property("AlternativeSlug") + .HasColumnType("nvarchar(max)"); + b.Property("AlternativeTitle") .HasColumnType("nvarchar(max)"); @@ -1545,6 +1568,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("UpdatedBy"); }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyRedirect", b => + { + b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyVersion", "MethodologyVersion") + .WithMany() + .HasForeignKey("MethodologyVersionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MethodologyVersion"); + }); + modelBuilder.Entity("GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyStatus", b => { b.HasOne("GovUk.Education.ExploreEducationStatistics.Content.Model.User", "CreatedBy") diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandler.cs index 734556a3466..2324a6d74b8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AdoptMethodologyForSpecificPublicationAuthorizationHandler.cs @@ -14,12 +14,12 @@ public class AdoptMethodologyForSpecificPublicationRequirement : IAuthorizationR public class AdoptMethodologyForSpecificPublicationAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public AdoptMethodologyForSpecificPublicationAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -33,7 +33,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs index c1c48870c69..f301e0574ae 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs @@ -2,7 +2,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; @@ -15,12 +15,12 @@ public class AssignPrereleaseContactsToSpecificReleaseRequirement : IAuthorizati public class AssignPrereleaseContactsToSpecificReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public AssignPrereleaseContactsToSpecificReleaseAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -34,7 +34,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerResourceRoleService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerService.cs similarity index 54% rename from src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerResourceRoleService.cs rename to src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerService.cs index 8e1840b2662..89c2510a131 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerResourceRoleService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AuthorizationHandlerService.cs @@ -1,42 +1,54 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.Models; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Common.Services; +using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Extensions; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; -public class AuthorizationHandlerResourceRoleService +public class AuthorizationHandlerService { private static readonly ReleaseRole[] ReleaseEditorRoles = { - ReleaseRole.Contributor, + ReleaseRole.Contributor, ReleaseRole.Lead }; - + public static readonly ReleaseRole[] UnrestrictedReleaseViewerRoles = { - ReleaseRole.Viewer, + ReleaseRole.Viewer, ReleaseRole.Contributor, - ReleaseRole.Approver, + ReleaseRole.Approver, ReleaseRole.Lead }; - public static readonly List ReleaseEditorAndApproverRoles = + public static readonly List ReleaseEditorAndApproverRoles = ReleaseEditorRoles .Append(ReleaseRole.Approver) .ToList(); - + + private readonly ContentDbContext _contentDbContext; private readonly IUserReleaseRoleRepository _userReleaseRoleRepository; private readonly IUserPublicationRoleRepository _userPublicationRoleRepository; + private readonly IPreReleaseService _preReleaseService; - public AuthorizationHandlerResourceRoleService( - IUserReleaseRoleRepository userReleaseRoleRepository, - IUserPublicationRoleRepository userPublicationRoleRepository) + public AuthorizationHandlerService( + ContentDbContext contentDbContext, + IUserReleaseRoleRepository userReleaseRoleRepository, + IUserPublicationRoleRepository userPublicationRoleRepository, + IPreReleaseService preReleaseService) { + _contentDbContext = contentDbContext; _userReleaseRoleRepository = userReleaseRoleRepository; _userPublicationRoleRepository = userPublicationRoleRepository; + _preReleaseService = preReleaseService; } public Task HasRolesOnPublicationOrRelease( @@ -47,13 +59,13 @@ public Task HasRolesOnPublicationOrRelease( IEnumerable releaseRoles) { return HasRolesOnPublicationOrRelease( - userId, - publicationId, - () => Task.FromResult((Guid?) releaseId), - publicationRoles, + userId, + publicationId, + () => Task.FromResult((Guid?) releaseId), + publicationRoles, releaseRoles); } - + public async Task HasRolesOnPublicationOrRelease( Guid userId, Guid publicationId, @@ -63,7 +75,7 @@ public async Task HasRolesOnPublicationOrRelease( { var usersPublicationRoles = await _userPublicationRoleRepository .GetAllRolesByUserAndPublication(userId, publicationId); - + if (usersPublicationRoles.Any(publicationRoles.Contains)) { return true; @@ -75,13 +87,13 @@ public async Task HasRolesOnPublicationOrRelease( { return false; } - + var usersReleaseRoles = await _userReleaseRoleRepository .GetAllRolesByUserAndRelease(userId, releaseId.Value); return usersReleaseRoles.Any(releaseRoles.Contains); } - + public async Task HasRolesOnPublicationOrAnyRelease( Guid userId, Guid publicationId, @@ -90,7 +102,7 @@ public async Task HasRolesOnPublicationOrAnyRelease( { var usersPublicationRoles = await _userPublicationRoleRepository .GetAllRolesByUserAndPublication(userId, publicationId); - + if (usersPublicationRoles.Any(publicationRoles.Contains)) { return true; @@ -101,7 +113,7 @@ public async Task HasRolesOnPublicationOrAnyRelease( return usersReleaseRoles.Any(releaseRoles.Contains); } - + public async Task HasRolesOnPublication( Guid userId, Guid publicationId, @@ -112,7 +124,7 @@ public async Task HasRolesOnPublication( return usersPublicationRoles.Any(publicationRoles.Contains); } - + public async Task HasRolesOnRelease( Guid userId, Guid releaseId, @@ -123,4 +135,58 @@ public async Task HasRolesOnRelease( return usersReleaseRoles.Any(releaseRoles.Contains); } + + public async Task IsReleaseViewableByUser(Release release, ClaimsPrincipal user) + { + // If the user has the "AccessAllReleases" Claim, they can see any Release. + if (SecurityUtils.HasClaim(user, SecurityClaimTypes.AccessAllReleases)) + { + return true; + } + + // If the user has any PublicationRoles on the owning Publication, they can see its child Releases. + if (await HasRolesOnPublication( + user.GetUserId(), + release.PublicationId, + EnumUtil.GetEnumValuesAsArray())) + { + return true; + } + + // If the user has any non-Pre-release Viewer roles on the Release, they can see it at any time. + if (await HasRolesOnRelease( + user.GetUserId(), + release.Id, + UnrestrictedReleaseViewerRoles)) + { + return true; + } + + // If the user has the Pre-release Viewer role on this Release and the Release is within its open + // Pre-release window, they can see the Release. + if (await HasRolesOnRelease( + user.GetUserId(), + release.Id, + ReleaseRole.PrereleaseViewer)) + { + var windowStatus = _preReleaseService.GetPreReleaseWindowStatus(release, DateTime.UtcNow); + if (windowStatus.Access == PreReleaseAccess.Within) + { + return true; + } + } + + await _contentDbContext + .Entry(release) + .Reference(r => r.Publication) + .LoadAsync(); + + await _contentDbContext + .Entry(release.Publication) + .Collection(p => p.Releases) + .LoadAsync(); + + // If the Release is public, anyone can see it. + return release.IsLatestPublishedVersionOfRelease(); + } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CompoundAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CompoundAuthorizationHandler.cs deleted file mode 100644 index f10e703041a..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CompoundAuthorizationHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; - -namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers -{ - public class CompoundAuthorizationHandler : AuthorizationHandler - where TRequirement : IAuthorizationRequirement - where TEntity : class - { - private readonly IAuthorizationHandler[] _handlers; - - protected CompoundAuthorizationHandler(params IAuthorizationHandler[] handlers) - { - _handlers = handlers; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TEntity resource) - { - foreach (var handler in _handlers) - { - await handler.HandleAsync(context); - - if (context.HasSucceeded) - { - return; - } - } - } - } -} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlers.cs index f70fbe1734a..069fa8776c4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateMethodologyForSpecificPublicationAuthorizationHandlers.cs @@ -18,14 +18,14 @@ public class CreateMethodologyForSpecificPublicationAuthorizationHandler : AuthorizationHandler { private readonly ContentDbContext _context; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public CreateMethodologyForSpecificPublicationAuthorizationHandler( ContentDbContext context, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _context = context; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -40,8 +40,9 @@ protected override async Task HandleRequirementAsync( } // If a publication owns a methodology already, they cannot own another - if (await _context.PublicationMethodologies - .AnyAsync(pm => pm.PublicationId == publication.Id && pm.Owner)) + if (await _context + .PublicationMethodologies + .AnyAsync(pm => pm.PublicationId == publication.Id && pm.Owner)) { return; } @@ -52,7 +53,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreatePublicationForSpecificTopicAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreatePublicationForSpecificTopicAuthorizationHandlers.cs index 5ec93cddc56..cfaf5dae6c5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreatePublicationForSpecificTopicAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreatePublicationForSpecificTopicAuthorizationHandlers.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Microsoft.AspNetCore.Authorization; @@ -5,18 +6,19 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.Authorizatio { public class CreatePublicationForSpecificTopicRequirement : IAuthorizationRequirement {} - - public class CreatePublicationForSpecificTopicAuthorizationHandler : CompoundAuthorizationHandler< - CreatePublicationForSpecificTopicRequirement, Topic> + + public class CreatePublicationForSpecificTopicAuthorizationHandler : + AuthorizationHandler { - public CreatePublicationForSpecificTopicAuthorizationHandler() - : base(new CanCreateForAnyTopicAuthorizationHandler()) {} - - public class CanCreateForAnyTopicAuthorizationHandler : HasClaimAuthorizationHandler< - CreatePublicationForSpecificTopicRequirement> + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + CreatePublicationForSpecificTopicRequirement requirement, Topic resource) { - public CanCreateForAnyTopicAuthorizationHandler() - : base(SecurityClaimTypes.CreateAnyPublication) {} + if (SecurityUtils.HasClaim(context.User, SecurityClaimTypes.CreateAnyPublication)) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlers.cs index 9566d872b70..8461345c5ef 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/CreateReleaseForSpecificPublicationAuthorizationHandlers.cs @@ -15,12 +15,12 @@ public class CreateReleaseForSpecificPublicationRequirement : IAuthorizationRequ public class CreateReleaseForSpecificPublicationAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public CreateReleaseForSpecificPublicationAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -39,7 +39,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DelegatingAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DelegatingAuthorizationHandler.cs deleted file mode 100644 index 3afae0260d4..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DelegatingAuthorizationHandler.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using static System.Activator; - -namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers -{ - public class DelegatingAuthorizationHandler : AuthorizationHandler - where TRequirement : IAuthorizationRequirement - where TDelegateRequirement : IAuthorizationRequirement - where TEntity : class - where TDelegateEntity : class - { - private readonly AuthorizationHandler _delegateHandler; - private readonly Func _delegateResourceFn; - - public DelegatingAuthorizationHandler( - AuthorizationHandler delegateHandler, - Func delegateResourceFn) - { - _delegateHandler = delegateHandler; - _delegateResourceFn = delegateResourceFn; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TEntity resource) - { - IAuthorizationRequirement delegateRequirement = CreateInstance(); - var delegateContext = new AuthorizationHandlerContext(new[] {delegateRequirement}, context.User, _delegateResourceFn.Invoke(resource)); - await _delegateHandler.HandleAsync(delegateContext); - if (delegateContext.HasSucceeded) - { - context.Succeed(requirement); - } - } - } -} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandler.cs index a8ca6cd047e..d08e02fb315 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandler.cs @@ -1,7 +1,6 @@ #nullable enable using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; @@ -17,16 +16,13 @@ public class DeleteSpecificCommentAuthorizationHandler : AuthorizationHandler { private readonly ContentDbContext _contentDbContext; - private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public DeleteSpecificCommentAuthorizationHandler(ContentDbContext contentDbContext, - IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _contentDbContext = contentDbContext; - _releasePublishingStatusRepository = releasePublishingStatusRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -37,8 +33,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var updateSpecificReleaseContext = new AuthorizationHandlerContext( new[] {new UpdateSpecificReleaseRequirement()}, context.User, release); await new UpdateSpecificReleaseAuthorizationHandler( - _releasePublishingStatusRepository, - _authorizationHandlerResourceRoleService) + _authorizationHandlerService) .HandleAsync(updateSpecificReleaseContext); if (updateSpecificReleaseContext.HasSucceeded) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlers.cs index a0230654542..84436217b86 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificMethodologyAuthorizationHandlers.cs @@ -18,14 +18,14 @@ public class DeleteSpecificMethodologyAuthorizationHandler : AuthorizationHandler { private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public DeleteSpecificMethodologyAuthorizationHandler( IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -47,7 +47,7 @@ protected override async Task HandleRequirementAsync( var owningPublication = await _methodologyRepository.GetOwningPublication(methodologyVersion.MethodologyId); - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlers.cs index 1e1b65f87c5..af891760f16 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DeleteSpecificReleaseAuthorizationHandlers.cs @@ -14,12 +14,12 @@ public class DeleteSpecificReleaseRequirement : IAuthorizationRequirement public class DeleteSpecificReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public DeleteSpecificReleaseAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -38,7 +38,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandler.cs index d1823c70507..486e1e2ff33 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/DropMethodologyLinkAuthorizationHandler.cs @@ -15,12 +15,12 @@ public class DropMethodologyLinkRequirement : IAuthorizationRequirement public class DropMethodologyLinkAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public DropMethodologyLinkAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -41,7 +41,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), link.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/HasClaimAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/HasClaimAuthorizationHandler.cs deleted file mode 100644 index 3afd2fc71e6..00000000000 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/HasClaimAuthorizationHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; - -namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers -{ - public abstract class HasClaimAuthorizationHandler : AuthorizationHandler - where TRequirement : IAuthorizationRequirement - { - private readonly SecurityClaimTypes _claimType; - private readonly string? _claimValue; - - protected HasClaimAuthorizationHandler(SecurityClaimTypes claimType, string? claimValue = null) - { - _claimType = claimType; - _claimValue = claimValue; - } - - protected override Task HandleRequirementAsync(AuthorizationHandlerContext authContext, - TRequirement requirement) - { - if (SecurityUtils.HasClaim(authContext.User, _claimType, _claimValue)) - { - authContext.Succeed(requirement); - } - - return Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlers.cs index d6dddb5f9ac..e9d0631499e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/LegacyReleaseAuthorizationHandlers.cs @@ -16,12 +16,12 @@ public class ManageLegacyReleasesRequirement : IAuthorizationRequirement public class ManageLegacyReleasesAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ManageLegacyReleasesAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -34,7 +34,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, @@ -52,12 +52,12 @@ public class ViewLegacyReleaseRequirement : IAuthorizationRequirement public class ViewLegacyReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ViewLegacyReleaseAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -70,7 +70,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), legacyRelease.PublicationId, @@ -88,11 +88,11 @@ public class UpdateLegacyReleaseRequirement : IAuthorizationRequirement public class UpdateLegacyReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; - public UpdateLegacyReleaseAuthorizationHandler(AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + public UpdateLegacyReleaseAuthorizationHandler(AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -105,7 +105,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), legacyRelease.PublicationId, @@ -123,11 +123,11 @@ public class DeleteLegacyReleaseRequirement : IAuthorizationRequirement public class DeleteLegacyReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; - public DeleteLegacyReleaseAuthorizationHandler(AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + public DeleteLegacyReleaseAuthorizationHandler(AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -140,7 +140,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), legacyRelease.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlers.cs index 4c1cd271c12..c2bca2de1ae 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificMethodologyAuthorizationHandlers.cs @@ -18,16 +18,16 @@ public class MakeAmendmentOfSpecificMethodologyAuthorizationHandler { private readonly IMethodologyVersionRepository _methodologyVersionRepository; private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public MakeAmendmentOfSpecificMethodologyAuthorizationHandler( IMethodologyVersionRepository methodologyVersionRepository, IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyVersionRepository = methodologyVersionRepository; _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -54,7 +54,7 @@ protected override async Task HandleRequirementAsync( // If the user is a Publication Owner of the Publication that owns this Methodology, they can create // an Amendment of this Methodology. - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificReleaseAuthorizationHandlers.cs index 32253753b99..0cb34fcd20b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MakeAmendmentOfSpecificReleaseAuthorizationHandlers.cs @@ -18,14 +18,14 @@ public class MakeAmendmentOfSpecificReleaseAuthorizationHandler : AuthorizationHandler { private readonly ContentDbContext _contentDbContext; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public MakeAmendmentOfSpecificReleaseAuthorizationHandler( ContentDbContext contentDbContext, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _contentDbContext = contentDbContext; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -44,7 +44,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlers.cs index 2bbc83c7ce3..3c7ee90edba 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ManageExternalMethodologyForSpecificPublicationAuthorizationHandlers.cs @@ -15,12 +15,12 @@ public class ManageExternalMethodologyForSpecificPublicationRequirement : IAutho public class ManageExternalMethodologyForSpecificPublicationAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ManageExternalMethodologyForSpecificPublicationAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -33,7 +33,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs index ea778debb84..8683d379c9f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs @@ -19,16 +19,16 @@ public class MarkMethodologyAsApprovedAuthorizationHandler : { private readonly IMethodologyVersionRepository _methodologyVersionRepository; private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public MarkMethodologyAsApprovedAuthorizationHandler( IMethodologyVersionRepository methodologyVersionRepository, IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyVersionRepository = methodologyVersionRepository; _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -53,7 +53,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext // If the user is a Publication Approver that owns this Methodology, they can approve it. // Additionally, if they're an Approver for any Releases on the owning Publication, they can approve it. - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrAnyRelease( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs index 86fd5d608ec..ff4a45233dc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs @@ -5,7 +5,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; @@ -21,16 +21,16 @@ public class MarkMethodologyAsDraftAuthorizationHandler : AuthorizationHandler< { private readonly IMethodologyVersionRepository _methodologyVersionRepository; private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public MarkMethodologyAsDraftAuthorizationHandler( IMethodologyVersionRepository methodologyVersionRepository, IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyVersionRepository = methodologyVersionRepository; _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -60,7 +60,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var owningPublication = await _methodologyRepository.GetOwningPublication(methodologyVersion.MethodologyId); - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrAnyRelease( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs index 5dedbe3ffae..f5a8d17bcd9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs @@ -5,7 +5,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; @@ -21,16 +21,16 @@ public class MarkMethodologyAsHigherLevelReviewAuthorizationHandler : Authorizat { private readonly IMethodologyVersionRepository _methodologyVersionRepository; private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public MarkMethodologyAsHigherLevelReviewAuthorizationHandler( IMethodologyVersionRepository methodologyVersionRepository, IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyVersionRepository = methodologyVersionRepository; _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -59,7 +59,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var owningPublication = await _methodologyRepository.GetOwningPublication(methodologyVersion.MethodologyId); - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrAnyRelease( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandler.cs index 9fca960c521..5b35f3f1077 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/PublishSpecificReleaseAuthorizationHandler.cs @@ -14,12 +14,12 @@ public class PublishSpecificReleaseRequirement : IAuthorizationRequirement public class PublishSpecificReleaseAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public PublishSpecificReleaseAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -38,7 +38,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs index b7dde36c46c..c4b7000e79c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs @@ -6,7 +6,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; @@ -16,14 +16,14 @@ public abstract class ReleaseStatusAuthorizationHandler : Authoriz where TRequirement : IAuthorizationRequirement { private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; protected ReleaseStatusAuthorizationHandler( IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _releasePublishingStatusRepository = releasePublishingStatusRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected abstract ReleaseApprovalStatus TargetApprovalStatus { get; } @@ -71,7 +71,7 @@ private async Task HandleMovingToApproved( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, @@ -102,7 +102,7 @@ private async Task HandleMovingToHigherLevelReview( ? ListOf(ReleaseRole.Approver) : ReleaseEditorAndApproverRoles; - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, @@ -133,7 +133,7 @@ private async Task HandleMovingToDraft( ? ListOf(ReleaseRole.Approver) : ReleaseEditorAndApproverRoles; - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, @@ -155,10 +155,10 @@ public class MarkReleaseAsDraftAuthorizationHandler { public MarkReleaseAsDraftAuthorizationHandler( IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) : base( releasePublishingStatusRepository, - authorizationHandlerResourceRoleService) + authorizationHandlerService) { } @@ -174,10 +174,10 @@ public class MarkReleaseAsHigherLevelReviewAuthorizationHandler { public MarkReleaseAsHigherLevelReviewAuthorizationHandler( IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) : base( releasePublishingStatusRepository, - authorizationHandlerResourceRoleService) + authorizationHandlerService) { } @@ -193,10 +193,10 @@ public class MarkReleaseAsApprovedAuthorizationHandler { public MarkReleaseAsApprovedAuthorizationHandler( IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) : base( releasePublishingStatusRepository, - authorizationHandlerResourceRoleService) + authorizationHandlerService) { } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ResolveSpecificCommentAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ResolveSpecificCommentAuthorizationHandler.cs index 4e033ad2752..60ef02937d0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ResolveSpecificCommentAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ResolveSpecificCommentAuthorizationHandler.cs @@ -1,7 +1,6 @@ #nullable enable using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; @@ -17,16 +16,13 @@ public class ResolveSpecificCommentAuthorizationHandler : AuthorizationHandler { private readonly ContentDbContext _contentDbContext; - private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ResolveSpecificCommentAuthorizationHandler(ContentDbContext contentDbContext, - IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _contentDbContext = contentDbContext; - _releasePublishingStatusRepository = releasePublishingStatusRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -37,8 +33,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var updateSpecificReleaseContext = new AuthorizationHandlerContext( new[] {new UpdateSpecificReleaseRequirement()}, context.User, release); await new UpdateSpecificReleaseAuthorizationHandler( - _releasePublishingStatusRepository, - _authorizationHandlerResourceRoleService) + _authorizationHandlerService) .HandleAsync(updateSpecificReleaseContext); if (updateSpecificReleaseContext.HasSucceeded) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateContactAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateContactAuthorizationHandler.cs index bca13a01fb3..65a546f51b2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateContactAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateContactAuthorizationHandler.cs @@ -13,12 +13,12 @@ public class UpdateContactRequirement : IAuthorizationRequirement public class UpdateContactAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdateContactAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -31,7 +31,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandler.cs index 9a93549dc2d..3a88a82d177 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdatePublicationSummaryAuthorizationHandler.cs @@ -14,12 +14,12 @@ public class UpdatePublicationSummaryRequirement : IAuthorizationRequirement public class UpdatePublicationSummaryAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdatePublicationSummaryAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -32,7 +32,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandler.cs index 48bfb0322c8..9d5ed4f1e96 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateReleaseRoleAuthorizationHandler.cs @@ -17,12 +17,12 @@ public class UpdateReleaseRoleRequirement : IAuthorizationRequirement public class UpdateReleaseRoleAuthorizationHandler : AuthorizationHandler> { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdateReleaseRoleAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -40,7 +40,7 @@ protected override async Task HandleRequirementAsync( if (releaseRole == ReleaseRole.Contributor) { - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificCommentAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificCommentAuthorizationHandler.cs index 5c7150beeb7..c60918cbe1c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificCommentAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificCommentAuthorizationHandler.cs @@ -1,7 +1,6 @@ #nullable enable using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; @@ -19,16 +18,13 @@ public class UpdateSpecificCommentAuthorizationHandler : AuthorizationHandler { private readonly ContentDbContext _contentDbContext; - private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdateSpecificCommentAuthorizationHandler(ContentDbContext contentDbContext, - IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _contentDbContext = contentDbContext; - _releasePublishingStatusRepository = releasePublishingStatusRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -38,9 +34,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var release = GetRelease(_contentDbContext, resource); var updateSpecificReleaseContext = new AuthorizationHandlerContext( new[] {new UpdateSpecificReleaseRequirement()}, context.User, release); - await new UpdateSpecificReleaseAuthorizationHandler( - _releasePublishingStatusRepository, - _authorizationHandlerResourceRoleService) + + await new UpdateSpecificReleaseAuthorizationHandler(_authorizationHandlerService) .HandleAsync(updateSpecificReleaseContext); if (!updateSpecificReleaseContext.HasSucceeded) @@ -50,7 +45,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var canUpdateOwnCommentContext = new AuthorizationHandlerContext(new[] {requirement}, context.User, resource); - + await new CanUpdateOwnCommentAuthorizationHandler().HandleAsync(canUpdateOwnCommentContext); if (canUpdateOwnCommentContext.HasSucceeded) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs index 4a67dbb38d3..9d2a6678581 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs @@ -4,7 +4,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; @@ -18,14 +18,14 @@ public class UpdateSpecificMethodologyAuthorizationHandler : AuthorizationHandler { private readonly IMethodologyRepository _methodologyRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdateSpecificMethodologyAuthorizationHandler( IMethodologyRepository methodologyRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyRepository = methodologyRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -46,7 +46,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext var owningPublication = await _methodologyRepository.GetOwningPublication(methodologyVersion.MethodologyId); - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrAnyRelease( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs index 109bbda1bd0..3c922f900fd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs @@ -1,11 +1,8 @@ -using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; -using GovUk.Education.ExploreEducationStatistics.Publisher.Model; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; @@ -18,15 +15,12 @@ public class UpdateSpecificReleaseRequirement : IAuthorizationRequirement public class UpdateSpecificReleaseAuthorizationHandler : AuthorizationHandler { - private readonly IReleasePublishingStatusRepository _releasePublishingStatusRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public UpdateSpecificReleaseAuthorizationHandler( - IReleasePublishingStatusRepository releasePublishingStatusRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _releasePublishingStatusRepository = releasePublishingStatusRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -34,13 +28,7 @@ protected override async Task HandleRequirementAsync( UpdateSpecificReleaseRequirement requirement, Release release) { - var statuses = await _releasePublishingStatusRepository.GetAllByOverallStage( - release.Id, - ReleasePublishingStatusOverallStage.Started, - ReleasePublishingStatusOverallStage.Complete - ); - - if (statuses.Any() || release.Published != null) + if (release.ApprovalStatus == Approved) { return; } @@ -50,16 +38,11 @@ protected override async Task HandleRequirementAsync( context.Succeed(requirement); return; } - - var allowedPublicationRoles = release.ApprovalStatus == Approved - ? ListOf(PublicationRole.Approver) - : ListOf(PublicationRole.Owner, PublicationRole.Approver); - - var allowedReleaseRoles = release.ApprovalStatus == Approved - ? ListOf(ReleaseRole.Approver) - : ReleaseEditorAndApproverRoles; - if (await _authorizationHandlerResourceRoleService + var allowedPublicationRoles = ListOf(PublicationRole.Owner, PublicationRole.Approver); + var allowedReleaseRoles = ReleaseEditorAndApproverRoles; + + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandler.cs index 07c14010d91..a83ccaadc1b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewReleaseStatusHistoryAuthorizationHandler.cs @@ -2,7 +2,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers @@ -14,12 +14,12 @@ public class ViewReleaseStatusHistoryRequirement : IAuthorizationRequirement public class ViewReleaseStatusHistoryAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ViewReleaseStatusHistoryAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -33,7 +33,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs index 26802ff6290..d857a0d9781 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs @@ -8,7 +8,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers. - AuthorizationHandlerResourceRoleService; + AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using IPublicationRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IPublicationRepository; @@ -26,20 +26,20 @@ public class ViewSpecificMethodologyAuthorizationHandler : private readonly IUserReleaseRoleRepository _userReleaseRoleRepository; private readonly IPreReleaseService _preReleaseService; private readonly IPublicationRepository _publicationRepository; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ViewSpecificMethodologyAuthorizationHandler( IMethodologyRepository methodologyRepository, IUserReleaseRoleRepository userReleaseRoleRepository, IPreReleaseService preReleaseService, IPublicationRepository publicationRepository, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { _methodologyRepository = methodologyRepository; _userReleaseRoleRepository = userReleaseRoleRepository; _preReleaseService = preReleaseService; _publicationRepository = publicationRepository; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -59,7 +59,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext // If the user is a Publication Owner or Approver of the Publication that owns this Methodology, they can // view it. Additionally, if the user is an Editor (Contributor, Lead) or an Approver of any // (Live or non-Live) Release of the owning Publication of this Methodology, they can view it. - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrAnyRelease( context.User.GetUserId(), owningPublication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandler.cs index 19eb4046724..8590e0fdb86 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPreReleaseSummaryAuthorizationHandler.cs @@ -4,7 +4,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; using Microsoft.AspNetCore.Authorization; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; @@ -17,15 +17,15 @@ public class ViewSpecificPreReleaseSummaryRequirement : IAuthorizationRequiremen public class ViewSpecificPreReleaseSummaryAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; private static readonly ReleaseRole[] UnrestrictedReleaseViewerAndPrereleaseViewerRoles = UnrestrictedReleaseViewerRoles.Append(ReleaseRole.PrereleaseViewer).ToArray(); public ViewSpecificPreReleaseSummaryAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, @@ -38,7 +38,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublicationOrRelease( context.User.GetUserId(), release.PublicationId, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs index c235c8899f4..353d1ba489a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; @@ -13,78 +14,51 @@ public class ViewSpecificPublicationRequirement : IAuthorizationRequirement } public class - ViewSpecificPublicationAuthorizationHandler : CompoundAuthorizationHandler { + private readonly ContentDbContext _contentDbContext; + private readonly AuthorizationHandlerService _authorizationHandlerService; + public ViewSpecificPublicationAuthorizationHandler( ContentDbContext contentDbContext, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - : base( - new CanSeeAllPublicationsAuthorizationHandler(), - new HasOwnerOrApproverRoleOnPublicationAuthorizationHandler(authorizationHandlerResourceRoleService), - new HasRoleOnAnyChildReleaseAuthorizationHandler(contentDbContext)) + AuthorizationHandlerService authorizationHandlerService) { + _contentDbContext = contentDbContext; + _authorizationHandlerService = authorizationHandlerService; } - public class CanSeeAllPublicationsAuthorizationHandler : HasClaimAuthorizationHandler< - ViewSpecificPublicationRequirement> + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ViewSpecificPublicationRequirement requirement, + Publication publication) { - public CanSeeAllPublicationsAuthorizationHandler() - : base(SecurityClaimTypes.AccessAllReleases) + // If the user has the "AccessAllPublications" Claim, they can see any Publication. + if (SecurityUtils.HasClaim(context.User, SecurityClaimTypes.AccessAllPublications)) { + context.Succeed(requirement); + return; } - } - - public class HasRoleOnAnyChildReleaseAuthorizationHandler - : AuthorizationHandler - { - private readonly ContentDbContext _context; - public HasRoleOnAnyChildReleaseAuthorizationHandler(ContentDbContext context) + // If the user has any PublicationRole on the Publication, they can see it. + if (await _authorizationHandlerService + .HasRolesOnPublication( + context.User.GetUserId(), + publication.Id, + EnumUtil.GetEnumValuesAsArray())) { - _context = context; + context.Succeed(requirement); + return; } - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authContext, - ViewSpecificPublicationRequirement requirement, Publication publication) - { - var userId = authContext.User.GetUserId(); - - if (await _context + // If the user has any ReleaseRoles on any of the Publication's Releases, they can see it. + if (await _contentDbContext .UserReleaseRoles .Include(r => r.Release) - .Where(r => r.UserId == userId) + .Where(r => r.UserId == context.User.GetUserId()) .AnyAsync(r => r.Release.PublicationId == publication.Id)) - { - authContext.Succeed(requirement); - } - } - } - - public class HasOwnerOrApproverRoleOnPublicationAuthorizationHandler - : AuthorizationHandler - { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; - - public HasOwnerOrApproverRoleOnPublicationAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ViewSpecificPublicationRequirement requirement, - Publication publication) { - if (await _authorizationHandlerResourceRoleService - .HasRolesOnPublication( - context.User.GetUserId(), - publication.Id, - PublicationRole.Owner, PublicationRole.Approver)) - { - context.Succeed(requirement); - } + context.Succeed(requirement); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler.cs index 1193dd1cd8e..900d529da5b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler.cs @@ -14,12 +14,12 @@ public class ViewSpecificPublicationReleaseTeamAccessRequirement : IAuthorizatio public class ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler : AuthorizationHandler { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; + private readonly AuthorizationHandlerService _authorizationHandlerService; public ViewSpecificPublicationReleaseTeamAccessAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) + AuthorizationHandlerService authorizationHandlerService) { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; + _authorizationHandlerService = authorizationHandlerService; } protected override async Task HandleRequirementAsync( @@ -33,7 +33,7 @@ protected override async Task HandleRequirementAsync( return; } - if (await _authorizationHandlerResourceRoleService + if (await _authorizationHandlerService .HasRolesOnPublication( context.User.GetUserId(), publication.Id, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlers.cs index 1fa8d98bd85..1f31b318d87 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificReleaseAuthorizationHandlers.cs @@ -1,123 +1,28 @@ using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Models; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; -using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Security.AuthorizationHandlers; using Microsoft.AspNetCore.Authorization; -using static System.DateTime; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; -namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers -{ - public class ViewSpecificReleaseAuthorizationHandler : CompoundAuthorizationHandler - { - public ViewSpecificReleaseAuthorizationHandler( - IPreReleaseService preReleaseService, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - : base( - new CanSeeAllReleasesAuthorizationHandler(), - new HasOwnerOrApproverRoleOnParentPublicationAuthorizationHandler(authorizationHandlerResourceRoleService), - new HasUnrestrictedViewerRoleOnReleaseAuthorizationHandler(authorizationHandlerResourceRoleService), - new HasPreReleaseRoleWithinAccessWindowAuthorizationHandler(preReleaseService, authorizationHandlerResourceRoleService)) - { - } - - public class CanSeeAllReleasesAuthorizationHandler : HasClaimAuthorizationHandler< - ViewReleaseRequirement> - { - public CanSeeAllReleasesAuthorizationHandler() - : base(SecurityClaimTypes.AccessAllReleases) - { - } - } - - public class HasUnrestrictedViewerRoleOnReleaseAuthorizationHandler - : AuthorizationHandler - { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; +namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; - public HasUnrestrictedViewerRoleOnReleaseAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ViewReleaseRequirement requirement, - Release release) - { - if (await _authorizationHandlerResourceRoleService - .HasRolesOnRelease( - context.User.GetUserId(), - release.Id, - UnrestrictedReleaseViewerRoles)) - { - context.Succeed(requirement); - } - } - } - - public class HasOwnerOrApproverRoleOnParentPublicationAuthorizationHandler - : AuthorizationHandler - { - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; +public class ViewSpecificReleaseAuthorizationHandler : AuthorizationHandler +{ + private readonly AuthorizationHandlerService _authorizationHandlerService; - public HasOwnerOrApproverRoleOnParentPublicationAuthorizationHandler( - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; - } + public ViewSpecificReleaseAuthorizationHandler( + AuthorizationHandlerService authorizationHandlerService) + { + _authorizationHandlerService = authorizationHandlerService; + } - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ViewReleaseRequirement requirement, - Release release) - { - if (await _authorizationHandlerResourceRoleService - .HasRolesOnPublication( - context.User.GetUserId(), - release.PublicationId, - PublicationRole.Owner, PublicationRole.Approver)) - { - context.Succeed(requirement); - } - } - } - - public class HasPreReleaseRoleWithinAccessWindowAuthorizationHandler - : AuthorizationHandler + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ViewReleaseRequirement requirement, + Release release) + { + if (await _authorizationHandlerService.IsReleaseViewableByUser(release, context.User)) { - private readonly IPreReleaseService _preReleaseService; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; - - public HasPreReleaseRoleWithinAccessWindowAuthorizationHandler( - IPreReleaseService preReleaseService, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - { - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; - _preReleaseService = preReleaseService; - } - - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ViewReleaseRequirement requirement, - Release release) - { - if (await _authorizationHandlerResourceRoleService - .HasRolesOnRelease( - context.User.GetUserId(), - release.Id, - ReleaseRole.PrereleaseViewer)) - { - var windowStatus = _preReleaseService.GetPreReleaseWindowStatus(release, UtcNow); - if (windowStatus.Access == PreReleaseAccess.Within) - { - context.Succeed(requirement); - } - } - } + context.Succeed(requirement); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSubjectDataAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSubjectDataAuthorizationHandler.cs index e2677b14b5a..4ed3c9a05e9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSubjectDataAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSubjectDataAuthorizationHandler.cs @@ -1,71 +1,39 @@ using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Security.AuthorizationHandlers; using GovUk.Education.ExploreEducationStatistics.Data.Model; using GovUk.Education.ExploreEducationStatistics.Data.Services.Security; -using GovUk.Education.ExploreEducationStatistics.Data.Services.Security.AuthorizationHandlers; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; -namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers -{ - public class ViewSubjectDataAuthorizationHandler : CompoundAuthorizationHandler< - ViewSubjectDataRequirement, ReleaseSubject> - { - public ViewSubjectDataAuthorizationHandler( - ContentDbContext contentDbContext, - IPreReleaseService preReleaseService, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) : base( - new ViewSubjectDataForPublishedReleasesAuthorizationHandler(contentDbContext), - new SubjectBelongsToViewableReleaseAuthorizationHandler( - contentDbContext, - preReleaseService, - authorizationHandlerResourceRoleService)) - { - } - - public class SubjectBelongsToViewableReleaseAuthorizationHandler : AuthorizationHandler< - ViewSubjectDataRequirement, ReleaseSubject> - { - private readonly ContentDbContext _contentDbContext; - private readonly IPreReleaseService _preReleaseService; - private readonly AuthorizationHandlerResourceRoleService _authorizationHandlerResourceRoleService; - - public SubjectBelongsToViewableReleaseAuthorizationHandler( - ContentDbContext contentDbContext, - IPreReleaseService preReleaseService, - AuthorizationHandlerResourceRoleService authorizationHandlerResourceRoleService) - { - _contentDbContext = contentDbContext; - _preReleaseService = preReleaseService; - _authorizationHandlerResourceRoleService = authorizationHandlerResourceRoleService; - } +namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; - protected override async Task HandleRequirementAsync( - AuthorizationHandlerContext context, - ViewSubjectDataRequirement requirement, - ReleaseSubject releaseSubject) - { - var viewSpecificReleaseHandler = new - ViewSpecificReleaseAuthorizationHandler( - _preReleaseService, - _authorizationHandlerResourceRoleService); - - var contentRelease = await _contentDbContext - .Releases - .FirstAsync(release => release.Id == releaseSubject.ReleaseId); +public class ViewSubjectDataAuthorizationHandler : AuthorizationHandler< + ViewSubjectDataRequirement, ReleaseSubject> +{ + private readonly ContentDbContext _contentDbContext; + private readonly AuthorizationHandlerService _authorizationHandlerService; - var delegatedContext = new AuthorizationHandlerContext( - new[] {new ViewReleaseRequirement()}, context.User, contentRelease); + public ViewSubjectDataAuthorizationHandler( + ContentDbContext contentDbContext, + AuthorizationHandlerService authorizationHandlerService) + { + _contentDbContext = contentDbContext; + _authorizationHandlerService = authorizationHandlerService; + } - await viewSpecificReleaseHandler.HandleAsync(delegatedContext); + protected override async Task HandleRequirementAsync( + AuthorizationHandlerContext context, + ViewSubjectDataRequirement requirement, + ReleaseSubject releaseSubject) + { + // If this data has been published, it is visible to anyone. + var release = await _contentDbContext + .Releases + .FirstAsync(r => r.Id == releaseSubject.ReleaseId); - if (delegatedContext.HasSucceeded) - { - context.Succeed(requirement); - } - } + if (await _authorizationHandlerService.IsReleaseViewableByUser(release, context.User)) + { + context.Succeed(requirement); } } -} +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyApprovalService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyApprovalService.cs index bc4f11eb37a..dcd0c61aaf6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyApprovalService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyApprovalService.cs @@ -36,6 +36,7 @@ public class MethodologyApprovalService : IMethodologyApprovalService private readonly IUserReleaseRoleService _userReleaseRoleService; private readonly IMethodologyCacheService _methodologyCacheService; private readonly IEmailTemplateService _emailTemplateService; + private readonly IRedirectsCacheService _redirectsCacheService; public MethodologyApprovalService( IPersistenceHelper persistenceHelper, @@ -48,7 +49,8 @@ public MethodologyApprovalService( IUserService userService, IUserReleaseRoleService userReleaseRoleService, IMethodologyCacheService methodologyCacheService, - IEmailTemplateService emailTemplateService) + IEmailTemplateService emailTemplateService, + IRedirectsCacheService redirectsCacheService) { _persistenceHelper = persistenceHelper; _context = context; @@ -61,6 +63,7 @@ public MethodologyApprovalService( _userReleaseRoleService = userReleaseRoleService; _methodologyCacheService = methodologyCacheService; _emailTemplateService = emailTemplateService; + _redirectsCacheService = redirectsCacheService; } public async Task> UpdateApprovalStatus( @@ -123,8 +126,8 @@ private async Task> UpdateStatus( if (isToBePublished) { - // Update the 'All Methodologies' cache item await _methodologyCacheService.UpdateSummariesTree(); + await _redirectsCacheService.UpdateRedirects(); } if (request.Status == MethodologyApprovalStatus.HigherLevelReview) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index 3b0319fd081..5a30c0e990a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -245,6 +245,7 @@ public async Task> UpdateMetho return await _persistenceHelper .CheckEntityExists(id, q => q.Include(m => m.Methodology)) + // NOTE: Permission checks nested within UpdateStatus and UpdateDetails .OnSuccess(methodologyVersion => UpdateStatus(methodologyVersion, request)) .OnSuccess(methodologyVersion => UpdateDetails(methodologyVersion, request)) .OnSuccess(BuildMethodologyVersionViewModel); @@ -300,13 +301,19 @@ await _context.Entry(loadedMethodologyVersion.ScheduledWithRelease) private async Task> UpdateStatus( MethodologyVersion methodologyVersionToUpdate, - MethodologyApprovalUpdateRequest request) + MethodologyUpdateRequest request) { if (!request.IsStatusUpdateRequired(methodologyVersionToUpdate)) { return methodologyVersionToUpdate; } + if (methodologyVersionToUpdate.Title != request.Title) // EES-3789 Should also check for slug change here too? + { + throw new ArgumentException( + "Should not update status of MethodologyVersion while simultaneously updating it's title"); + } + return await _methodologyApprovalService .UpdateApprovalStatus(methodologyVersionToUpdate.Id, request) .OnSuccess(_ => _context @@ -319,40 +326,76 @@ private async Task> UpdateDetails( MethodologyVersion methodologyVersionToUpdate, MethodologyUpdateRequest request) { - if (methodologyVersionToUpdate.Title == request.Title) + var newSlug = SlugFromTitle(request.Title); + + var titleChanged = methodologyVersionToUpdate.Title != request.Title; + var slugChanged = methodologyVersionToUpdate.Slug != newSlug; + + if (!titleChanged && !slugChanged) { // Details unchanged return methodologyVersionToUpdate; } - var newSlug = SlugFromTitle(request.Title); + if (request.Status == MethodologyApprovalStatus.Approved) + { + throw new ArgumentException("Should not be updating details of an approved methodology"); + } return await _userService.CheckCanUpdateMethodologyVersion(methodologyVersionToUpdate) - .OnSuccessDo(async _ => await ValidateMethodologySlug( - newSlug, oldSlug: methodologyVersionToUpdate.Methodology.Slug)) + .OnSuccessDo(async methodologyVersion => await ValidateMethodologySlug( + newSlug, + oldSlug: methodologyVersionToUpdate.Slug, + methodologyId: methodologyVersion.MethodologyId)) .OnSuccess(async methodologyVersion => { methodologyVersion.Updated = DateTime.UtcNow; - if (request.Title != methodologyVersion.Title) + if (titleChanged) { methodologyVersion.AlternativeTitle = request.Title != methodologyVersion.Methodology.OwningPublicationTitle ? request.Title : null; + } + + if (slugChanged) + { + var methodology = methodologyVersion.Methodology; + await _context.Entry(methodology) + .Collection(m => m.Versions) + .LoadAsync(); + + // Only unpublished versions need a redirect to be created, as users cannot + // update a published methodology version's AlternativeSlug + if (methodologyVersion.Id == methodology.LatestVersion().Id + // If an unpublished version already has a redirect, that means the version's slug has + // been updated previously. And so it doesn't need a new redirect, as the hypothetical + // redirect's `Slug` would have never have been live. We only need to create one redirect + // from the methodology's current slug for the unpublished amendment, as that is the only + // slug that has been live. + && !await _context.MethodologyRedirects.AnyAsync(mr => + mr.MethodologyVersionId == methodologyVersion.Id)) - // If we're updating a Methodology that is not an Amendment, it's not yet publicly - // visible and so its Slug can be updated. At the point that a Methodology is publicly - // visible and the only means of updating it is via Amendments, we will no longer allow its - // Slug to change even though its AlternativeTitle can. - if (!methodologyVersion.Amendment) { - methodologyVersion.Methodology.Slug = newSlug; + var methodologyRedirect = new MethodologyRedirect + { + MethodologyVersionId = methodologyVersion.Id, + Slug = methodologyVersion.Slug, + }; + await _context.MethodologyRedirects.AddAsync(methodologyRedirect); } + + methodologyVersion.AlternativeSlug = + newSlug != methodologyVersion.Methodology.OwningPublicationSlug + ? newSlug + : null; } await _context.SaveChangesAsync(); + // NOTE: No need to invalidate redirects.json cache here as the methodologyVersion is unpublished + return methodologyVersion; }); } @@ -477,6 +520,7 @@ private async Task> DeleteVersion(MethodologyVersion .OnSuccessVoid(async () => { _context.MethodologyVersions.Remove(methodologyVersion); + await _context.SaveChangesAsync(); }); } @@ -512,7 +556,7 @@ private static IdTitleViewModel BuildPublicationViewModel(PublicationMethodology } private async Task> ValidateMethodologySlug( - string newSlug, string? oldSlug = null) + string newSlug, string? oldSlug = null, Guid? methodologyId = null) { if (newSlug == oldSlug) { @@ -527,6 +571,18 @@ private async Task> ValidateMethodologySlug( return ValidationActionResult(SlugNotUnique); } + var redirectExistsToOtherMethodology = await _context.MethodologyRedirects + .Where(mr => + mr.Slug == newSlug + // we exclude redirects to the same methodology i.e. we allow the slug to be changed back + && (methodologyId != null && methodologyId != mr.MethodologyVersion.MethodologyId)) + .AnyAsync(); + + if (redirectExistsToOtherMethodology) + { + return ValidationActionResult(SlugUsedByRedirect); + } + return Unit.Instance; } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs index 03de9ebd2c5..fbfe84e4cad 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/PublicationService.cs @@ -5,24 +5,23 @@ using System.Threading.Tasks; using AutoMapper; using GovUk.Education.ExploreEducationStatistics.Admin.Requests; -using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Util; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels; -using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Common.ViewModels; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationUtils; using IPublicationRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IPublicationRepository; +using IPublicationService = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IPublicationService; namespace GovUk.Education.ExploreEducationStatistics.Admin.Services { @@ -33,7 +32,7 @@ public class PublicationService : IPublicationService private readonly IPersistenceHelper _persistenceHelper; private readonly IUserService _userService; private readonly IPublicationRepository _publicationRepository; - private readonly IMethodologyVersionRepository _methodologyVersionRepository; + private readonly IMethodologyService _methodologyService; private readonly IPublicationCacheService _publicationCacheService; private readonly IMethodologyCacheService _methodologyCacheService; @@ -43,7 +42,7 @@ public PublicationService( IPersistenceHelper persistenceHelper, IUserService userService, IPublicationRepository publicationRepository, - IMethodologyVersionRepository methodologyVersionRepository, + IMethodologyService methodologyService, IPublicationCacheService publicationCacheService, IMethodologyCacheService methodologyCacheService) { @@ -52,7 +51,7 @@ public PublicationService( _persistenceHelper = persistenceHelper; _userService = userService; _publicationRepository = publicationRepository; - _methodologyVersionRepository = methodologyVersionRepository; + _methodologyService = methodologyService; _publicationCacheService = publicationCacheService; _methodologyCacheService = methodologyCacheService; } @@ -211,9 +210,9 @@ public async Task> UpdatePublication( await _context.SaveChangesAsync(); - if (originalTitle != publication.Title) + if (originalTitle != publication.Title || originalSlug != publication.Slug) { - await _methodologyVersionRepository.PublicationTitleChanged(publicationId, + await _methodologyService.PublicationTitleOrSlugChanged(publicationId, originalSlug, publication.Title, publication.Slug); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseApprovalService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseApprovalService.cs index 8a68dffdb29..411120aa6b1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseApprovalService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseApprovalService.cs @@ -115,7 +115,6 @@ public async Task> CreateReleaseStatus( { return await _persistenceHelper .CheckEntityExists(releaseId, ReleaseChecklistService.HydrateReleaseForChecklist) - .OnSuccess(_userService.CheckCanUpdateRelease) .OnSuccessDo(release => _userService.CheckCanUpdateReleaseStatus(release, request.ApprovalStatus)) .OnSuccessDo(() => ValidatePublishDate(request)) .OnSuccess(async release => diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/UserReleaseRoleRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/UserReleaseRoleRepository.cs index e40a73f1beb..c4623f431c2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/UserReleaseRoleRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/UserReleaseRoleRepository.cs @@ -7,7 +7,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.EntityFrameworkCore; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; namespace GovUk.Education.ExploreEducationStatistics.Admin.Services diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 08b54685c47..4d110d84477 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -500,6 +500,8 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(s => { @@ -570,7 +572,7 @@ public virtual void ConfigureServices(IServiceCollection services) AddPersistenceHelper(services); AddPersistenceHelper(services); AddPersistenceHelper(services); - services.AddTransient(); + services.AddTransient(); services.AddScoped(); // This service handles the generation of the JWTs for users after they log in @@ -732,9 +734,9 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // // This Regex is case insensitive. rewriteOptions.AddRewrite( - @"^(?i)identity/(?!account/(login|externallogin|inviteexpired))", - replacement: "/", - skipRemainingRules: true); + @"^(?i)identity/(?!account/(login|externallogin|inviteexpired))", + replacement: "/", + skipRemainingRules: true); rewriteOptions.Add(new LowercasePathRule()); app.UseRewriter(rewriteOptions); @@ -818,4 +820,4 @@ private static void ApplyCustomMigrations(params ICustomMigration[] migrations) } } } -} \ No newline at end of file +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationErrorMessages.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationErrorMessages.cs index 734c9ee7f36..cc161c80284 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationErrorMessages.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Validators/ValidationErrorMessages.cs @@ -5,6 +5,7 @@ public enum ValidationErrorMessages { // Slug SlugNotUnique, + SlugUsedByRedirect, // Partial date PartialDateNotValid, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Methodology/MethodologyUpdateRequest.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Methodology/MethodologyUpdateRequest.cs index b6e06d38a05..4b2a1fe9558 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Methodology/MethodologyUpdateRequest.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/Methodology/MethodologyUpdateRequest.cs @@ -5,6 +5,5 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodolog public class MethodologyUpdateRequest : MethodologyApprovalUpdateRequest { - // TODO SOW4 EES-2212 - update to AlternativeTitle [Required] public string Title { get; set; } = string.Empty; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/RedirectsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/RedirectsController.cs new file mode 100644 index 00000000000..89813d514a2 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Controllers/RedirectsController.cs @@ -0,0 +1,28 @@ +#nullable enable +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; +using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Api.Controllers +{ + [Route("api")] + public class RedirectsController : ControllerBase + { + private readonly IRedirectsCacheService _redirectsCacheService; + + public RedirectsController( + IRedirectsCacheService redirectsCacheService) + { + _redirectsCacheService = redirectsCacheService; + } + + [HttpGet("redirects")] + public async Task> List() + { + return await _redirectsCacheService.List() + .HandleFailuresOrOk(); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs index 82cb49c7a0f..b40a05d3132 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Api/Startup.cs @@ -162,6 +162,8 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); StartupSecurityConfiguration.ConfigureAuthorizationPolicies(services); StartupSecurityConfiguration.ConfigureResourceBasedAuthorization(services); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs index 363f0b87267..e95c8d5547f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs @@ -16,8 +16,9 @@ public static Generator WithDefaults(this Generator ge public static InstanceSetters SetDefaults(this InstanceSetters setters) => setters - .SetDefault(p => p.Id) - .SetDefault(p => p.Slug); + .SetDefault(m => m.Id) + .SetDefault(m => m.OwningPublicationTitle) + .SetDefault(m => m.OwningPublicationSlug); public static Generator WithOwningPublication( this Generator generator, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/MethodologyVersionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/MethodologyVersionTests.cs index 4744b132e27..ec6e2acb13d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/MethodologyVersionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/MethodologyVersionTests.cs @@ -158,17 +158,33 @@ public void GetTitle_AlternativeTitleSet() } [Fact] - public void GetSlug() + public void GetSlug_OwningPublicationSlug() { - var methodology = new MethodologyVersion + var methodologyVersion = new MethodologyVersion { + AlternativeSlug = null, Methodology = new Methodology { - Slug = "owning-publication-slug" - } + OwningPublicationSlug = "owning-publication-slug", + }, + }; + + Assert.Equal(methodologyVersion.Methodology.OwningPublicationSlug, methodologyVersion.Slug); + } + + [Fact] + public void GetSlug_AlternativeSlug() + { + var methodologyVersion = new MethodologyVersion + { + AlternativeSlug = "alternativeSlug", + Methodology = new Methodology + { + OwningPublicationSlug = "owning-publication-slug", + }, }; - Assert.Equal(methodology.Methodology.Slug, methodology.Slug); + Assert.Equal(methodologyVersion.AlternativeSlug, methodologyVersion.Slug); } [Fact] diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/MethodologyVersionRepositoryTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/MethodologyVersionRepositoryTests.cs index f1e4a039fd7..beb6421243c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/MethodologyVersionRepositoryTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Repository/MethodologyVersionRepositoryTests.cs @@ -854,6 +854,147 @@ public async Task GetLatestPublishedVersionByPublication_PublicationHasNoMethodo } } + [Fact] + public async Task GetLatestPublishedVersionBySlug_AlternativeSlug() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "not-like-this", + Versions = new List + { + new() + { + AlternativeSlug = "not-like-this", + Version = 1, + PreviousVersionId = latestPublishedVersionId, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = "slug", + Version = 0, + }, + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.SaveChangesAsync(); + } + + var methodologyRepository = new Mock(Strict); + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = BuildMethodologyVersionRepository( + contentDbContext: contentDbContext); + + var result = await service + .GetLatestPublishedVersionBySlug("slug"); + + VerifyAllMocks(methodologyRepository); + + Assert.NotNull(result); + Assert.Equal(latestPublishedVersionId, result.Id); + Assert.Equal("slug", result.Slug); + } + } + + [Fact] + public async Task GetLatestPublishedVersionBySlug_OwningPublicationSlug() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "slug", + Versions = new List + { + new() + { + AlternativeSlug = "not-like-this", + Version = 1, + PreviousVersionId = latestPublishedVersionId, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = null, + Version = 0, + }, + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.SaveChangesAsync(); + } + + var methodologyRepository = new Mock(Strict); + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = BuildMethodologyVersionRepository( + contentDbContext: contentDbContext); + + var result = await service + .GetLatestPublishedVersionBySlug("slug"); + + VerifyAllMocks(methodologyRepository); + + Assert.NotNull(result); + Assert.Equal(latestPublishedVersionId, result.Id); + Assert.Null(result.AlternativeSlug); + // doesn't return result.Methodology, so cannot check result.Slug/result.Methodology.OwningPublicationSlug + } + } + + [Fact] + public async Task GetLatestPublishedVersionBySlug_UnpublishedMethodology() + { + var methodology = new Methodology + { + LatestPublishedVersionId = null, + OwningPublicationSlug = "owning-publication-slug", + Versions = new List + { + new() + { + AlternativeSlug = "alternative-slug", + Version = 0, + }, + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.SaveChangesAsync(); + } + + var methodologyRepository = new Mock(Strict); + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = BuildMethodologyVersionRepository( + contentDbContext: contentDbContext); + + var result = await service + .GetLatestPublishedVersionBySlug("slug"); + + VerifyAllMocks(methodologyRepository); + + Assert.Null(result); + } + } + [Fact] public async Task IsToBePublished_ApprovedAndPublishedImmediately() { @@ -1451,272 +1592,6 @@ await Assert.ThrowsAsync(() => service.IsToBePublished(methodologyVersion)); } } - - [Fact] - public async Task PublicationTitleChanged() - { - var publicationId = Guid.NewGuid(); - - var contentDbContextId = Guid.NewGuid().ToString(); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = new PublicationMethodology - { - PublicationId = publicationId, - Owner = true, - Methodology = new Methodology - { - Versions = ListOf(new MethodologyVersion - { - Status = Draft - }), - Slug = "original-slug", - OwningPublicationTitle = "Original Title" - } - }; - - await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); - await contentDbContext.SaveChangesAsync(); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildMethodologyVersionRepository(contentDbContext); - await service.PublicationTitleChanged(publicationId, "original-slug", "New Title", "new-slug"); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = await contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .SingleAsync(m => m.PublicationId == publicationId); - - // As the Publication's Title and Slug changed and as this Methodology is not yet publicly accessible - // and is still inheriting the Publication's Slug at this point, this change will be reflected in the - // Methodology's Slug also. - Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); - Assert.Equal("new-slug", publicationMethodology.Methodology.Slug); - } - } - - [Fact] - public async Task PublicationTitleChanged_DoesNotAffectUnrelatedMethodologies() - { - var publicationId = Guid.NewGuid(); - var unrelatedPublicationId = Guid.NewGuid(); - - var contentDbContextId = Guid.NewGuid().ToString(); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = new PublicationMethodology - { - PublicationId = publicationId, - Owner = true, - Methodology = new Methodology - { - Versions = ListOf(new MethodologyVersion - { - Status = Draft - }), - Slug = "original-slug", - OwningPublicationTitle = "Original Title" - } - }; - - var unrelatedPublicationMethodology = new PublicationMethodology - { - PublicationId = unrelatedPublicationId, - Owner = true, - Methodology = new Methodology - { - Versions = ListOf(new MethodologyVersion - { - Status = Draft - }), - Slug = "original-slug", - OwningPublicationTitle = "Original Title" - } - }; - - await contentDbContext.PublicationMethodologies.AddRangeAsync( - publicationMethodology, - unrelatedPublicationMethodology); - await contentDbContext.SaveChangesAsync(); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildMethodologyVersionRepository(contentDbContext); - await service.PublicationTitleChanged(publicationId, "original-slug", "New Title", "new-slug"); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = await contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .SingleAsync(m => m.PublicationId == unrelatedPublicationId); - - // This Methodology was not related to the Publication being updated, and so was not affected by the update. - Assert.Equal("Original Title", publicationMethodology.Methodology.OwningPublicationTitle); - Assert.Equal("original-slug", publicationMethodology.Methodology.Slug); - } - } - - [Fact] - public async Task PublicationTitleChanged_DoesNotAffectUnownedMethodologies() - { - var publicationId = Guid.NewGuid(); - - var contentDbContextId = Guid.NewGuid().ToString(); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = new PublicationMethodology - { - PublicationId = publicationId, - Owner = false, - Methodology = new Methodology - { - Versions = ListOf(new MethodologyVersion - { - Status = Draft - }), - Slug = "original-slug", - OwningPublicationTitle = "Original Title" - } - }; - - await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); - await contentDbContext.SaveChangesAsync(); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildMethodologyVersionRepository(contentDbContext); - await service.PublicationTitleChanged(publicationId, "original-slug", "New Title", "new-slug"); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = await contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .SingleAsync(m => m.PublicationId == publicationId); - - // This Methodology was not owned by the Publication being updated, and so was not affected by the update. - Assert.Equal("Original Title", publicationMethodology.Methodology.OwningPublicationTitle); - Assert.Equal("original-slug", publicationMethodology.Methodology.Slug); - } - } - - [Fact] - public async Task PublicationTitleChanged_MethodologySlugHasAlreadyBeenAmendedByAlternativeTitle() - { - var publicationId = Guid.NewGuid(); - - var contentDbContextId = Guid.NewGuid().ToString(); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = new PublicationMethodology - { - PublicationId = publicationId, - Owner = true, - Methodology = new Methodology - { - Versions = ListOf(new MethodologyVersion - { - Status = Draft - }), - Slug = "alternative-slug", - OwningPublicationTitle = "Original title" - } - }; - - await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); - await contentDbContext.SaveChangesAsync(); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildMethodologyVersionRepository(contentDbContext); - await service.PublicationTitleChanged(publicationId, "original-slug", "New Title", "new-slug"); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = await contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .SingleAsync(m => m.PublicationId == publicationId); - - // The Methodology has already had an alternative Slug set by virtue of one of its Versions' - // Alternative Titles being updated, and so changing the Publication's Title and Slug won't override - // this more specific Slug change. - Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); - Assert.Equal("alternative-slug", publicationMethodology.Methodology.Slug); - } - } - - [Fact] - public async Task PublicationTitleChanged_MethodologyAlreadyPubliclyAvailable() - { - var publicationId = Guid.NewGuid(); - - var contentDbContextId = Guid.NewGuid().ToString(); - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var methodologyVersionId = Guid.NewGuid(); - var publicationMethodology = new PublicationMethodology - { - Publication = new Publication - { - Id = publicationId, - LatestPublishedRelease = new Release() - }, - Owner = true, - Methodology = new Methodology - { - LatestPublishedVersionId = Guid.NewGuid(), - Versions = ListOf(new MethodologyVersion - { - Id = methodologyVersionId, - Status = Approved, - PublishingStrategy = Immediately, - }), - Slug = "original-slug", - OwningPublicationTitle = "Original title" - } - }; - - await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); - await contentDbContext.SaveChangesAsync(); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var service = BuildMethodologyVersionRepository(contentDbContext); - await service.PublicationTitleChanged(publicationId, "original-slug", "New Title", "new-slug"); - } - - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) - { - var publicationMethodology = await contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .SingleAsync(m => m.PublicationId == publicationId); - - Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); - // The Methodology has LatestPublishedVersionId set - i.e. is live - and so its Slug does not update. - Assert.Equal("original-slug", publicationMethodology.Methodology.Slug); - } - } - private static MethodologyVersionRepository BuildMethodologyVersionRepository( ContentDbContext contentDbContext, IMethodologyRepository? methodologyRepository = null) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs index 7dae7869725..46776607b02 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Database/ContentDbContext.cs @@ -74,6 +74,7 @@ private void Configure(bool updateTimestamps = true) public virtual DbSet Permalinks { get; set; } = null!; public virtual DbSet Contacts { get; set; } public virtual DbSet Update { get; set; } + public virtual DbSet MethodologyRedirects { get; set; } public virtual DbSet Users { get; set; } public virtual DbSet UserPublicationRoles { get; set; } public virtual DbSet UserReleaseRoles { get; set; } @@ -470,6 +471,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasIndex(data => data.SubjectId); + modelBuilder.Entity() + .HasKey(mr => new { mr.MethodologyVersionId, mr.Slug }); + modelBuilder.Entity(); modelBuilder.Entity() @@ -560,4 +564,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .ToTable("KeyStatisticsText"); } } -} \ No newline at end of file +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Methodology.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Methodology.cs index 55168104389..fa8971a4df2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Methodology.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Methodology.cs @@ -13,11 +13,11 @@ public class Methodology public List Publications { get; set; } = new(); - [Required] - public string OwningPublicationTitle { get; set; } + [Required] public string OwningPublicationTitle { get; set; } = null!; - [Required] - public string Slug { get; set; } + [Required] public string OwningPublicationSlug { get; set; } = null!; + + [Required] public string Slug { get; set; } = null!; // TODO: Remove in EES-4627 public List Versions { get; set; } = new(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyRedirect.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyRedirect.cs new file mode 100644 index 00000000000..e86cb3a8b78 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyRedirect.cs @@ -0,0 +1,16 @@ +#nullable enable +using System; +using GovUk.Education.ExploreEducationStatistics.Common.Model; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model; + +public class MethodologyRedirect : ICreatedTimestamp +{ + public string Slug { get; init; } = null!; + + public Guid MethodologyVersionId { get; init; } + + public MethodologyVersion MethodologyVersion { get; init; } = null!; + + public DateTime Created { get; set; } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyVersion.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyVersion.cs index eb9b869f70e..ffbe1905561 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyVersion.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/MethodologyVersion.cs @@ -29,7 +29,9 @@ public class MethodologyVersion public string? AlternativeTitle { get; set; } - public string Slug => Methodology.Slug; + public string Slug => AlternativeSlug ?? Methodology.OwningPublicationSlug; + + public string? AlternativeSlug { get; set; } public MethodologyApprovalStatus Status { get; set; } @@ -142,5 +144,3 @@ public MethodologyVersionContent Clone(DateTime createdDate) } } } - - diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IMethodologyVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IMethodologyVersionRepository.cs index e6a01f2d452..b43f36e0a3e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IMethodologyVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/Interfaces/IMethodologyVersionRepository.cs @@ -22,7 +22,5 @@ public interface IMethodologyVersionRepository Task IsLatestPublishedVersion(MethodologyVersion methodologyVersion); Task IsToBePublished(MethodologyVersion methodologyVersion); - - Task PublicationTitleChanged(Guid publicationId, string originalSlug, string updatedTitle, string updatedSlug); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/MethodologyVersionRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/MethodologyVersionRepository.cs index 23564a52dd4..037d86cc6b5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/MethodologyVersionRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model/Repository/MethodologyVersionRepository.cs @@ -35,8 +35,9 @@ public async Task CreateMethodologyForPublication(Guid publi PublishingStrategy = Immediately, Methodology = new Methodology { - Slug = publication.Slug, + Slug = publication.Slug, // Remove EES-4627 OwningPublicationTitle = publication.Title, + OwningPublicationSlug = publication.Slug, Publications = new List { new() @@ -87,74 +88,39 @@ await _contentDbContext.Entry(methodology) return await _contentDbContext .MethodologyVersions .Where(mv => - mv.Methodology.Slug == slug - && mv.Methodology.LatestPublishedVersionId == mv.Id) + mv.Methodology.LatestPublishedVersionId == mv.Id + // EF cannot translate mv.Slug into a Queryable, so we have to do this... + && slug == (mv.AlternativeSlug ?? mv.Methodology.OwningPublicationSlug)) .SingleOrDefaultAsync(); } public async Task GetLatestPublishedVersion(Guid methodologyId) { - var methodology = await _contentDbContext.Methodologies + return await _contentDbContext.Methodologies + .Include(m => m.LatestPublishedVersion) .AsQueryable() - .SingleAsync(mp => mp.Id == methodologyId); - - return await GetLatestPublishedByMethodology(methodology); + .Where(mp => mp.Id == methodologyId) + .Select(m => m.LatestPublishedVersion) + .SingleAsync(); } public async Task> GetLatestPublishedVersionByPublication(Guid publicationId) { var methodologies = await _methodologyRepository.GetByPublication(publicationId); - return (await methodologies - .SelectAsync(async methodology => - await GetLatestPublishedByMethodology(methodology))) - .WhereNotNull() - .ToList(); - } - - private async Task GetLatestPublishedByMethodology(Methodology methodology) - { - await _contentDbContext.Entry(methodology) - .Collection(m => m.Versions) - .LoadAsync(); - - return methodology - .Versions - .SingleOrDefault(mv => methodology.LatestPublishedVersionId == mv.Id); - } - // This method is responsible for keeping Methodology Titles and Slugs in sync with their owning Publications - // where appropriate. Methodologies always keep track of their owning Publication's title for - // optimisation purposes, but Methodology.Slug is used for the actual Slug for all of its Methodology - // Versions. It's therefore important to keep it up-to-date with changes to its owning Publication's Slug too, - // but only if none of its Versions are yet publicly accessible. - public async Task PublicationTitleChanged(Guid publicationId, string originalSlug, string updatedTitle, - string updatedSlug) - { - var slugChanged = originalSlug != updatedSlug; - - // If the Publication Title changed, also change the OwningPublicationTitles of any Methodologies - // that are owned by this Publication - var ownedMethodologies = await _contentDbContext - .PublicationMethodologies - .Include(m => m.Methodology) - .Where(m => m.PublicationId == publicationId && m.Owner) - .Select(m => m.Methodology) - .ToListAsync(); - - ownedMethodologies - .ForEach(methodology => - { - methodology.OwningPublicationTitle = updatedTitle; - - if (slugChanged && methodology.Slug == originalSlug && - methodology.LatestPublishedVersionId == null) + var methodologyVersions = await methodologies + .SelectAsync(async methodology => { - methodology.Slug = updatedSlug; - } - }); + await _contentDbContext.Entry(methodology) + .Reference(m => m.LatestPublishedVersion) + .LoadAsync(); - _contentDbContext.Methodologies.UpdateRange(ownedMethodologies); - await _contentDbContext.SaveChangesAsync(); + return methodology.LatestPublishedVersion; + }); + + return methodologyVersions + .WhereNotNull() + .ToList(); } public async Task IsLatestPublishedVersion(MethodologyVersion methodologyVersion) @@ -166,7 +132,7 @@ await _contentDbContext.Entry(methodologyVersion) return methodologyVersion.Id == methodologyVersion.Methodology.LatestPublishedVersionId; } - // TODO: Move IsToBePublished to MethodologyApprovalService and change to private after MethodologyMigrationController has been removed + // TODO: EES-4613 Move IsToBePublished to MethodologyApprovalService and change to private after MethodologyMigrationController has been removed public async Task IsToBePublished(MethodologyVersion methodologyVersion) { // A version that's not approved can't be publicly accessible @@ -243,7 +209,7 @@ await _contentDbContext.Entry(methodologyVersion.Methodology) .Collection(mp => mp.Versions) .LoadAsync(); - // TODO EES-2672 SingleOrDefault here is susceptible to bug EES-2672 which is allowing multiple amendments + // TODO EES-4628 SingleOrDefault here is susceptible to bug EES-2672 which is allowing multiple amendments // of the same version to be created. If there is a next version there should only be one. return methodologyVersion.Methodology.Versions.SingleOrDefault(mv => mv.PreviousVersionId == methodologyVersion.Id); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/MethodologyServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/MethodologyServiceTests.cs index 9ca13214642..c6649f4e9f0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/MethodologyServiceTests.cs @@ -10,8 +10,10 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.Mappings; using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.EntityFrameworkCore; using Moq; using Xunit; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; @@ -27,17 +29,21 @@ public class MethodologyServiceTests [Fact] public async Task GetLatestMethodologyBySlug() { + var methodologyVersionId = Guid.NewGuid(); var methodology = new Methodology { - Slug = "methodology-slug", - OwningPublicationTitle = "Methodology title", + OwningPublicationSlug = "publication-title", + OwningPublicationTitle = "Publication title", + LatestPublishedVersionId = methodologyVersionId, Versions = new List { new() { + Id = methodologyVersionId, PublishingStrategy = Immediately, Published = DateTime.UtcNow, Status = Approved, + AlternativeSlug = "alternative-title", AlternativeTitle = "Alternative title", } } @@ -45,7 +51,7 @@ public async Task GetLatestMethodologyBySlug() var publication = new Publication { - Slug = "publication-slug", + Slug = "publication-title", Title = "Publication title", LatestPublishedReleaseId = Guid.NewGuid(), Methodologies = new List @@ -67,25 +73,17 @@ public async Task GetLatestMethodologyBySlug() await contentDbContext.SaveChangesAsync(); } - var methodologyVersionRepository = new Mock(MockBehavior.Strict); - - methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) - .ReturnsAsync(methodology.Versions[0]); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Attach(methodology.Versions[0]); - var service = SetupMethodologyService(contentDbContext, - methodologyVersionRepository: methodologyVersionRepository.Object); + var service = SetupMethodologyService(contentDbContext); - var result = (await service.GetLatestMethodologyBySlug(methodology.Slug)).AssertRight(); - - VerifyAllMocks(methodologyVersionRepository); + var result = (await service.GetLatestMethodologyBySlug(methodology.Versions[0].Slug)).AssertRight(); Assert.Equal(methodology.Versions[0].Id, result.Id); Assert.Equal(methodology.Versions[0].Published, result.Published); - Assert.Equal(methodology.Slug, result.Slug); + Assert.Equal("alternative-title", result.Slug); Assert.Equal("Alternative title", result.Title); Assert.Empty(result.Annexes); Assert.Empty(result.Content); @@ -101,12 +99,17 @@ public async Task GetLatestMethodologyBySlug() [Fact] public async Task GetLatestMethodologyBySlug_FiltersUnpublishedPublications() { + var methodologyVersionId = Guid.NewGuid(); var methodology = new Methodology { + OwningPublicationSlug = "publication-a", + OwningPublicationTitle = "Publication A", + LatestPublishedVersionId = methodologyVersionId, Versions = new List { new() { + Id = methodologyVersionId, PublishingStrategy = Immediately, Published = DateTime.UtcNow, Status = Approved @@ -155,21 +158,14 @@ public async Task GetLatestMethodologyBySlug_FiltersUnpublishedPublications() await contentDbContext.SaveChangesAsync(); } - var methodologyVersionRepository = new Mock(MockBehavior.Strict); - - methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) - .ReturnsAsync(methodology.Versions[0]); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Attach(methodology.Versions[0]); - var service = SetupMethodologyService(contentDbContext, - methodologyVersionRepository: methodologyVersionRepository.Object); + var service = SetupMethodologyService(contentDbContext); - var result = (await service.GetLatestMethodologyBySlug(methodology.Slug)).AssertRight(); - VerifyAllMocks(methodologyVersionRepository); + var result = (await service.GetLatestMethodologyBySlug(methodology.Versions[0].Slug)).AssertRight(); Assert.Single(result.Publications); Assert.Equal(publicationA.Id, result.Publications[0].Id); @@ -181,15 +177,17 @@ public async Task GetLatestMethodologyBySlug_FiltersUnpublishedPublications() [Fact] public async Task GetLatestMethodologyBySlug_TestContentSections() { + var methodologyVersionId = Guid.NewGuid(); var methodology = new Methodology { - Slug = "methodology-slug", + OwningPublicationSlug = "methodology-title", OwningPublicationTitle = "Methodology title", + LatestPublishedVersionId = methodologyVersionId, Versions = new List { new() { - Id = Guid.NewGuid(), + Id = methodologyVersionId, MethodologyContent = new MethodologyVersionContent { Annexes = new List @@ -243,7 +241,8 @@ public async Task GetLatestMethodologyBySlug_TestContentSections() }, PublishingStrategy = Immediately, Status = Approved, - AlternativeTitle = "Alternative title" + AlternativeTitle = "Alternative title", + AlternativeSlug = "alternative-title", } } }; @@ -257,21 +256,13 @@ public async Task GetLatestMethodologyBySlug_TestContentSections() await contentDbContext.SaveChangesAsync(); } - var methodologyVersionRepository = new Mock(MockBehavior.Strict); - - methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) - .ReturnsAsync(methodology.Versions[0]); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Attach(methodology.Versions[0]); - var service = SetupMethodologyService(contentDbContext, - methodologyVersionRepository: methodologyVersionRepository.Object); + var service = SetupMethodologyService(contentDbContext); - var result = (await service.GetLatestMethodologyBySlug(methodology.Slug)).AssertRight(); - - VerifyAllMocks(methodologyVersionRepository); + var result = (await service.GetLatestMethodologyBySlug(methodology.Versions[0].Slug)).AssertRight(); Assert.Equal(3, result.Annexes.Count); Assert.Equal(3, result.Content.Count); @@ -307,15 +298,17 @@ private static void AssertContentSectionAndViewModelEqual( [Fact] public async Task GetLatestMethodologyBySlug_TestNotes() { + var methodologyVersionId = Guid.NewGuid(); var methodology = new Methodology { - Slug = "methodology-slug", + OwningPublicationSlug = "methodology-slug", OwningPublicationTitle = "Methodology title", + LatestPublishedVersionId = methodologyVersionId, Versions = new List { new() { - Id = Guid.NewGuid(), + Id = methodologyVersionId, PublishingStrategy = Immediately, Status = Approved, AlternativeTitle = "Alternative title" @@ -354,21 +347,14 @@ public async Task GetLatestMethodologyBySlug_TestNotes() await contentDbContext.SaveChangesAsync(); } - var methodologyVersionRepository = new Mock(MockBehavior.Strict); - - methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) - .ReturnsAsync(methodology.Versions[0]); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { contentDbContext.Attach(methodology.Versions[0]); - var service = SetupMethodologyService(contentDbContext, - methodologyVersionRepository: methodologyVersionRepository.Object); + var service = SetupMethodologyService(contentDbContext); - var result = (await service.GetLatestMethodologyBySlug(methodology.Slug)).AssertRight(); - - VerifyAllMocks(methodologyVersionRepository); + var result = (await service.GetLatestMethodologyBySlug( + methodology.Versions[0].Slug)).AssertRight(); Assert.Equal(3, result.Notes.Count); @@ -392,7 +378,7 @@ public async Task GetLatestMethodologyBySlug_MethodologyHasNoPublishedVersion() { var methodology = new Methodology { - Slug = "methodology-slug", + OwningPublicationSlug = "methodology-title", OwningPublicationTitle = "Methodology title" }; @@ -404,19 +390,12 @@ public async Task GetLatestMethodologyBySlug_MethodologyHasNoPublishedVersion() await contentDbContext.SaveChangesAsync(); } - var methodologyVersionRepository = new Mock(MockBehavior.Strict); - - methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) - .ReturnsAsync((MethodologyVersion?) null); - await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) { - var service = SetupMethodologyService(contentDbContext, - methodologyVersionRepository: methodologyVersionRepository.Object); - - var result = await service.GetLatestMethodologyBySlug(methodology.Slug); + var service = SetupMethodologyService(contentDbContext); - VerifyAllMocks(methodologyVersionRepository); + var result = await service + .GetLatestMethodologyBySlug(methodology.OwningPublicationSlug); result.AssertNotFound(); } @@ -428,8 +407,7 @@ public async Task GetLatestMethodologyBySlug_SlugNotFound() // Set up a methodology with a different slug to make sure it's not returned var methodology = new Methodology { - Slug = "some-other-slug", - OwningPublicationTitle = "Methodology title" + OwningPublicationTitle = "Methodology title", }; var contentDbContextId = Guid.NewGuid().ToString(); @@ -477,33 +455,37 @@ public async Task GetSummariesTree() Topics = ListOf(topic) }; + var methodologyVersion1Id = Guid.NewGuid(); + var methodologyVersion2Id = Guid.NewGuid(); var latestVersions = ListOf( new MethodologyVersion { - Id = Guid.NewGuid(), + Id = methodologyVersion1Id, MethodologyContent = new MethodologyVersionContent(), PreviousVersionId = null, PublishingStrategy = Immediately, Status = Approved, AlternativeTitle = "Methodology 1 v0 title", + AlternativeSlug = "methodology-1-slug", Version = 0, Methodology = new Methodology { - Slug = "methodology-1-slug", + LatestPublishedVersionId = methodologyVersion1Id, } }, new MethodologyVersion { - Id = Guid.NewGuid(), + Id = methodologyVersion2Id, MethodologyContent = new MethodologyVersionContent(), PreviousVersionId = null, PublishingStrategy = Immediately, Status = Approved, AlternativeTitle = "Methodology 2 v0 title", + AlternativeSlug = "methodology-2-slug", Version = 0, Methodology = new Methodology { - Slug = "methodology-2-slug" + LatestPublishedVersionId = methodologyVersion2Id, } }); @@ -697,17 +679,565 @@ public async Task GetSummariesTree_ThemeWithoutPublishedMethodologiesIsNotInclud VerifyAllMocks(methodologyVersionRepository); } + [Fact] + public async Task PublicationTitleOrSlugChanged() + { + var publicationId = Guid.NewGuid(); + var originalVersionId = Guid.NewGuid(); + var latestPublishedVersionId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + PublicationId = publicationId, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationTitle = "Original Title", + OwningPublicationSlug = "original-slug", + Versions = new List + { + new() + { + Id = originalVersionId, + Version = 0, + }, + new() + { + Id = latestPublishedVersionId, + Version = 1, + PreviousVersionId = originalVersionId, + }, + }, + }, + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology.Versions) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + // As no AlternativeTitle or AlternativeSlug set, the MethodologyVersion's title and slug will also change + Assert.Equal("New Title", publicationMethodology.Methodology.Versions[1].Title); + Assert.Equal("new-slug", publicationMethodology.Methodology.Versions[1].Slug); + + // As methodology is published and it's slug has changed, a redirect is created for LatestPublishedVersion + var methodologyRedirects = await contentDbContext.MethodologyRedirects + .ToListAsync(); + var methodologyRedirect = Assert.Single(methodologyRedirects); + Assert.Equal(latestPublishedVersionId, methodologyRedirect.MethodologyVersionId); + Assert.Equal("original-slug", methodologyRedirect.Slug); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_NoMethodologyRedirectAsMethodologyUnpublished() + { + var publicationId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + PublicationId = publicationId, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = null, + OwningPublicationTitle = "Original Title", + OwningPublicationSlug = "original-slug", + Versions = ListOf(new MethodologyVersion()), + }, + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, + "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology.Versions) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + // As the Publication's Title and Slug changed, and no AlternateTitle/Slug set, + // the methodology's title and slug will also change + Assert.Equal("New Title", publicationMethodology.Methodology.Versions[0].Title); + Assert.Equal("new-slug", publicationMethodology.Methodology.Versions[0].Slug); + + // Methodology is unpublished, so no redirect + var methodologyRedirects = await contentDbContext.MethodologyRedirects + .ToListAsync(); + Assert.Empty(methodologyRedirects); + } + } + + + + [Fact] + public async Task PublicationTitleOrSlugChanged_DoesNotAffectUnrelatedMethodologies() + { + var publicationId = Guid.NewGuid(); + var unrelatedPublicationId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + PublicationId = publicationId, + Owner = true, + Methodology = new Methodology + { + Versions = ListOf(new MethodologyVersion + { + Status = Draft + }), + OwningPublicationTitle = "Original Title", + OwningPublicationSlug = "original-slug", + } + }; + + var unrelatedPublicationMethodology = new PublicationMethodology + { + PublicationId = unrelatedPublicationId, + Owner = true, + Methodology = new Methodology + { + Versions = ListOf(new MethodologyVersion + { + Status = Draft + }), + OwningPublicationTitle = "Original Title", + OwningPublicationSlug = "original-slug", + } + }; + + await contentDbContext.PublicationMethodologies.AddRangeAsync( + publicationMethodology, + unrelatedPublicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology) + .SingleAsync(m => m.PublicationId == unrelatedPublicationId); + + // This Methodology was not related to the Publication being updated, and so was not affected by the update. + Assert.Equal("Original Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("original-slug", publicationMethodology.Methodology.OwningPublicationSlug); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_DoesNotAffectUnownedMethodologies() + { + var publicationId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + PublicationId = publicationId, + Owner = false, + Methodology = new Methodology + { + Versions = ListOf(new MethodologyVersion()), + OwningPublicationTitle = "Original Title", + OwningPublicationSlug = "original-slug", + } + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(contentDbContext); + await service.PublicationTitleOrSlugChanged(publicationId, + "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology) + .SingleAsync(m => m.PublicationId == publicationId); + + // This Methodology was not owned by the Publication being updated, and so was not affected by the update. + Assert.Equal("Original Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("original-slug", publicationMethodology.Methodology.OwningPublicationSlug); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_MethodologySlugHasAlreadyBeenAmended() + { + var publicationId = Guid.NewGuid(); + var latestPublishedVersionId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + PublicationId = publicationId, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationTitle = "Original title", + OwningPublicationSlug = "original-slug", + Versions = ListOf(new MethodologyVersion + { + Id = latestPublishedVersionId, + AlternativeSlug = "alternative-slug", + }), + } + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology) + .ThenInclude(m => m.Versions) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + // The MethodologyVersion has already had an AlternativeSlug set. It doesn't have a AlternativeTitle + // set. So the MethodologyVersion title is updated, but the slug remains the same. + Assert.Equal("New Title", publicationMethodology.Methodology.Versions[0].Title); + Assert.Equal("alternative-slug", publicationMethodology.Methodology.Versions[0].Slug); + + // No redirect created as slug hasn't changed + var methodologyRedirects = await contentDbContext.MethodologyRedirects + .ToListAsync(); + Assert.Empty(methodologyRedirects); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_MethodologyAlreadyPubliclyAvailable() + { + var publicationId = Guid.NewGuid(); + var latestPublishedVersionId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + Publication = new Publication + { + Id = publicationId, + LatestPublishedRelease = new Release() + }, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + Versions = ListOf(new MethodologyVersion + { + Id = latestPublishedVersionId, + }), + OwningPublicationTitle = "Original title", + OwningPublicationSlug = "original-slug", + } + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + var redirect = await contentDbContext + .MethodologyRedirects + .SingleAsync(); + + Assert.Equal(latestPublishedVersionId, redirect.MethodologyVersionId); + Assert.Equal("original-slug", redirect.Slug); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_NewMethodologyRedirectSlugMatchesExistingRedirectSlug() + { + var publicationId = Guid.NewGuid(); + var latestPublishedVersionId = Guid.NewGuid(); + var amendmentVersionId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + Publication = new Publication + { + Id = publicationId, + LatestPublishedRelease = new Release() + }, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + Versions = new List + { + new () + { + Id = latestPublishedVersionId, + Version = 0, + }, + new () + { + Id = amendmentVersionId, + AlternativeSlug = "newer-slug", + Version = 1, + PreviousVersionId = latestPublishedVersionId, + }, + }, + OwningPublicationTitle = "Original title", + OwningPublicationSlug = "original-slug", + } + }; + + var methodologyRedirect = new MethodologyRedirect + { + MethodologyVersion = publicationMethodology.Methodology.Versions[1], + Slug = "original-slug", + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.MethodologyRedirects.AddAsync(methodologyRedirect); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", + "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology.Versions) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + var redirects = await contentDbContext + .MethodologyRedirects + .ToListAsync(); + + // We remove the now redundant redirect from the unpublished amendment. This is necessary because + // if the unpublished amendment's slug is changed again from "newer-slug" to "even-newer-slug", no + // new redirect will be created, as the amendment already has a redirect from "original-slug". This + // would mean that when the amendment is published, and the methodology's slug changes to + // "even-newer-slug", there will be no redirect from "new-slug". + var redirect = Assert.Single(redirects); + + Assert.Equal(latestPublishedVersionId, redirect.MethodologyVersionId); + Assert.Equal("original-slug", redirect.Slug); + } + } + + [Fact] + public async Task PublicationTitleOrSlugChanged_NoDuplicateMethodologyRedirectAllowed() + { + var publicationId = Guid.NewGuid(); + var latestPublishedVersionId = Guid.NewGuid(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = new PublicationMethodology + { + Publication = new Publication + { + Id = publicationId, + LatestPublishedRelease = new Release() + }, + Owner = true, + Methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + Versions = new List + { + new () + { + Id = latestPublishedVersionId, + Version = 0, + }, + }, + OwningPublicationTitle = "Original title", + OwningPublicationSlug = "original-slug", + } + }; + + var methodologyRedirect = new MethodologyRedirect + { + MethodologyVersion = publicationMethodology.Methodology.Versions[0], + // The current methodology slug could match a redirect if a user changes via publication slug + // multiple times + Slug = "original-slug", + }; + + await contentDbContext.PublicationMethodologies.AddAsync(publicationMethodology); + await contentDbContext.MethodologyRedirects.AddAsync(methodologyRedirect); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsCacheService = new Mock(MockBehavior.Strict); + redirectsCacheService.Setup(mock => mock.UpdateRedirects()) + .ReturnsAsync(new RedirectsViewModel(new List())); + + var service = SetupMethodologyService(contentDbContext, + redirectsCacheService: redirectsCacheService.Object); + await service.PublicationTitleOrSlugChanged(publicationId, "original-slug", + "New Title", "new-slug"); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var publicationMethodology = await contentDbContext + .PublicationMethodologies + .Include(m => m.Methodology.Versions) + .SingleAsync(m => m.PublicationId == publicationId); + + Assert.Equal("New Title", publicationMethodology.Methodology.OwningPublicationTitle); + Assert.Equal("new-slug", publicationMethodology.Methodology.OwningPublicationSlug); + + var redirects = await contentDbContext + .MethodologyRedirects + .ToListAsync(); + + var redirect = Assert.Single(redirects); + + Assert.Equal(latestPublishedVersionId, redirect.MethodologyVersionId); + Assert.Equal("original-slug", redirect.Slug); + } + } + private static MethodologyService SetupMethodologyService( ContentDbContext contentDbContext, IPersistenceHelper? contentPersistenceHelper = null, IMethodologyVersionRepository? methodologyVersionRepository = null, - IMapper? mapper = null) + IMapper? mapper = null, + IRedirectsCacheService? redirectsCacheService = null) { return new( contentDbContext, contentPersistenceHelper ?? new PersistenceHelper(contentDbContext), mapper ?? MapperUtils.MapperForProfile(), - methodologyVersionRepository ?? Mock.Of(MockBehavior.Strict) + methodologyVersionRepository ?? Mock.Of(MockBehavior.Strict), + redirectsCacheService ?? Mock.Of(MockBehavior.Strict) ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/RedirectsServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/RedirectsServiceTests.cs new file mode 100644 index 00000000000..5e6edb3c9e2 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services.Tests/RedirectsServiceTests.cs @@ -0,0 +1,403 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils.ContentDbUtils; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.Tests +{ + public class RedirectsServiceTests + { + [Fact] + public async Task List_MethodologyRedirect() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "no-redirect-to-1", + Versions = new List + { + new() + { + // previous version + AlternativeSlug = "no-redirect-to-2", + Version = 0, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = "redirect-to", + Version = 1, + }, + new() + { + // latestVersion but unpublished + AlternativeSlug = "no-redirect-to-3", + Version = 2, + }, + } + }; + + var methodologyRedirects = new List + { + new() + { + Slug = "redirect-from-2", + MethodologyVersion = methodology.Versions[0], + }, + new() + { + Slug = "redirect-from-1", + MethodologyVersion = methodology.Versions[1], + }, + new() + { + Slug = "no-redirect-from-1", + MethodologyVersion = methodology.Versions[2], + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.MethodologyRedirects.AddRangeAsync(methodologyRedirects); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + var methodologyRedirectViewModel = viewModel.Methodologies; + + Assert.Equal(2, methodologyRedirectViewModel.Count); + + Assert.Equal("redirect-from-2", methodologyRedirectViewModel[0].FromSlug); + Assert.Equal("redirect-to", methodologyRedirectViewModel[0].ToSlug); + + Assert.Equal("redirect-from-1", methodologyRedirectViewModel[1].FromSlug); + Assert.Equal("redirect-to", methodologyRedirectViewModel[1].ToSlug); + } + } + + [Fact] + public async Task List_MethodologyRedirect_MethodologyNotPublished() + { + var methodology = new Methodology + { + LatestPublishedVersionId = null, + Versions = new List + { + new() + { + AlternativeSlug = "redirect-to-slug", + } + } + }; + + var methodologyRedirect = new MethodologyRedirect + { + Slug = "redirect-from-slug", + MethodologyVersion = methodology.Versions[0], + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.MethodologyRedirects.AddAsync(methodologyRedirect); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + + Assert.Empty(viewModel.Methodologies); + } + } + + [Fact] + public async Task List_MethodologyRedirect_PreviousVersionRedirectOnly() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "no-redirect-to-1", + Versions = new List + { + new() + { + // previous version + AlternativeSlug = "no-redirect-to-2", + Version = 0, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = "redirect-to", + Version = 1, + }, + } + }; + + var methodologyRedirects = new List + { + new() + { + Slug = "redirect-from-1", + MethodologyVersion = methodology.Versions[0], + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.MethodologyRedirects.AddRangeAsync(methodologyRedirects); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + var methodologyRedirectViewModel = viewModel.Methodologies; + + var redirect = Assert.Single(methodologyRedirectViewModel); + + Assert.Equal("redirect-from-1", redirect.FromSlug); + Assert.Equal("redirect-to", redirect.ToSlug); + } + } + + [Fact] + public async Task List_MethodologyRedirect_MultipleMethodologies() + { + var latestPublishedVersion1 = Guid.NewGuid(); + var methodology1 = new Methodology + { + LatestPublishedVersionId = latestPublishedVersion1, + OwningPublicationSlug = "redirect-to-slug-1", + Versions = new List + { + new() + { + Id = latestPublishedVersion1, + // No AlternativeSlug so uses OwningPublicationSlug + Version = 0, + + }, + new() + { + // LatestVersion but unpublished + AlternativeSlug = "no-redirect-to-slug-1", + Version = 1, + }, + } + }; + + var latestPublishedVersion2 = Guid.NewGuid(); + var methodology2 = new Methodology + { + LatestPublishedVersionId = latestPublishedVersion2, + OwningPublicationSlug = "no-redirect-to-slug-2", + Versions = new List + { + new() + { + Id = latestPublishedVersion2, + AlternativeSlug = "redirect-to-slug-2", + Version = 0, + + }, + new() + { + // LatestVersion but unpublished + AlternativeSlug = "no-redirect-to-slug-3", + Version = 1, + }, + }, + }; + + var methodologyRedirects = new List + { + new() + { + Slug = "redirect-from-slug-1", + MethodologyVersion = methodology1.Versions[0], + }, + new() + { + Slug = "redirect-from-slug-2", + MethodologyVersion = methodology2.Versions[0], + } + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddRangeAsync(methodology1, methodology2); + await contentDbContext.MethodologyRedirects.AddRangeAsync(methodologyRedirects); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + var methodologyRedirectsViewModel = viewModel.Methodologies; + + Assert.Equal(2, methodologyRedirectsViewModel.Count); + + Assert.Equal("redirect-from-slug-1", methodologyRedirectsViewModel[0].FromSlug); + Assert.Equal("redirect-to-slug-1", methodologyRedirectsViewModel[0].ToSlug); + + Assert.Equal("redirect-from-slug-2", methodologyRedirectsViewModel[1].FromSlug); + Assert.Equal("redirect-to-slug-2", methodologyRedirectsViewModel[1].ToSlug); + } + } + + [Fact] + public async Task List_MethodologyRedirect_FilterRedirectIfSameAsCurrentSlug() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "no-redirect-to-1", + Versions = new List + { + new() + { + // previous version + AlternativeSlug = "no-redirect-to-2", + Version = 0, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = "redirect-to", + Version = 1, + }, + } + }; + + var methodologyRedirects = new List + { + new() + { + Slug = "redirect-to", + MethodologyVersion = methodology.Versions[0], + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.MethodologyRedirects.AddRangeAsync(methodologyRedirects); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + Assert.Empty(viewModel.Methodologies); + } + } + + [Fact] + public async Task List_MethodologyRedirect_DuplicateRedirects() + { + var latestPublishedVersionId = Guid.NewGuid(); + var methodology = new Methodology + { + LatestPublishedVersionId = latestPublishedVersionId, + OwningPublicationSlug = "no-redirect-to-1", + Versions = new List + { + new() + { + // previous version + AlternativeSlug = "no-redirect-to-2", + Version = 0, + }, + new() + { + Id = latestPublishedVersionId, + AlternativeSlug = "redirect-to", + Version = 1, + }, + } + }; + + var methodologyRedirects = new List + { + new() + { + Slug = "duplicated-redirect", + MethodologyVersion = methodology.Versions[0], + }, + new() + { + Slug = "duplicated-redirect", + MethodologyVersion = methodology.Versions[1], + }, + }; + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + await contentDbContext.Methodologies.AddAsync(methodology); + await contentDbContext.MethodologyRedirects.AddRangeAsync(methodologyRedirects); + await contentDbContext.SaveChangesAsync(); + } + + await using (var contentDbContext = InMemoryContentDbContext(contentDbContextId)) + { + var redirectsService = SetupRedirectsService(contentDbContext); + + var result = await redirectsService.List(); + + var viewModel = result.AssertRight(); + + var redirect = Assert.Single(viewModel.Methodologies); + Assert.Equal("duplicated-redirect", redirect.FromSlug); + Assert.Equal("redirect-to", redirect.ToSlug); + } + } + + private static RedirectsService SetupRedirectsService( + ContentDbContext? contentDbContext = null) + { + contentDbContext ??= InMemoryContentDbContext(); + + return new(contentDbContext); + } + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheKey.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheKey.cs new file mode 100644 index 00000000000..1c3c1eeb5aa --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheKey.cs @@ -0,0 +1,13 @@ +#nullable enable +using GovUk.Education.ExploreEducationStatistics.Common; +using GovUk.Education.ExploreEducationStatistics.Common.Cache.Interfaces; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.Cache; + +public record RedirectsCacheKey : IBlobCacheKey +{ + public string Key => "redirects.json"; + + public IBlobContainer Container => BlobContainers.PublicContent; +} + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheService.cs new file mode 100644 index 00000000000..91a1ad6661b --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Cache/RedirectsCacheService.cs @@ -0,0 +1,32 @@ +#nullable enable +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Cache; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; +using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.Cache; + +public class RedirectsCacheService : IRedirectsCacheService +{ + private readonly IRedirectsService _redirectsService; + + public RedirectsCacheService(IRedirectsService redirectsService) + { + _redirectsService = redirectsService; + } + + [BlobCache(typeof(RedirectsCacheKey), ServiceName = "public")] + public async Task> List() + { + return await _redirectsService.List(); + } + + [BlobCache(typeof(RedirectsCacheKey), forceUpdate: true, ServiceName = "public")] + public Task> UpdateRedirects() + { + return _redirectsService.List(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/Cache/IRedirectsCacheService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/Cache/IRedirectsCacheService.cs new file mode 100644 index 00000000000..dec2dde8857 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/Cache/IRedirectsCacheService.cs @@ -0,0 +1,14 @@ +#nullable enable +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; + +public interface IRedirectsCacheService +{ + Task> List(); + + Task> UpdateRedirects(); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IMethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IMethodologyService.cs index 4b3619506be..c587ea5e6ab 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IMethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IMethodologyService.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Threading.Tasks; using GovUk.Education.ExploreEducationStatistics.Common.Model; @@ -12,4 +13,10 @@ public interface IMethodologyService public Task> GetLatestMethodologyBySlug(string slug); public Task>> GetSummariesTree(); + + public Task PublicationTitleOrSlugChanged( + Guid publicationId, + string originalSlug, + string updatedTitle, + string updatedSlug); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IRedirectsService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IRedirectsService.cs new file mode 100644 index 00000000000..0e772216e66 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/Interfaces/IRedirectsService.cs @@ -0,0 +1,13 @@ +#nullable enable +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.AspNetCore.Mvc; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces +{ + public interface IRedirectsService + { + Task> List(); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/MethodologyService.cs index aac55eaa837..693e6bb64ea 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/MethodologyService.cs @@ -10,6 +10,7 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -22,16 +23,19 @@ public class MethodologyService : IMethodologyService private readonly IPersistenceHelper _persistenceHelper; private readonly IMapper _mapper; private readonly IMethodologyVersionRepository _methodologyVersionRepository; + private readonly IRedirectsCacheService _redirectsCacheService; public MethodologyService(ContentDbContext contentDbContext, IPersistenceHelper persistenceHelper, IMapper mapper, - IMethodologyVersionRepository methodologyVersionRepository) + IMethodologyVersionRepository methodologyVersionRepository, + IRedirectsCacheService redirectsCacheService) { _contentDbContext = contentDbContext; _persistenceHelper = persistenceHelper; _mapper = mapper; _methodologyVersionRepository = methodologyVersionRepository; + _redirectsCacheService = redirectsCacheService; } public async Task> GetLatestMethodologyBySlug(string slug) @@ -39,12 +43,14 @@ public async Task> GetLatestMe return await _persistenceHelper .CheckEntityExists( query => query - .Where(mp => mp.Slug == slug)) + .Include(m => m.LatestPublishedVersion) + .Where(m => + m.LatestPublishedVersion != null + && slug == (m.LatestPublishedVersion.AlternativeSlug ?? m.OwningPublicationSlug))) // slug == mv.Slug doesn't translate .OnSuccess(async methodology => { - var latestPublishedVersion = - await _methodologyVersionRepository.GetLatestPublishedVersion(methodology.Id); - + var latestPublishedVersion = methodology.LatestPublishedVersion; + if (latestPublishedVersion == null) { return new NotFoundResult(); @@ -125,5 +131,83 @@ private async Task> BuildMethodologiesF await _methodologyVersionRepository.GetLatestPublishedVersionByPublication(publicationId); return _mapper.Map>(latestPublishedMethodologies); } + + // This method is responsible for keeping Methodology Titles and Slugs in sync with their owning Publications + // where appropriate. Methodologies always keep track of their owning Publication's titles and slugs for + // optimisation purposes. + public async Task PublicationTitleOrSlugChanged(Guid publicationId, string originalSlug, string updatedTitle, + string updatedSlug) + { + var slugChanged = originalSlug != updatedSlug; + + var ownedMethodology = await _contentDbContext + .PublicationMethodologies + .Include(pm => pm.Methodology.LatestPublishedVersion) + .Include(pm => pm.Methodology.Versions) + .Where(pm => pm.PublicationId == publicationId && pm.Owner) + .Select(pm => pm.Methodology) + .SingleOrDefaultAsync(); + + if (ownedMethodology == null) + { + return; + } + + ownedMethodology.OwningPublicationTitle = updatedTitle; + ownedMethodology.OwningPublicationSlug = updatedSlug; + + _contentDbContext.Methodologies.Update(ownedMethodology); + + if (slugChanged) + { + // A redirect is only needed for the LatestPublishedVersion. + // Unpublished methodologies don't need a redirect - they're not live. + // An unpublished amendment doesn't need a redirect because: + // - if it uses OwningPublicationSlug, it is covered by the LatestPublishedVersion redirect created here + // - if it uses AlternativeSlug, a redirect would have been created at the time the AlternativeSlug + // was set (and that redirect will become active when that version is published). + if (ownedMethodology.LatestPublishedVersion is { AlternativeSlug: null } + // guard against duplicates due to users making multiple publication slug changes + && !await _contentDbContext.MethodologyRedirects + .AnyAsync(mr => + mr.MethodologyVersionId == ownedMethodology.LatestPublishedVersionId + && mr.Slug == originalSlug)) + { + var redirect = new MethodologyRedirect + { + MethodologyVersion = ownedMethodology.LatestPublishedVersion, + Slug = originalSlug, + }; + _contentDbContext.MethodologyRedirects.Add(redirect); + + // It's possible we now have two redirects from the same slug. This happens if: + // - An unpublished amendment sets an AlternativeSlug + // - Then the OwningPublicationSlug changes when the LatestPublishedVersion is + // inheriting it. + // We must delete the unpublished amendment's redirect, as otherwise if the unpublished version + // changes its slug again, no redirect will be created for the methodology's new slug. (If updating + // an unpublished version's slug multiple times, a redirect is only created the first time - see + // MethodologyUpdate code). If this doesn't make any sense, see the unit test + // PublicationTitleOrSlugChanged_NewMethodologyRedirectSlugMatchesExistingRedirectSlug + var redirectToRemove = await _contentDbContext.MethodologyRedirects + .Where(mr => + mr.MethodologyVersionId == ownedMethodology.LatestVersion().Id + && mr.Slug == originalSlug) + .SingleOrDefaultAsync(); + + if (redirectToRemove != null) + { + _contentDbContext.MethodologyRedirects.Remove(redirectToRemove); + } + } + } + + await _contentDbContext.SaveChangesAsync(); + + if (slugChanged) + { + await _redirectsCacheService.UpdateRedirects(); + } + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/RedirectsService.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/RedirectsService.cs new file mode 100644 index 00000000000..fff8e4ce4e3 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/RedirectsService.cs @@ -0,0 +1,65 @@ +#nullable enable +using System.Linq; +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services; + +public class RedirectsService : IRedirectsService +{ + private readonly ContentDbContext _contentDbContext; + + public RedirectsService(ContentDbContext contentDbContext) + { + _contentDbContext = contentDbContext; + } + + public async Task> List() + { + // A redirect for a MethodologyVersion that isn't published shouldn't appear in the list. A redirect becomes + // active once the associated MethodologyVersion is published. It remains active if subsequent methodology + // amendments are published. We establish this by checking against each MethodologyVersion's Version number. + var methodologyRedirects = await _contentDbContext.MethodologyRedirects + .Where(mr => mr.MethodologyVersion.Methodology.LatestPublishedVersion != null + && mr.MethodologyVersion.Methodology.LatestPublishedVersion.Version >= + mr.MethodologyVersion.Version + ) + .Select(mr => new + { + RedirectSlug = mr.Slug, + // MethodologyVersion.Slug cannot be translated into SQL, so we do this... + LatestPublishedSlug = mr.MethodologyVersion.Methodology.LatestPublishedVersion!.AlternativeSlug + ?? mr.MethodologyVersion.Methodology.OwningPublicationSlug, + }) + .ToListAsync(); + + var methodologyRedirectViewModels = methodologyRedirects + .Select(mr => + { + if (mr.RedirectSlug == mr.LatestPublishedSlug) + { + // It is possible for a redirect to point to the currently active slug. This can happen as a + // methodology can change to a slug even if there is a redirect for that slug, + // *if* the redirect is for the same methodology - i.e. the methodology's slug is changing + // back to a previous slug it used. But the slug may not change back immediately, if the change + // is on an unpublished amendment, so we cannot remove the redirect immediately when changing the + // slug back; redirect needs to be active until the amendment is published. + return null; + } + + return new MethodologyRedirectViewModel( + FromSlug: mr.RedirectSlug, ToSlug: mr.LatestPublishedSlug); + }) + .WhereNotNull() + .Distinct() + .ToList(); + + return new RedirectsViewModel(methodologyRedirectViewModels); + } +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ViewModels/RedirectViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ViewModels/RedirectViewModels.cs new file mode 100644 index 00000000000..e3b1f4317c7 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Services/ViewModels/RedirectViewModels.cs @@ -0,0 +1,12 @@ +#nullable enable +using System.Collections.Generic; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; + +public record RedirectsViewModel( + List Methodologies); + +public record MethodologyRedirectViewModel( + string FromSlug, + string ToSlug); + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs index b98fc8e815a..f145cadf543 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/PublishingCompletionService.cs @@ -23,6 +23,7 @@ public class PublishingCompletionService : IPublishingCompletionService private readonly IPublicationRepository _publicationRepository; private readonly IPublicationCacheService _publicationCacheService; private readonly IReleaseService _releaseService; + private readonly IRedirectsCacheService _redirectsCacheService; public PublishingCompletionService(ContentDbContext contentDbContext, IContentService contentService, @@ -31,7 +32,8 @@ public PublishingCompletionService(ContentDbContext contentDbContext, IReleasePublishingStatusService releasePublishingStatusService, IPublicationRepository publicationRepository, IPublicationCacheService publicationCacheService, - IReleaseService releaseService) + IReleaseService releaseService, + IRedirectsCacheService redirectsCacheService) { _contentDbContext = contentDbContext; _contentService = contentService; @@ -41,6 +43,7 @@ public PublishingCompletionService(ContentDbContext contentDbContext, _publicationCacheService = publicationCacheService; _releaseService = releaseService; _publicationRepository = publicationRepository; + _redirectsCacheService = redirectsCacheService; } public async Task CompletePublishingIfAllPriorStagesComplete( @@ -141,6 +144,8 @@ await publicationSlugsToUpdate // are now accessible for the first time after publishing these releases await _contentService.UpdateCachedTaxonomyBlobs(); + await _redirectsCacheService.UpdateRedirects(); + await prePublishingStagesComplete .ToAsyncEnumerable() .ForEachAwaitAsync(status => _releasePublishingStatusService diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs index 4bd304df0c4..0d8e2c311fd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs @@ -11,7 +11,9 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository; using GovUk.Education.ExploreEducationStatistics.Content.Model.Repository.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Content.Services; using GovUk.Education.ExploreEducationStatistics.Content.Services.Cache; +using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.Mappings; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; @@ -32,6 +34,10 @@ using ContentPublicationService = GovUk.Education.ExploreEducationStatistics.Content.Services.PublicationService; using IContentReleaseService = GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.IReleaseService; using ContentReleaseService = GovUk.Education.ExploreEducationStatistics.Content.Services.ReleaseService; +using IMethodologyService = GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces.IMethodologyService; +using IReleaseService = GovUk.Education.ExploreEducationStatistics.Publisher.Services.Interfaces.IReleaseService; +using MethodologyService = GovUk.Education.ExploreEducationStatistics.Publisher.Services.MethodologyService; +using ReleaseService = GovUk.Education.ExploreEducationStatistics.Publisher.Services.ReleaseService; [assembly: FunctionsStartup(typeof(Startup))] @@ -108,7 +114,9 @@ public override void Configure(IFunctionsHostBuilder builder) .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped() + .AddScoped(); AddPersistenceHelper(builder.Services); AddPersistenceHelper(builder.Services); diff --git a/src/explore-education-statistics-admin/src/components/PageFooter.tsx b/src/explore-education-statistics-admin/src/components/PageFooter.tsx index 097c0a28041..d0405bb2408 100644 --- a/src/explore-education-statistics-admin/src/components/PageFooter.tsx +++ b/src/explore-education-statistics-admin/src/components/PageFooter.tsx @@ -41,6 +41,16 @@ const PageFooter = ({ wide }: Props) => { +
+ Our statistical practice is regulated by the{' '} + + Office for Statistics Regulation + {' '} + (OSR) +
{ All content is available under the{' '} - Open Government Licence v3.0 - + , except where otherwise stated @@ -77,12 +87,12 @@ const PageFooter = ({ wide }: Props) => { )} diff --git a/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx b/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx index 67f3bda2bf5..1c034b3e908 100644 --- a/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx +++ b/src/explore-education-statistics-admin/src/pages/methodology/components/MethodologySummaryForm.tsx @@ -19,7 +19,8 @@ const errorMappings = [ mapFieldErrors({ target: 'title', messages: { - SlugNotUnique: 'Choose a unique title', + SlugNotUnique: 'Used by other methodology. Choose a unique title', + SlugUsedByRedirect: 'Used by methodology redirect. Choose a unique title', }, }), ]; diff --git a/src/explore-education-statistics-admin/src/pages/release/footnotes/ReleaseFootnotesPage.tsx b/src/explore-education-statistics-admin/src/pages/release/footnotes/ReleaseFootnotesPage.tsx index 55085fa550b..8d32aea2b35 100644 --- a/src/explore-education-statistics-admin/src/pages/release/footnotes/ReleaseFootnotesPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/release/footnotes/ReleaseFootnotesPage.tsx @@ -152,7 +152,9 @@ const ReleaseFootnotesPage = ({

{!canUpdateRelease && ( -

This release has been approved, and can no longer be updated.

+ + This release has been approved, and can no longer be updated. + )} diff --git a/src/explore-education-statistics-common/src/modules/release/components/NationalStatisticsSection.tsx b/src/explore-education-statistics-common/src/modules/release/components/NationalStatisticsSection.tsx index c8d1c4ac98b..776e827f1fa 100644 --- a/src/explore-education-statistics-common/src/modules/release/components/NationalStatisticsSection.tsx +++ b/src/explore-education-statistics-common/src/modules/release/components/NationalStatisticsSection.tsx @@ -9,20 +9,25 @@ export default function NationalStatisticsSection({ <> {showHeading &&

National statistics

}

- The{' '} - - United Kingdom Statistics Authority - {' '} - designated these statistics as National Statistics in accordance with + These accredited official statistics have been independently reviewed by the{' '} + + Office for Statistics Regulation + {' '} + (OSR). They comply with the standards of trustworthiness, quality and + value in the{' '} + + Code of Practice for Statistics + + . Accredited official statistics are called National Statistics in the{' '} Statistics and Registration Service Act 2007 - {' '} - and signifying compliance with the Code of Practice for Statistics. + + .

- Designation signifying their compliance with the authority's{' '} - + Accreditation signifies their compliance with the authority's{' '} + Code of Practice for Statistics {' '} which broadly means these statistics are: @@ -34,17 +39,24 @@ export default function NationalStatisticsSection({

  • well explained and readily accessible
  • - Once designated as National Statistics it's a statutory requirement for - statistics to follow and comply with the Code of Practice for Statistics - to be observed. + Our statistical practice is regulated by the Office for Statistics + Regulation (OSR). +

    +

    + OSR sets the standards of trustworthiness, quality and value in the{' '} + + Code of Practice for Statistics + {' '} + that all producers of official statistics should adhere to.

    - Find out more about the standards we follow to produce these statistics - through our{' '} - - Standards for official statistics published by DfE + You are welcome to contact us directly with any comments about how we + meet these standards. Alternatively, you can contact OSR by emailing{' '} + + regulation@statistics.gov.uk {' '} - guidance. + or via the{' '} + OSR website.

    ); diff --git a/src/explore-education-statistics-common/src/modules/release/components/__tests__/NationalStatisticsSection.test.tsx b/src/explore-education-statistics-common/src/modules/release/components/__tests__/NationalStatisticsSection.test.tsx new file mode 100644 index 00000000000..4f7800a1ac0 --- /dev/null +++ b/src/explore-education-statistics-common/src/modules/release/components/__tests__/NationalStatisticsSection.test.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import NationalStatisticsSection from '@common/modules/release/components/NationalStatisticsSection'; + +describe('NationalStatisticsSection', () => { + test('renders', () => { + render(); + + expect( + screen.getByRole('link', { + name: 'Office for Statistics Regulation', + }), + ).toBeInTheDocument(); + }); + + test('shows the heading if showHeading is true', () => { + render(); + + expect( + screen.getByRole('heading', { name: 'National statistics' }), + ).toBeInTheDocument(); + }); + + test('hides the heading if showHeading is false', () => { + render(); + + expect( + screen.queryByRole('heading', { name: 'Official statistics' }), + ).not.toBeInTheDocument(); + }); + + test('includes the OSR guidance text', () => { + // Introduced because of changes to the National Statistics Designation Review, + // Documented in https://dfedigital.atlassian.net/browse/EES-4620 + + render(); + + expect( + screen.getByText( + 'Our statistical practice is regulated by the Office for Statistics Regulation (OSR).', + ), + ).toBeInTheDocument(); + + expect( + screen.queryAllByRole('link', { + name: 'Code of Practice for Statistics', + })[0], + ).toHaveAttribute( + 'href', + 'https://code.statisticsauthority.gov.uk/the-code/', + ); + + expect( + screen.getByRole('link', { name: 'regulation@statistics.gov.uk' }), + ).toHaveAttribute('href', 'mailto:regulation@statistics.gov.uk'); + + expect(screen.getByRole('link', { name: 'OSR website' })).toHaveAttribute( + 'href', + 'https://osr.statisticsauthority.gov.uk/', + ); + }); +}); diff --git a/src/explore-education-statistics-frontend/.env b/src/explore-education-statistics-frontend/.env index 4362a4ceaef..b4ed931ef00 100644 --- a/src/explore-education-statistics-frontend/.env +++ b/src/explore-education-statistics-frontend/.env @@ -3,4 +3,4 @@ DATA_API_BASE_URL=http://localhost:5000/api NOTIFICATION_API_BASE_URL=http://localhost:7073/api GA_TRACKING_ID= PUBLIC_URL=http://localhost:3000/ -APP_ENV=Local +APP_ENV=Local \ No newline at end of file diff --git a/src/explore-education-statistics-frontend/server.js b/src/explore-education-statistics-frontend/server.js index d77222eef91..da359b37a7c 100644 --- a/src/explore-education-statistics-frontend/server.js +++ b/src/explore-education-statistics-frontend/server.js @@ -48,7 +48,13 @@ const cspScriptSrc = [ const port = process.env.PORT || 3000; const dev = process.env.NODE_ENV !== 'production'; -const app = next({ dev }); +const url = new URL(process.env.PUBLIC_URL); + +const app = next({ + dev, + hostname: url.hostname, + port: process.env.PORT || 3000, +}); const handleRequest = app.getRequestHandler(); async function startServer() { diff --git a/src/explore-education-statistics-frontend/src/components/PageFooter.tsx b/src/explore-education-statistics-frontend/src/components/PageFooter.tsx index f72e7d1397a..07d2b719ebf 100644 --- a/src/explore-education-statistics-frontend/src/components/PageFooter.tsx +++ b/src/explore-education-statistics-frontend/src/components/PageFooter.tsx @@ -72,7 +72,16 @@ const PageFooter = ({ wide }: Props) => ( - +
    + Our statistical practice is regulated by the{' '} + + Office for Statistics Regulation + {' '} + (OSR) +
    ( All content is available under the{' '} - Open Government Licence v3.0 - + , except where otherwise stated @@ -109,12 +118,12 @@ const PageFooter = ({ wide }: Props) => ( )} diff --git a/src/explore-education-statistics-frontend/src/middleware.ts b/src/explore-education-statistics-frontend/src/middleware.ts new file mode 100644 index 00000000000..832b25f411b --- /dev/null +++ b/src/explore-education-statistics-frontend/src/middleware.ts @@ -0,0 +1,11 @@ +import redirectPages from '@frontend/middleware/pages/redirectPages'; +import type { NextRequest } from 'next/server'; + +export default async function middleware(request: NextRequest) { + return redirectPages(request); +} + +// Restrict to release and methodology pages. +export const config = { + matcher: ['/find-statistics/:path/:path*', '/methodology/:path*'], +}; diff --git a/src/explore-education-statistics-frontend/src/middleware/pages/__tests__/redirectPages.test.ts b/src/explore-education-statistics-frontend/src/middleware/pages/__tests__/redirectPages.test.ts new file mode 100644 index 00000000000..2ee2efa85e4 --- /dev/null +++ b/src/explore-education-statistics-frontend/src/middleware/pages/__tests__/redirectPages.test.ts @@ -0,0 +1,255 @@ +import _redirectService, { + Redirects, +} from '@frontend/services/redirectService'; +import redirectPages from '@frontend/middleware/pages/redirectPages'; +import { NextResponse, NextRequest } from 'next/server'; + +jest.mock('@frontend/services/redirectService'); +const redirectService = _redirectService as jest.Mocked< + typeof _redirectService +>; + +describe('redirectPages', () => { + const redirectSpy = jest.spyOn(NextResponse, 'redirect'); + const nextSpy = jest.spyOn(NextResponse, 'next'); + + const testRedirects: Redirects = { + methodologies: [ + { fromSlug: 'original-slug-1', toSlug: 'updated-slug-1' }, + { fromSlug: 'original-slug-2', toSlug: 'updated-slug-2' }, + ], + publications: [ + { fromSlug: 'original-slug-3', toSlug: 'updated-slug-3' }, + { fromSlug: 'original-slug-4', toSlug: 'updated-slug-4' }, + ], + }; + + test('does not re-request the list of redirects once it has been fetched', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original-slug'), + ); + await redirectPages(req); + + expect(redirectService.list).toHaveBeenCalledTimes(1); + + const req2 = new NextRequest( + new Request('https://my-env/methodology/another-slug'), + ); + await redirectPages(req2); + + expect(redirectService.list).toHaveBeenCalledTimes(1); + }); + + describe('redirect methodology pages', () => { + test('redirects the request when the slug for the requested page has changed', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original-slug-1'), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/methodology/updated-slug-1'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('does not redirect when the slug for the requested page has not changed', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/my-methodology'), + ); + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + }); + + test('does not redirect if the `fromSlug` only partially matches', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original'), + ); + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + + const req2 = new NextRequest( + new Request('https://my-env/methodology/original-slug-and-something'), + ); + await redirectPages(req2); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(2); + }); + + test('redirects child pages', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original-slug-1/child-page'), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/methodology/updated-slug-1/child-page'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('redirects with anchor links', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original-slug-1#anchor-link'), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/methodology/updated-slug-1#anchor-link'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('redirects with query params', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request( + 'https://my-env/methodology/original-slug-1?search=something', + ), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/methodology/updated-slug-1?search=something'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('does not redirect when the slug matches a `fromSlug` in a different page type', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/methodology/original-slug-4'), + ); + + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('redirect publication pages', () => { + test('redirects the request when the slug for the requested page has changed', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/find-statistics/original-slug-3'), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/find-statistics/updated-slug-3'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('does not redirect when the slug for the requested page has not changed', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/find-statistics/my-publication'), + ); + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + }); + + test('does not redirect if the `fromSlug` only partially matches', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/find-statistics/original'), + ); + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + + const req2 = new NextRequest( + new Request( + 'https://my-env/find-statistics/original-slug-and-something', + ), + ); + await redirectPages(req2); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(2); + }); + + test('redirects child pages', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request( + 'https://my-env/find-statistics/original-slug-3/child-page', + ), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/find-statistics/updated-slug-3/child-page'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('redirects with anchor links', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request( + 'https://my-env/find-statistics/original-slug-3#anchor-link', + ), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL('https://my-env/find-statistics/updated-slug-3#anchor-link'), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('redirects with query params', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request( + 'https://my-env/find-statistics/original-slug-3?search=something', + ), + ); + await redirectPages(req); + + expect(redirectSpy).toHaveBeenCalledTimes(1); + expect(redirectSpy).toHaveBeenCalledWith( + new URL( + 'https://my-env/find-statistics/updated-slug-3?search=something', + ), + ); + expect(nextSpy).not.toHaveBeenCalled(); + }); + + test('does not redirect when the slug matches a `fromSlug` in a different page type', async () => { + redirectService.list.mockResolvedValue(testRedirects); + const req = new NextRequest( + new Request('https://my-env/find-statistics/original-slug-1'), + ); + + await redirectPages(req); + + expect(redirectSpy).not.toHaveBeenCalled(); + expect(nextSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/src/explore-education-statistics-frontend/src/middleware/pages/redirectPages.ts b/src/explore-education-statistics-frontend/src/middleware/pages/redirectPages.ts new file mode 100644 index 00000000000..f83e8cf5965 --- /dev/null +++ b/src/explore-education-statistics-frontend/src/middleware/pages/redirectPages.ts @@ -0,0 +1,75 @@ +import redirectService, { + Redirects, + RedirectType, +} from '@frontend/services/redirectService'; +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +interface CachedRedirects { + redirects: Redirects; + fetchedAt: number; +} + +const cacheTime = getCacheTime(); + +let cachedRedirects: CachedRedirects | undefined; + +// The middleware only runs on paths defined in the config +// in middleware.ts, that will also need to be +// updated if any other paths are added here. +const redirectPaths = { + methodologies: '/methodology', + publications: '/find-statistics', +}; + +export default async function redirectPages(request: NextRequest) { + const shouldRefetch = + !cachedRedirects || cachedRedirects.fetchedAt + cacheTime < Date.now(); + + if (shouldRefetch) { + cachedRedirects = { + redirects: await redirectService.list(), + fetchedAt: Date.now(), + }; + } + + const redirectUrl = Object.keys(redirectPaths).reduce((acc, key) => { + const redirectType = key as RedirectType; + if (request.nextUrl.pathname.startsWith(redirectPaths[redirectType])) { + const pathSegments = request.nextUrl.pathname.split('/'); + + const rewriteRule = cachedRedirects?.redirects[redirectType]?.find( + ({ fromSlug }) => pathSegments[2] === fromSlug, + ); + + if (rewriteRule) { + return pathSegments + .map(segment => + segment === rewriteRule?.fromSlug ? rewriteRule?.toSlug : segment, + ) + .join('/'); + } + } + return acc; + }, ''); + + if (redirectUrl) { + return NextResponse.redirect(new URL(redirectUrl, request.url)); + } + + return NextResponse.next(); +} + +// Cache the redirect paths for 2 seconds on Local, +// 10 seconds on Development, and 60 seconds in all other +// environments. +function getCacheTime(): number { + switch (process.env.APP_ENV) { + case 'Local': + return 2_000; + case 'Development': + return 10_000; + default: + return 60_000; + } +} diff --git a/src/explore-education-statistics-frontend/src/services/redirectService.ts b/src/explore-education-statistics-frontend/src/services/redirectService.ts new file mode 100644 index 00000000000..4daf2724edb --- /dev/null +++ b/src/explore-education-statistics-frontend/src/services/redirectService.ts @@ -0,0 +1,21 @@ +export interface Redirects { + methodologies: Redirect[]; + publications: Redirect[]; +} + +export type RedirectType = keyof Redirects; + +interface Redirect { + fromSlug: string; + toSlug: string; +} + +const contentApiUrl = process.env.CONTENT_API_BASE_URL; + +const redirectService = { + async list(): Promise { + return (await fetch(`${contentApiUrl}/redirects`)).json(); + }, +}; + +export default redirectService; diff --git a/src/explore-education-statistics-frontend/test/setupTests.js b/src/explore-education-statistics-frontend/test/setupTests.js index 583cd0238a1..76e830503d9 100644 --- a/src/explore-education-statistics-frontend/test/setupTests.js +++ b/src/explore-education-statistics-frontend/test/setupTests.js @@ -13,3 +13,6 @@ if (typeof window !== 'undefined') { // fetch polyfill for making API calls. require('cross-fetch'); } + +global.Request = jest.requireActual('node-fetch').Request; +global.Response = jest.requireActual('node-fetch').Response; diff --git a/tests/robot-tests/tests/admin_and_public/bau/publish_methodology.robot b/tests/robot-tests/tests/admin_and_public/bau/publish_methodology.robot index 378f7a619a0..61cb9f385d7 100644 --- a/tests/robot-tests/tests/admin_and_public/bau/publish_methodology.robot +++ b/tests/robot-tests/tests/admin_and_public/bau/publish_methodology.robot @@ -415,7 +415,12 @@ Verify that the amended methodology is visible on the public methodologies page user scrolls down 400 user checks page contains link with text and url ... ${PUBLICATION_NAME} - Amended methodology - ... ${PUBLIC_METHODOLOGY_URL_ENDING} + ... ${PUBLIC_METHODOLOGY_URL_ENDING}-amended-methodology # Slug has changed + +Validate methodology redirect works + go to %{PUBLIC_URL}${PUBLIC_METHODOLOGY_URL_ENDING} + user waits until h1 is visible ${PUBLICATION_NAME} - Amended methodology + user checks url contains %{PUBLIC_URL}${PUBLIC_METHODOLOGY_URL_ENDING}-amended-methodology Schedule a methodology amendment to be published with a release amendment user creates amendment for release ${PUBLICATION_NAME} ${RELEASE_NAME} diff --git a/tests/robot-tests/tests/libs/admin-common.robot b/tests/robot-tests/tests/libs/admin-common.robot index 71f6abb8424..5b566a6e88f 100644 --- a/tests/robot-tests/tests/libs/admin-common.robot +++ b/tests/robot-tests/tests/libs/admin-common.robot @@ -126,7 +126,7 @@ user navigates to scheduled release page from dashboard ... ${TOPIC_NAME}=%{TEST_TOPIC_NAME} user navigates to release page from dashboard ... publication-scheduled-releases - ... Edit + ... View ... ${PUBLICATION_NAME} ... ${RELEASE_NAME} ... ${THEME_NAME} diff --git a/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json b/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json index 6eadbf3f132..c08e1b80651 100644 --- a/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json +++ b/tests/robot-tests/tests/snapshots/find_statistics_snapshot.json @@ -163,7 +163,7 @@ { "publication_summary": "Annual data release on schools, pupils, teachers, qualifications gained, education expenditure, further education and higher education in the UK.", "publication_title": "Education and training statistics for the UK", - "published": "17 Nov 2022", + "published": "9 Nov 2023", "release_type": "National statistics", "theme": "UK education and training statistics" }, @@ -513,7 +513,7 @@ { "publication_summary": "Pupil attendance and absence data including termly national statistics and fortnightly experimental statistics derived from DfE\u2019s regular attendance data", "publication_title": "Pupil attendance in schools", - "published": "26 Oct 2023", + "published": "9 Nov 2023", "release_type": "Experimental statistics", "theme": "Pupils and schools" }, @@ -604,8 +604,8 @@ { "publication_summary": "Provider-reported Skills Bootcamps starts between April 2021 and March 2022 (in FY 2021-22). National level estimates of the number of Skills Bootcamps starts.", "publication_title": "Skills Bootcamps starts", - "published": "8 Dec 2022", - "release_type": "Ad hoc statistics", + "published": "9 Nov 2023", + "release_type": "Management information", "theme": "Further education" }, {