From e06849dacc9bbb53f87418ac3c346c4dc4a3c153 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Wed, 9 Aug 2023 08:45:00 +0100 Subject: [PATCH 01/22] EES-4448 wip approvals tab --- .../admin-dashboard/AdminDashboardPage.tsx | 180 +++++++++++++++-- .../components/ApprovalsTab.tsx | 36 ++++ .../components/ApprovalsTable.tsx | 153 +++++++++++++++ .../components/PublicationsTab.tsx | 11 +- .../__tests__/ApprovalsTable.test.tsx | 184 ++++++++++++++++++ .../__tests__/PublicationsTab.test.tsx | 3 +- .../src/queries/releaseQueries.ts | 15 ++ .../src/queries/themeQueries.ts | 11 ++ 8 files changed, 571 insertions(+), 22 deletions(-) create mode 100644 src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx create mode 100644 src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx create mode 100644 src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx create mode 100644 src/explore-education-statistics-admin/src/queries/releaseQueries.ts create mode 100644 src/explore-education-statistics-admin/src/queries/themeQueries.ts diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx index 75c0aa0d29a..cf4fd063738 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx @@ -2,37 +2,163 @@ import Link from '@admin/components/Link'; import Page from '@admin/components/Page'; import PageTitle from '@admin/components/PageTitle'; import { useAuthContext } from '@admin/contexts/AuthContext'; -import useAsyncHandledRetry from '@common/hooks/useAsyncHandledRetry'; +import ApprovalsTab from '@admin/pages/admin-dashboard/components/ApprovalsTab'; import DraftReleasesTab from '@admin/pages/admin-dashboard/components/DraftReleasesTab'; import PublicationsTab from '@admin/pages/admin-dashboard/components/PublicationsTab'; import ScheduledReleasesTab from '@admin/pages/admin-dashboard/components/ScheduledReleasesTab'; +import releaseQueries from '@admin/queries/releaseQueries'; import loginService from '@admin/services/loginService'; -import releaseService from '@admin/services/releaseService'; +import { MethodologyVersion } from '@admin/services/methodologyService'; +import { Release } from '@admin/services/releaseService'; import RelatedInformation from '@common/components/RelatedInformation'; import Tabs from '@common/components/Tabs'; import TabsSection from '@common/components/TabsSection'; -import React, { useState } from 'react'; +import WarningMessage from '@common/components/WarningMessage'; +import React from 'react'; +import { useQuery } from '@tanstack/react-query'; + +const testMethodologies: MethodologyVersion[] = [ + { + id: 'c8c911e3-39c1-452b-801f-25bb79d1deb7', + methodologyId: 'b8bd000c-f9d8-4319-a2b3-6bc18675e5ac', + owningPublication: { + id: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', + title: 'Permanent and fixed-period exclusions in England', + }, + otherPublications: [], + published: '2018-08-25T00:00:00', + publishingStrategy: 'Immediately', + slug: 'permanent-and-fixed-period-exclusions-in-england', + status: 'Approved', + title: 'Pupil exclusion statistics: methodology', + amendment: false, + }, +]; + +const testReleases: Release[] = [ + { + id: 'test-id', + title: 'Academic year 2016/17', + slug: '2024-25', + publicationId: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', + publicationTitle: 'Permanent and fixed-period exclusions in England', + publicationSummary: '', + publicationSlug: 'pub-slug', + year: 2024, + yearTitle: '2024/25', + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + preReleaseAccessList: '

Test public access list

', + preReleaseUsersOrInvitesAdded: false, + previousVersionId: 'f', + latestRelease: false, + type: 'NationalStatistics', + contact: { + teamName: 'UI test team name', + teamEmail: 'ui_test@test.com', + contactName: 'UI test contact name', + contactTelNo: '1234 1234', + }, + approvalStatus: 'Approved', + notifySubscribers: false, + latestInternalReleaseNote: 'Approved by UI tests', + amendment: false, + permissions: { + canAddPrereleaseUsers: true, + canViewRelease: true, + canUpdateRelease: true, + canDeleteRelease: false, + canMakeAmendmentOfRelease: false, + }, + updatePublishedDate: false, + }, + { + id: '86d868cf-ff4b-4325-ef26-08d93c9b5089', + title: 'Academic year 2024/25', + slug: '2024-25', + publicationId: '959bd40c-4685-46ff-396d-08d93c9b5159', + publicationTitle: + 'UI tests - Publication and Release UI Permissions Publication Owner', + publicationSummary: '', + publicationSlug: + 'ui-tests-publication-and-release-ui-permissions-publication-owner', + year: 2024, + yearTitle: '2024/25', + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + publishScheduled: '2048-11-16', + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + preReleaseAccessList: '

Test public access list

', + preReleaseUsersOrInvitesAdded: false, + previousVersionId: 'f', + latestRelease: false, + type: 'NationalStatistics', + contact: { + teamName: 'UI test team name', + teamEmail: 'ui_test@test.com', + contactName: 'UI test contact name', + contactTelNo: '1234 1234', + }, + approvalStatus: 'Approved', + notifySubscribers: false, + latestInternalReleaseNote: 'Approved by UI tests', + amendment: false, + permissions: { + canAddPrereleaseUsers: true, + canViewRelease: true, + canUpdateRelease: true, + canDeleteRelease: false, + canMakeAmendmentOfRelease: false, + }, + updatePublishedDate: false, + }, +]; const AdminDashboardPage = () => { + // TO DO EES-4448 replace with real permission + const isApprover = true; const { user } = useAuthContext(); const isBauUser = user?.permissions.isBauUser ?? false; - const [totalDraftReleases, setTotalDraftReleases] = useState(0); - const { - value: draftReleases = [], + data: draftReleases = [], isLoading: isLoadingDraftReleases, - retry: reloadDraftReleases, - } = useAsyncHandledRetry(async () => { - const releases = await releaseService.getDraftReleases(); - // Store the total so it doesn't flicker in the tab while reloading. - setTotalDraftReleases(releases.length); - return releases; - }); + refetch: reloadDraftReleases, + } = useQuery(releaseQueries.listDraftReleases); const { - value: scheduledReleases = [], + data: scheduledReleases = [], isLoading: isLoadingScheduledReleases, - } = useAsyncHandledRetry(releaseService.getScheduledReleases); + } = useQuery(releaseQueries.listScheduledReleases); + + // TO DO EES-4448 fetch approvals data here and remove test data + // const { + // data: methodologyApprovals = [], + // isLoading: isLoadingMethodologyApprovals, + // } = useQuery(TBC); + // const { + // data: releaseApprovals = [], + // isLoading: isLoadingReleaseApprovals, + // } = useQuery(TBC); + const methodologyApprovals = testMethodologies; + const releaseApprovals = testReleases; + const isLoadingApprovals = false; + + const totalApprovals = methodologyApprovals.length + releaseApprovals.length; return ( @@ -55,6 +181,12 @@ const AdminDashboardPage = () => {

+ {isApprover && totalApprovals > 0 && ( + + You have outstanding approvals + + )} +

This is your administration dashboard, here you can manage publications, releases and methodologies. @@ -96,7 +228,7 @@ const AdminDashboardPage = () => { lazy id="draft-releases" data-testid="publication-draft-releases" - title={`Draft releases (${totalDraftReleases})`} + title={`Draft releases (${draftReleases.length})`} > { onChangeRelease={reloadDraftReleases} /> + + {isApprover && ( + + + + )} + +

+
+

My approvals

+

+ Here you can view any releases or methodologies awaiting approval. +

+
+
+ + + + + ); +} diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx new file mode 100644 index 00000000000..340964c9f5b --- /dev/null +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -0,0 +1,153 @@ +import Link from '@admin/components/Link'; +import { MethodologyVersion } from '@admin/services/methodologyService'; +import { Release } from '@admin/services/releaseService'; +import { + MethodologyRouteParams, + methodologyContentRoute, +} from '@admin/routes/methodologyRoutes'; +import { + ReleaseRouteParams, + releaseContentRoute, +} from '@admin/routes/releaseRoutes'; +import VisuallyHidden from '@common/components/VisuallyHidden'; +import { Dictionary } from '@common/types'; +import orderBy from 'lodash/orderBy'; +import React, { useMemo } from 'react'; +import { generatePath } from 'react-router'; +import merge from 'lodash/merge'; + +interface Props { + methodologyApprovals: MethodologyVersion[]; + releaseApprovals: Release[]; +} + +export default function ApprovalsTable({ + methodologyApprovals, + releaseApprovals, +}: Props) { + const releasesByPublication: Dictionary<{ + releases: Release[]; + }> = useMemo(() => { + return releaseApprovals.reduce>( + (acc, release) => { + if (acc[release.publicationTitle]) { + acc[release.publicationTitle].releases.push(release); + } else { + acc[release.publicationTitle] = { + ...acc[release.publicationTitle], + releases: [release], + }; + } + return acc; + }, + {}, + ); + }, [releaseApprovals]); + + const methodologiesByPublication: Dictionary<{ + methodologies: MethodologyVersion[]; + }> = useMemo(() => { + return methodologyApprovals.reduce< + Dictionary<{ methodologies: MethodologyVersion[] }> + >((acc, methodology) => { + if (acc[methodology.owningPublication.title]) { + acc[methodology.owningPublication.title].methodologies.push( + methodology, + ); + } else { + acc[methodology.owningPublication.title] = { + ...acc[methodology.owningPublication.title], + methodologies: [methodology], + }; + } + return acc; + }, {}); + }, [methodologyApprovals]); + + const allApprovalsByPublication = merge( + releasesByPublication, + methodologiesByPublication, + ); + + if (!Object.keys(allApprovalsByPublication).length) { + return

There are no pages awaiting your approval.

; + } + + return ( + + + + + + + + {orderBy(Object.keys(allApprovalsByPublication)).map(publication => ( + + ))} + +
Publication / PagePage typeActions
+ ); +} + +interface PublicationRowProps { + publication: string; + methodologies: MethodologyVersion[]; + releases: Release[]; +} + +function PublicationRow({ + publication, + methodologies, + releases, +}: PublicationRowProps) { + return ( + <> + + + {publication} + + + {releases?.map(release => ( + + {release.title} + Release + + (releaseContentRoute.path, { + publicationId: release.publicationId, + releaseId: release.id, + })} + > + Review this page + for {release.title} + + + + ))} + {methodologies?.map(methodology => ( + + {methodology.title} + Methodology + + ( + methodologyContentRoute.path, + { + methodologyId: methodology.id, + }, + )} + > + Review this page + for {methodology.title} + + + + ))} + + ); +} diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/PublicationsTab.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/PublicationsTab.tsx index 08de03dfe5a..d874c3d398a 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/PublicationsTab.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/PublicationsTab.tsx @@ -2,15 +2,16 @@ import FormThemeTopicSelect from '@admin/components/form/FormThemeTopicSelect'; import useQueryParams from '@admin/hooks/useQueryParams'; import TopicPublications from '@admin/pages/admin-dashboard/components/TopicPublications'; import { ThemeTopicParams, dashboardRoute } from '@admin/routes/routes'; -import themeService, { Theme } from '@admin/services/themeService'; +import { Theme } from '@admin/services/themeService'; import { Topic } from '@admin/services/topicService'; import appendQuery from '@common/utils/url/appendQuery'; import LoadingSpinner from '@common/components/LoadingSpinner'; -import useAsyncHandledRetry from '@common/hooks/useAsyncHandledRetry'; +import themeQueries from '@admin/queries/themeQueries'; import useStorageItem from '@common/hooks/useStorageItem'; import orderBy from 'lodash/orderBy'; import React, { useEffect, useMemo } from 'react'; import { useHistory, useLocation } from 'react-router'; +import { useQuery } from '@tanstack/react-query'; interface Props { isBauUser: boolean; @@ -24,8 +25,8 @@ const PublicationsTab = ({ isBauUser }: Props) => { const [savedThemeTopic, setSavedThemeTopic] = useStorageItem('dashboardThemeTopic', undefined); - const { value: themes, isLoading: loadingThemes } = useAsyncHandledRetry( - themeService.getThemes, + const { data: themes, isLoading: isLoadingThemes } = useQuery( + themeQueries.listThemes, ); const selectedTheme = useMemo(() => { @@ -105,7 +106,7 @@ const PublicationsTab = ({ isBauUser }: Props) => { return (

View and manage your publications

diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx new file mode 100644 index 00000000000..defddd396ca --- /dev/null +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx @@ -0,0 +1,184 @@ +import ApprovalsTable from '@admin/pages/admin-dashboard/components/ApprovalsTable'; +import { MethodologyVersion } from '@admin/services/methodologyService'; +import { Release } from '@admin/services/releaseService'; +import { waitFor, within } from '@testing-library/dom'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +describe('ApprovalsTable', () => { + const testMethodologies: MethodologyVersion[] = [ + { + id: 'c8c911e3-39c1-452b-801f-25bb79d1deb7', + methodologyId: 'b8bd000c-f9d8-4319-a2b3-6bc18675e5ac', + owningPublication: { + id: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', + title: 'Permanent and fixed-period exclusions in England', + }, + otherPublications: [], + published: '2018-08-25T00:00:00', + publishingStrategy: 'Immediately', + slug: 'permanent-and-fixed-period-exclusions-in-england', + status: 'Approved', + title: 'Pupil exclusion statistics: methodology', + amendment: false, + }, + ]; + + const testReleases: Release[] = [ + { + id: 'test-id', + title: 'Academic year 2016/17', + slug: '2024-25', + publicationId: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', + publicationTitle: 'Permanent and fixed-period exclusions in England', + publicationSummary: '', + publicationSlug: 'pub-slug', + year: 2024, + yearTitle: '2024/25', + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + preReleaseAccessList: '

Test public access list

', + preReleaseUsersOrInvitesAdded: false, + previousVersionId: 'f', + latestRelease: false, + type: 'NationalStatistics', + contact: { + teamName: 'UI test team name', + teamEmail: 'ui_test@test.com', + contactName: 'UI test contact name', + contactTelNo: '1234 1234', + }, + approvalStatus: 'Approved', + notifySubscribers: false, + latestInternalReleaseNote: 'Approved by UI tests', + amendment: false, + permissions: { + canAddPrereleaseUsers: true, + canViewRelease: true, + canUpdateRelease: true, + canDeleteRelease: false, + canMakeAmendmentOfRelease: false, + }, + updatePublishedDate: false, + }, + { + id: '86d868cf-ff4b-4325-ef26-08d93c9b5089', + title: 'Academic year 2024/25', + slug: '2024-25', + publicationId: '959bd40c-4685-46ff-396d-08d93c9b5159', + publicationTitle: + 'UI tests - Publication and Release UI Permissions Publication Owner', + publicationSummary: '', + publicationSlug: + 'ui-tests-publication-and-release-ui-permissions-publication-owner', + year: 2024, + yearTitle: '2024/25', + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + publishScheduled: '2048-11-16', + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + preReleaseAccessList: '

Test public access list

', + preReleaseUsersOrInvitesAdded: false, + previousVersionId: 'f', + latestRelease: false, + type: 'NationalStatistics', + contact: { + teamName: 'UI test team name', + teamEmail: 'ui_test@test.com', + contactName: 'UI test contact name', + contactTelNo: '1234 1234', + }, + approvalStatus: 'Approved', + notifySubscribers: false, + latestInternalReleaseNote: 'Approved by UI tests', + amendment: false, + permissions: { + canAddPrereleaseUsers: true, + canViewRelease: true, + canUpdateRelease: true, + canDeleteRelease: false, + canMakeAmendmentOfRelease: false, + }, + updatePublishedDate: false, + }, + ]; + + test('renders the table of releases and methodologies grouped by publication ', async () => { + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByText('Publication / Page')).toBeInTheDocument(); + }); + + const rows = screen.getAllByRole('row'); + expect(rows).toHaveLength(6); + + expect(within(rows[1]).getByRole('columnheader')).toHaveTextContent( + 'Permanent and fixed-period exclusions in England', + ); + + const row3cells = within(rows[2]).getAllByRole('cell'); + expect(row3cells[0]).toHaveTextContent('Academic year 2016/17'); + expect(row3cells[1]).toHaveTextContent('Release'); + expect( + within(row3cells[2]).getByRole('link', { name: /Review this page/ }), + ).toBeInTheDocument(); + + const row4cells = within(rows[3]).getAllByRole('cell'); + expect(row4cells[0]).toHaveTextContent( + 'Pupil exclusion statistics: methodology', + ); + expect(row4cells[1]).toHaveTextContent('Methodology'); + expect( + within(row4cells[2]).getByRole('link', { name: /Review this page/ }), + ).toBeInTheDocument(); + + expect(within(rows[4]).getByRole('columnheader')).toHaveTextContent( + 'UI tests - Publication and Release UI Permissions Publication Owner', + ); + + const row6cells = within(rows[5]).getAllByRole('cell'); + expect(row6cells[0]).toHaveTextContent('Academic year 2024/25'); + expect(row6cells[1]).toHaveTextContent('Release'); + expect( + within(row6cells[2]).getByRole('link', { name: /Review this page/ }), + ).toBeInTheDocument(); + }); + + test('renders correctly when there are no approvals', async () => { + render( + + + , + ); + + await waitFor(() => { + expect( + screen.getByText('There are no pages awaiting your approval.'), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/PublicationsTab.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/PublicationsTab.test.tsx index 9bc688d038a..ad662d32181 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/PublicationsTab.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/PublicationsTab.test.tsx @@ -5,8 +5,9 @@ import _publicationService, { } from '@admin/services/publicationService'; import _themeService, { Theme } from '@admin/services/themeService'; import _storageService from '@common/services/storageService'; +import render from '@common-test/render'; import { waitFor } from '@testing-library/dom'; -import { render, screen, within } from '@testing-library/react'; +import { screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { createMemoryHistory } from 'history'; import React from 'react'; diff --git a/src/explore-education-statistics-admin/src/queries/releaseQueries.ts b/src/explore-education-statistics-admin/src/queries/releaseQueries.ts new file mode 100644 index 00000000000..33763ee8071 --- /dev/null +++ b/src/explore-education-statistics-admin/src/queries/releaseQueries.ts @@ -0,0 +1,15 @@ +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import releaseService from '@admin/services/releaseService'; + +const releaseQueries = createQueryKeys('release', { + listDraftReleases: { + queryKey: null, + queryFn: () => releaseService.getDraftReleases(), + }, + listScheduledReleases: { + queryKey: null, + queryFn: () => releaseService.getScheduledReleases(), + }, +}); + +export default releaseQueries; diff --git a/src/explore-education-statistics-admin/src/queries/themeQueries.ts b/src/explore-education-statistics-admin/src/queries/themeQueries.ts new file mode 100644 index 00000000000..9a9d27774d2 --- /dev/null +++ b/src/explore-education-statistics-admin/src/queries/themeQueries.ts @@ -0,0 +1,11 @@ +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import themeService from '@admin/services/themeService'; + +const themeQueries = createQueryKeys('theme', { + listThemes: { + queryKey: null, + queryFn: () => themeService.getThemes(), + }, +}); + +export default themeQueries; From 5a48511419a1bfed74b5b2b98dd8fd7f4366f750 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Mon, 14 Aug 2023 12:51:42 +0100 Subject: [PATCH 02/22] EES-4448 - added new services and API endpoints for listing Releases for Approval --- .../Api/ReleasesControllerTests.cs | 215 +++++++------ .../Services/ReleaseServiceTests.cs | 296 +++++++++++++++++- .../Controllers/Api/ReleasesController.cs | 16 +- .../ReleaseStatusAuthorizationHandlers.cs | 11 +- .../Services/Interfaces/IReleaseService.cs | 2 + .../Services/ReleaseService.cs | 37 +++ .../Fixtures/InstanceSetters.cs | 8 + .../Services/Security/UserService.cs | 5 +- .../PublicationGeneratorExtensions.cs | 53 ++++ .../Fixtures/ReleaseGeneratorExtensions.cs | 40 +++ .../UserPublicationRoleGeneratorExtensions.cs | 62 ++++ .../UserReleaseRoleGeneratorExtensions.cs | 62 ++++ 12 files changed, 692 insertions(+), 115 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/PublicationGeneratorExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserPublicationRoleGeneratorExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserReleaseRoleGeneratorExtensions.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs index a34b4bd2ef3..2309c7d1c7a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -9,6 +10,7 @@ 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.Tests.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -16,6 +18,7 @@ using Xunit; using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationUtils; +using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; @@ -31,18 +34,17 @@ public async Task Create_Release_Returns_Ok() { var returnedViewModel = new ReleaseViewModel(); - var mocks = Mocks(); + var releaseService = new Mock(Strict); - mocks - .ReleaseService + releaseService .Setup(s => s.CreateRelease(It.IsAny())) .ReturnsAsync(returnedViewModel); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseService.Object); // Call the method under test var result = await controller.CreateRelease(new ReleaseCreateRequest(), _publicationId); - VerifyAllMocks(mocks.ReleaseService); + VerifyAllMocks(releaseService); result.AssertOkResult(returnedViewModel); } @@ -50,7 +52,6 @@ public async Task Create_Release_Returns_Ok() [Fact] public async Task AddDataFilesAsync_UploadsTheFiles_Returns_Ok() { - var mocks = Mocks(); var dataFile = MockFile("datafile.csv"); var metaFile = MockFile("metafile.csv"); @@ -59,7 +60,8 @@ public async Task AddDataFilesAsync_UploadsTheFiles_Returns_Ok() Name = "Subject name", }; - mocks.ReleaseDataFilesService + var releaseDataFileService = new Mock(Strict); + releaseDataFileService .Setup(service => service.Upload(_releaseId, dataFile, metaFile, @@ -68,13 +70,15 @@ public async Task AddDataFilesAsync_UploadsTheFiles_Returns_Ok() .ReturnsAsync(dataFileInfo); // Call the method under test - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseDataFileService: releaseDataFileService.Object); + var result = await controller.AddDataFilesAsync(releaseId: _releaseId, replacingFileId: null, subjectName: "Subject name", file: dataFile, metaFile: metaFile); - VerifyAllMocks(mocks); + + VerifyAllMocks(releaseDataFileService); var dataFileInfoResult = result.AssertOkResult(); Assert.Equal("Subject name", dataFileInfoResult.Name); @@ -83,11 +87,11 @@ public async Task AddDataFilesAsync_UploadsTheFiles_Returns_Ok() [Fact] public async Task AddDataFilesAsync_UploadsTheFiles_Returns_ValidationProblem() { - var mocks = Mocks(); var dataFile = MockFile("datafile.csv"); var metaFile = MockFile("metafile.csv"); - mocks.ReleaseDataFilesService + var releaseDataFileService = new Mock(Strict); + releaseDataFileService .Setup(service => service.Upload(_releaseId, dataFile, metaFile, @@ -95,7 +99,7 @@ public async Task AddDataFilesAsync_UploadsTheFiles_Returns_ValidationProblem() "Subject name")) .ReturnsAsync(ValidationActionResult(CannotOverwriteFile)); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseDataFileService: releaseDataFileService.Object); // Call the method under test var result = await controller.AddDataFilesAsync(releaseId: _releaseId, @@ -103,7 +107,8 @@ public async Task AddDataFilesAsync_UploadsTheFiles_Returns_ValidationProblem() subjectName: "Subject name", file: dataFile, metaFile: metaFile); - VerifyAllMocks(mocks); + + VerifyAllMocks(releaseDataFileService); result.AssertBadRequest(CannotOverwriteFile); } @@ -127,18 +132,17 @@ public async Task GetDataFileInfo_Returns_A_List_Of_Files() } }; - var mocks = Mocks(); + var releaseDataFileService = new Mock(Strict); - mocks - .ReleaseDataFilesService + releaseDataFileService .Setup(s => s.ListAll(_releaseId)) .ReturnsAsync(testFiles); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseDataFileService: releaseDataFileService.Object); // Call the method under test var result = await controller.GetDataFileInfo(_releaseId); - VerifyAllMocks(mocks.ReleaseDataFilesService); + VerifyAllMocks(releaseDataFileService); var unboxed = result.AssertOkResult(); Assert.Contains(unboxed, f => f.Name == "Release a file 1"); @@ -148,16 +152,18 @@ public async Task GetDataFileInfo_Returns_A_List_Of_Files() [Fact] public async Task DeleteDataFilesAsync_Returns_OK() { - var mocks = Mocks(); + var releaseService = new Mock(Strict); var fileId = Guid.NewGuid(); - mocks.ReleaseService.Setup(service => service.RemoveDataFiles(_releaseId, fileId)) + releaseService + .Setup(service => service.RemoveDataFiles(_releaseId, fileId)) .ReturnsAsync(Unit.Instance); - var controller = ReleasesControllerWithMocks(mocks); + + var controller = BuildController(releaseService: releaseService.Object); var result = await controller.DeleteDataFiles(_releaseId, fileId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); Assert.IsAssignableFrom(result); } @@ -165,17 +171,18 @@ public async Task DeleteDataFilesAsync_Returns_OK() [Fact] public async Task DeleteDataFilesAsync_Returns_ValidationProblem() { - var mocks = Mocks(); - + var releaseService = new Mock(Strict); + var fileId = Guid.NewGuid(); - mocks.ReleaseService + releaseService .Setup(service => service.RemoveDataFiles(_releaseId, fileId)) .ReturnsAsync(ValidationActionResult(UnableToFindMetadataFileToDelete)); - var controller = ReleasesControllerWithMocks(mocks); + + var controller = BuildController(releaseService: releaseService.Object); var result = await controller.DeleteDataFiles(_releaseId, fileId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); result.AssertBadRequest(UnableToFindMetadataFileToDelete); } @@ -183,19 +190,20 @@ public async Task DeleteDataFilesAsync_Returns_ValidationProblem() [Fact] public async Task UpdateRelease_Returns_Ok() { - var mocks = Mocks(); + var releaseService = new Mock(Strict); - mocks.ReleaseService + releaseService .Setup(s => s.UpdateRelease( It.Is(id => id.Equals(_releaseId)), It.IsAny()) ) .ReturnsAsync(new ReleaseViewModel {Id = _releaseId}); - var controller = ReleasesControllerWithMocks(mocks); + + var controller = BuildController(releaseService: releaseService.Object); // Method under test var result = await controller.UpdateRelease(new ReleaseUpdateRequest(), _releaseId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); var unboxed = result.AssertOkResult(); Assert.Equal(_releaseId, unboxed.Id); @@ -204,17 +212,20 @@ public async Task UpdateRelease_Returns_Ok() [Fact] public async Task GetTemplateRelease_Returns_Ok() { - var mocks = Mocks(); + var releaseService = new Mock(Strict); + var templateReleaseResult = new Either(new IdTitleViewModel()); - mocks.ReleaseService + + releaseService .Setup(s => s.GetLatestPublishedRelease(It.Is(id => id == _releaseId))) - .Returns(x => Task.FromResult(templateReleaseResult)); - var controller = ReleasesControllerWithMocks(mocks); + .ReturnsAsync(templateReleaseResult); + + var controller = BuildController(releaseService.Object); // Method under test var result = await controller.GetTemplateRelease(_releaseId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); result.AssertOkResult(); } @@ -222,18 +233,18 @@ public async Task GetTemplateRelease_Returns_Ok() [Fact] public async Task CancelFileImport() { - var mocks = Mocks(); + var importService = new Mock(Strict); var fileId = Guid.NewGuid(); - mocks.ImportService + importService .Setup(s => s.CancelImport(_releaseId, fileId)) .ReturnsAsync(Unit.Instance); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(importService: importService.Object); var result = await controller.CancelFileImport(_releaseId, fileId); - VerifyAllMocks(mocks); + VerifyAllMocks(importService); result.AssertAccepted(); } @@ -241,18 +252,18 @@ public async Task CancelFileImport() [Fact] public async Task CancelFileImportButNotAllowed() { - var mocks = Mocks(); - + var importService = new Mock(Strict); + var fileId = Guid.NewGuid(); - mocks.ImportService + importService .Setup(s => s.CancelImport(_releaseId, fileId)) .ReturnsAsync(new ForbidResult()); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(importService: importService.Object); var result = await controller.CancelFileImport(_releaseId, fileId); - VerifyAllMocks(mocks); + VerifyAllMocks(importService); result.AssertForbidden(); } @@ -260,20 +271,20 @@ public async Task CancelFileImportButNotAllowed() [Fact] public async Task GetDeleteDataFilePlan() { - var mocks = Mocks(); - + var releaseService = new Mock(Strict); + var fileId = Guid.NewGuid(); var deleteDataFilePlan = new DeleteDataFilePlan(); - mocks.ReleaseService + releaseService .Setup(s => s.GetDeleteDataFilePlan(_releaseId, fileId)) .ReturnsAsync(deleteDataFilePlan); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseService: releaseService.Object); var result = await controller.GetDeleteDataFilePlan(_releaseId, fileId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); result.AssertOkResult(deleteDataFilePlan); } @@ -281,18 +292,18 @@ public async Task GetDeleteDataFilePlan() [Fact] public async Task GetDeleteReleasePlan() { - var mocks = Mocks(); - + var releaseService = new Mock(Strict); + var deleteReleasePlan = new DeleteReleasePlan(); - mocks.ReleaseService + releaseService .Setup(s => s.GetDeleteReleasePlan(_releaseId)) .ReturnsAsync(deleteReleasePlan); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController(releaseService: releaseService.Object); var result = await controller.GetDeleteReleasePlan(_releaseId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseService); result.AssertOkResult(deleteReleasePlan); } @@ -300,30 +311,61 @@ public async Task GetDeleteReleasePlan() [Fact] public async Task CreateReleaseStatus() { + var releaseApprovalService = new Mock(Strict); + var releaseService = new Mock(Strict); + var request = new ReleaseStatusCreateRequest(); var returnedReleaseViewModel = new ReleaseViewModel(); - var mocks = Mocks(); - - mocks - .ReleaseApprovalService + releaseApprovalService .Setup(s => s.CreateReleaseStatus(_releaseId, request)) .ReturnsAsync(Unit.Instance); - mocks - .ReleaseService + releaseService .Setup(s => s.GetRelease(_releaseId)) .ReturnsAsync(returnedReleaseViewModel); - var controller = ReleasesControllerWithMocks(mocks); + var controller = BuildController( + releaseApprovalService: releaseApprovalService.Object, + releaseService: releaseService.Object); // Call the method under test var result = await controller.CreateReleaseStatus(request, _releaseId); - VerifyAllMocks(mocks); + VerifyAllMocks(releaseApprovalService, releaseService); result.AssertOkResult(returnedReleaseViewModel); } + [Fact] + public async Task ListReleasesForApproval() + { + var userId = Guid.NewGuid(); + var releases = ListOf(new ReleaseViewModel + { + Id = Guid.NewGuid() + }); + + var userService = new Mock(Strict); + var releaseService = new Mock(Strict); + + userService + .Setup(s => s.GetUserId()) + .Returns(userId); + + releaseService + .Setup(s => s.ListReleasesForApproval(userId)) + .ReturnsAsync(releases); + + var controller = BuildController( + userService: userService.Object, + releaseService: releaseService.Object); + + var result = await controller.ListReleasesForApproval(); + VerifyAllMocks(userService, releaseService); + + result.AssertOkResult(releases); + } + private static IFormFile MockFile(string fileName) { var fileMock = new Mock(Strict); @@ -334,40 +376,23 @@ private static IFormFile MockFile(string fileName) return fileMock.Object; } - private static ( - Mock ReleaseService, - Mock ReleaseApprovalService, - Mock ReleaseDataFilesService, - Mock ReleasePublishingStatusService, - Mock ReleaseChecklistService, - Mock ImportService) Mocks() - { - return (new Mock(Strict), - new Mock(Strict), - new Mock(Strict), - new Mock(Strict), - new Mock(Strict), - new Mock(Strict) - ); - } - - private static ReleasesController ReleasesControllerWithMocks( - ( - Mock ReleaseService, - Mock ReleaseApprovalService, - Mock ReleaseDataFileService, - Mock ReleaseStatusService, - Mock ReleaseChecklistService, - Mock ImportService - ) mocks) + private static ReleasesController BuildController( + IReleaseService? releaseService = null, + IReleaseApprovalService? releaseApprovalService = null, + IReleaseDataFileService? releaseDataFileService = null, + IReleasePublishingStatusService? releaseStatusService = null, + IReleaseChecklistService? releaseChecklistService = null, + IDataImportService? importService = null, + IUserService? userService = null) { return new ReleasesController( - mocks.ReleaseService.Object, - mocks.ReleaseApprovalService.Object, - mocks.ReleaseDataFileService.Object, - mocks.ReleaseStatusService.Object, - mocks.ReleaseChecklistService.Object, - mocks.ImportService.Object); + releaseService ?? Mock.Of(Strict), + releaseApprovalService ?? Mock.Of(Strict), + releaseDataFileService ?? Mock.Of(Strict), + releaseStatusService ?? Mock.Of(Strict), + releaseChecklistService ?? Mock.Of(Strict), + importService ?? Mock.Of(Strict), + userService ?? Mock.Of(Strict)); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs index a61588d2965..cd6eda0d5e1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs @@ -13,11 +13,13 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; 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.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; using GovUk.Education.ExploreEducationStatistics.Data.Model; @@ -30,8 +32,10 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.MapperUtils; using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; +using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyPublishingStrategy; +using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; using static Moq.MockBehavior; using IReleaseRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IReleaseRepository; using Release = GovUk.Education.ExploreEducationStatistics.Content.Model.Release; @@ -41,7 +45,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services { public class ReleaseServiceTests { - private readonly Guid _userId = Guid.NewGuid(); + private static readonly Guid UserId = Guid.NewGuid(); [Fact] public async Task CreateReleaseNoTemplate() @@ -77,7 +81,7 @@ public async Task CreateReleaseNoTemplate() Assert.Equal("2018/19", result.YearTitle); Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriodCoverage); Assert.Equal(ReleaseType.OfficialStatistics, result.Type); - Assert.Equal(ReleaseApprovalStatus.Draft, result.ApprovalStatus); + Assert.Equal(Draft, result.ApprovalStatus); Assert.False(result.Amendment); Assert.False(result.LatestRelease); // Most recent - but not published yet. @@ -97,7 +101,7 @@ public async Task CreateReleaseNoTemplate() Assert.Equal(2018, actual.Year); Assert.Equal(TimeIdentifier.AcademicYear, actual.TimePeriodCoverage); Assert.Equal(ReleaseType.OfficialStatistics, actual.Type); - Assert.Equal(ReleaseApprovalStatus.Draft, actual.ApprovalStatus); + Assert.Equal(Draft, actual.ApprovalStatus); Assert.Equal(0, actual.Version); Assert.Null(actual.PreviousVersionId); @@ -261,7 +265,7 @@ public async Task RemoveDataFiles() { var release = new Release { - ApprovalStatus = ReleaseApprovalStatus.Draft + ApprovalStatus = Draft }; var subject = new Subject @@ -351,7 +355,7 @@ public async Task RemoveDataFiles_FileImporting() { var release = new Release { - ApprovalStatus = ReleaseApprovalStatus.Draft + ApprovalStatus = Draft }; var subject = new Subject @@ -401,7 +405,7 @@ public async Task RemoveDataFiles_ReplacementExists() { var release = new Release { - ApprovalStatus = ReleaseApprovalStatus.Draft + ApprovalStatus = Draft }; var subject = new Subject @@ -530,7 +534,7 @@ public async Task RemoveDataFiles_ReplacementFileImporting() { var release = new Release { - ApprovalStatus = ReleaseApprovalStatus.Draft + ApprovalStatus = Draft }; var subject = new Subject @@ -1151,7 +1155,7 @@ public async Task DeleteRelease() var userReleaseRole = new UserReleaseRole { - UserId = _userId, + UserId = UserId, Release = release }; @@ -1628,7 +1632,279 @@ public async Task UpdateReleasePublished_ConvertsPublishedFromLocalToUniversalTi } } - private ReleaseService BuildReleaseService( + public class ListReleasesForApproval + { + private readonly DataFixture _fixture = new(); + + [Fact] + public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() + { + var contextId = Guid.NewGuid().ToString(); + + var user = new User(); + var otherUser = new User(); + + var publications = _fixture + .DefaultPublication() + .WithReleases(_ => _fixture + .DefaultRelease() + .WithApprovalStatuses(ListOf( + Draft, + HigherLevelReview, + Approved)) + .GenerateList()) + .GenerateList(4); + + var contributorReleaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(user) + .WithRole(ReleaseRole.Contributor) + .WithReleases(publications[0].Releases) + .GenerateList(); + + var approverReleaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(user) + .WithRole(ReleaseRole.Approver) + .WithReleases(publications[1].Releases) + .GenerateList(); + + var prereleaseReleaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(user) + .WithRole(ReleaseRole.PrereleaseViewer) + .WithReleases(publications[2].Releases) + .GenerateList(); + + var approverReleaseRolesForOtherUser = _fixture + .DefaultUserReleaseRole() + .WithUser(otherUser) + .WithRole(ReleaseRole.Approver) + .WithReleases(publications.SelectMany(publication => publication.Releases)) + .GenerateList(); + + var higherReviewReleaseWithApproverRoleForUser = publications[1].Releases[1]; + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + await context.Publications.AddRangeAsync(publications); + await context.UserReleaseRoles.AddRangeAsync(contributorReleaseRolesForUser); + await context.UserReleaseRoles.AddRangeAsync(approverReleaseRolesForUser); + await context.UserReleaseRoles.AddRangeAsync(prereleaseReleaseRolesForUser); + await context.UserReleaseRoles.AddRangeAsync(approverReleaseRolesForOtherUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var service = BuildReleaseService(context); + + var result = await service + .ListReleasesForApproval(user.Id); + + var viewModels = result.AssertRight(); + + // Assert that the only Release returned for this user is the Release where they have a direct + // Approver role on and it is in Higher Review. + Assert.Single(viewModels); + Assert.Equal(higherReviewReleaseWithApproverRoleForUser.Id, viewModels[0].Id); + } + } + + [Fact] + public async Task ListReleasesForApproval_UserHasApproverRoleOnPublications() + { + var contextId = Guid.NewGuid().ToString(); + + var user = new User(); + var otherUser = new User(); + + var publications = _fixture + .DefaultPublication() + .WithReleases(_ => _fixture + .DefaultRelease() + .WithApprovalStatuses(ListOf( + Draft, + HigherLevelReview, + Approved, + HigherLevelReview)) + .GenerateList()) + .GenerateList(3); + + var ownerPublicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithRole(PublicationRole.Owner) + .WithPublication(publications[0]) + .Generate(); + + var approverPublicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithRole(PublicationRole.Approver) + .WithPublication(publications[1]) + .Generate(); + + var ownerPublicationRolesForOtherUser = _fixture + .DefaultUserPublicationRole() + .WithUser(otherUser) + .WithRole(PublicationRole.Owner) + .WithPublications(publications) + .GenerateList(); + + var approverPublicationRolesForOtherUser = _fixture + .DefaultUserPublicationRole() + .WithUser(otherUser) + .WithRole(PublicationRole.Approver) + .WithPublications(publications) + .GenerateList(); + + var release1WithApproverRoleForUser = publications[1].Releases[1]; + var release2WithApproverRoleForUser = publications[1].Releases[3]; + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + await context.Publications.AddRangeAsync(publications); + await context.UserPublicationRoles.AddRangeAsync( + ownerPublicationRoleForUser, + approverPublicationRoleForUser); + await context.UserPublicationRoles.AddRangeAsync(ownerPublicationRolesForOtherUser); + await context.UserPublicationRoles.AddRangeAsync(approverPublicationRolesForOtherUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var service = BuildReleaseService(context); + + var result = await service + .ListReleasesForApproval(user.Id); + + var viewModels = result.AssertRight(); + + // Assert that the only Releases returned for this user are the Releases where they have Approver + // role on the overarching Publication and the Releases are in Higher Review. + Assert.Equal(2, viewModels.Count); + Assert.Equal(release1WithApproverRoleForUser.Id, viewModels[0].Id); + Assert.Equal(release2WithApproverRoleForUser.Id, viewModels[1].Id); + } + } + + [Fact] + public async Task ListReleasesForApproval_MixOfApproverReleaseAndPublicationRoles() + { + var contextId = Guid.NewGuid().ToString(); + + var user = new User(); + + var publications = _fixture + .DefaultPublication() + .WithReleases(_ => _fixture + .DefaultRelease() + .WithApprovalStatuses(ListOf( + Draft, + HigherLevelReview, + Approved)) + .GenerateList()) + .GenerateList(3); + + var approverPublicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithRole(PublicationRole.Approver) + .WithPublication(publications[0]) + .Generate(); + + var approverReleaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(user) + .WithRole(ReleaseRole.Approver) + .WithReleases(publications[1].Releases) + .GenerateList(); + + var release1WithApproverRoleForUser = publications[0].Releases[1]; + var release2WithApproverRoleForUser = publications[1].Releases[1]; + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + await context.Publications.AddRangeAsync(publications); + await context.UserPublicationRoles.AddRangeAsync(approverPublicationRoleForUser); + await context.UserReleaseRoles.AddRangeAsync(approverReleaseRolesForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var service = BuildReleaseService(context); + + var result = await service + .ListReleasesForApproval(user.Id); + + var viewModels = result.AssertRight(); + + // Assert that the only Releases returned for this user are the Releases where they have Approver + // role on the overarching Publication and the Releases are in Higher Review, and where the user + // has a direct Approver role on a Release in higher review. + Assert.Equal(2, viewModels.Count); + Assert.Equal(release1WithApproverRoleForUser.Id, viewModels[0].Id); + Assert.Equal(release2WithApproverRoleForUser.Id, viewModels[1].Id); + } + } + + [Fact] + public async Task ListReleasesForApproval_UserHasApproverRoleOnPublicationsAndApproverRoleOnRelease() + { + var contextId = Guid.NewGuid().ToString(); + + var user = new User(); + + var publication = _fixture + .DefaultPublication() + .WithReleases(_ => _fixture + .DefaultRelease() + .WithApprovalStatus(HigherLevelReview) + .Generate(1)) + .Generate(); + + var approverReleaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(user) + .WithRole(ReleaseRole.Approver) + .WithReleases(publication.Releases) + .GenerateList(); + + var approverPublicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithRole(PublicationRole.Approver) + .WithPublication(publication) + .Generate(); + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + await context.Publications.AddRangeAsync(publication); + await context.UserReleaseRoles.AddRangeAsync(approverReleaseRolesForUser); + await context.UserPublicationRoles.AddRangeAsync(approverPublicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contextId)) + { + var service = BuildReleaseService(context); + + var result = await service.ListReleasesForApproval(user.Id); + + var viewModels = result.AssertRight(); + + // Assert that the Release only appears once despite the user having approval directly via the + // Release itself AND via the overarching Publication. + Assert.Single(viewModels); + Assert.Equal(publication.Releases[0].Id, viewModels[0].Id); + } + } + } + + private static ReleaseService BuildReleaseService( ContentDbContext contentDbContext, StatisticsDbContext? statisticsDbContext = null, IReleaseRepository? releaseRepository = null, @@ -1648,7 +1924,7 @@ private ReleaseService BuildReleaseService( userService .Setup(s => s.GetUserId()) - .Returns(_userId); + .Returns(UserId); return new ReleaseService( contentDbContext, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs index 5fd5f7e4c22..4d9505e5571 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs @@ -9,6 +9,7 @@ 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.Content.Model; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -27,6 +28,7 @@ public class ReleasesController : ControllerBase private readonly IReleasePublishingStatusService _releasePublishingStatusService; private readonly IReleaseChecklistService _releaseChecklistService; private readonly IDataImportService _dataImportService; + private readonly IUserService _userService; public ReleasesController( IReleaseService releaseService, @@ -34,14 +36,16 @@ public ReleasesController( IReleaseDataFileService releaseDataFileService, IReleasePublishingStatusService releasePublishingStatusService, IReleaseChecklistService releaseChecklistService, - IDataImportService dataImportService) + IDataImportService dataImportService, + IUserService userService) { _releaseService = releaseService; + _releaseApprovalService = releaseApprovalService; _releaseDataFileService = releaseDataFileService; _releasePublishingStatusService = releasePublishingStatusService; _releaseChecklistService = releaseChecklistService; _dataImportService = dataImportService; - _releaseApprovalService = releaseApprovalService; + _userService = userService; } [HttpPost("publications/{publicationId:guid}/releases")] @@ -206,6 +210,14 @@ public async Task>> ListDraftReleases() .HandleFailuresOrOk(); } + [HttpGet("releases/approvals")] + public async Task>> ListReleasesForApproval() + { + return await _releaseService + .ListReleasesForApproval(_userService.GetUserId()) + .HandleFailuresOrOk(); + } + [HttpGet("releases/scheduled")] public async Task>> ListScheduledReleases() { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs index c49d0e9094f..b7dde36c46c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ReleaseStatusAuthorizationHandlers.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers @@ -77,7 +76,7 @@ private async Task HandleMovingToApproved( context.User.GetUserId(), release.PublicationId, release.Id, - ListOf(Approver), + ListOf(PublicationRole.Approver), ListOf(ReleaseRole.Approver))) { context.Succeed(requirement); @@ -96,8 +95,8 @@ private async Task HandleMovingToHigherLevelReview( } var allowedPublicationRoles = release.ApprovalStatus == Approved - ? ListOf(Approver) - : ListOf(Owner, Approver); + ? ListOf(PublicationRole.Approver) + : ListOf(PublicationRole.Owner, PublicationRole.Approver); var allowedReleaseRoles = release.ApprovalStatus == Approved ? ListOf(ReleaseRole.Approver) @@ -127,8 +126,8 @@ private async Task HandleMovingToDraft( } var allowedPublicationRoles = release.ApprovalStatus == Approved - ? ListOf(Approver) - : ListOf(Owner, Approver); + ? ListOf(PublicationRole.Approver) + : ListOf(PublicationRole.Owner, PublicationRole.Approver); var allowedReleaseRoles = release.ApprovalStatus == Approved ? ListOf(ReleaseRole.Approver) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs index 72249e1f349..c5fdff0494f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs @@ -34,6 +34,8 @@ Task> UpdateReleasePublished(Guid releaseId, Task>> ListReleasesWithStatuses( params ReleaseApprovalStatus[] releaseApprovalStatues); + Task>> ListReleasesForApproval(Guid userId); + Task>> ListScheduledReleases(); Task> GetDeleteDataFilePlan(Guid releaseId, Guid fileId); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs index f39a329a719..26234de9e44 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs @@ -443,6 +443,43 @@ public async Task>> ListReleasesWith }); } + public async Task>> ListReleasesForApproval(Guid userId) + { + var directReleasesWithApprovalRole = await _context + .UserReleaseRoles + .Where(role => role.UserId == userId && role.Role == ReleaseRole.Approver) + .Select(role => role.ReleaseId) + .ToListAsync(); + + var indirectReleasesWithApprovalRole = await _context + .UserPublicationRoles + .Include(role => role.Publication) + .ThenInclude(publication => publication.Releases) + .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) + .Select(role => role.Publication) + .SelectMany(publication => publication.Releases.Select(release => release.Id)) + .ToListAsync(); + + var releaseIdsForApproval = directReleasesWithApprovalRole + .Concat(indirectReleasesWithApprovalRole) + .Distinct(); + + var releasesForApproval = await _context + .Releases + .Where(release => + release.ApprovalStatus == ReleaseApprovalStatus.HigherLevelReview + && releaseIdsForApproval.Contains(release.Id)) + .ToListAsync(); + + return releasesForApproval + .Select(release => { + var viewModel = _mapper.Map(release); + // TODO DW - need any permissions adding? + return viewModel; + }) + .ToList(); + } + public async Task>> ListScheduledReleases() { return await _userService diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/InstanceSetters.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/InstanceSetters.cs index c7488abbac4..74db07f1ac7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/InstanceSetters.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/InstanceSetters.cs @@ -81,6 +81,14 @@ public InstanceSetters SetDefault(Expression> property) ); } + public InstanceSetters SetDefault(Expression> property) + { + return Set( + property, + (faker, _, context) => GetDisplayIndex(context, faker) + ); + } + private static int GetDisplayIndex(SetterContext context, Faker faker) => context.FixtureTypeIndex > 0 ? context.FixtureTypeIndex : faker.IndexFaker; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Security/UserService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Security/UserService.cs index cad03b63621..babeb548652 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Security/UserService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Security/UserService.cs @@ -1,3 +1,4 @@ +#nullable enable using System; using System.Security.Claims; using System.Threading.Tasks; @@ -33,9 +34,9 @@ public Guid GetUserId() return GetUser().GetUserId(); } - private ClaimsPrincipal GetUser() + private ClaimsPrincipal? GetUser() { - return _httpContextAccessor.HttpContext.User; + return _httpContextAccessor.HttpContext?.User; } } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/PublicationGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/PublicationGeneratorExtensions.cs new file mode 100644 index 00000000000..f7c589a80c6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/PublicationGeneratorExtensions.cs @@ -0,0 +1,53 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class PublicationGeneratorExtensions +{ + public static Generator DefaultPublication(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.Slug) + .SetDefault(p => p.Summary) + .SetDefault(p => p.Title); + + public static Generator WithReleases( + this Generator generator, + IEnumerable releases) + => generator.ForInstance(s => s.SetReleases(releases)); + + public static Generator WithReleases( + this Generator generator, + Func> releases) + => generator.ForInstance(s => s.SetReleases(releases.Invoke)); + + public static InstanceSetters SetReleases( + this InstanceSetters setters, + IEnumerable releases) + => setters.SetReleases(_ => releases); + + private static InstanceSetters SetReleases( + this InstanceSetters setters, + Func> releases) + => setters.Set( + p => p.Releases, + (_, publication, context) => + { + var list = releases.Invoke(context).ToList(); + + list.ForEach(release => release.Publication = publication); + + return list; + } + ); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs new file mode 100644 index 00000000000..1fe948f5ad6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs @@ -0,0 +1,40 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class ReleaseGeneratorExtensions +{ + public static Generator DefaultRelease(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static Generator WithApprovalStatus(this Generator generator, ReleaseApprovalStatus status) + => generator.ForInstance(d => d.SetApprovalStatus(status)); + + public static Generator WithApprovalStatuses(this Generator generator, IEnumerable statuses) + { + statuses.ForEach((status, index) => + generator.ForIndex(index, s => s.SetApprovalStatus(status))); + + return generator; + } + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.Slug) + .SetDefault(p => p.Title) + .SetDefault(p => p.DataGuidance) + .Set(p => p.ReleaseName, (_, _, context) => $"{1000 + context.Index}"); + + public static InstanceSetters SetApprovalStatus( + this InstanceSetters setters, + ReleaseApprovalStatus status) + => setters.Set(d => d.ApprovalStatus, status); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserPublicationRoleGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserPublicationRoleGeneratorExtensions.cs new file mode 100644 index 00000000000..6e542c7d7d4 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserPublicationRoleGeneratorExtensions.cs @@ -0,0 +1,62 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class UserPublicationRoleGeneratorExtensions +{ + public static Generator DefaultUserPublicationRole(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static Generator WithPublication(this Generator generator, Publication Publication) + => generator.ForInstance(d => d.SetPublication(Publication)); + + public static Generator WithPublications(this Generator generator, IEnumerable Publications) + { + Publications.ForEach((Publication, index) => + generator.ForIndex(index, s => s.SetPublication(Publication))); + + return generator; + } + + public static Generator WithUser(this Generator generator, User user) + => generator.ForInstance(d => d.SetUser(user)); + + public static Generator WithRole(this Generator generator, PublicationRole role) + => generator.ForInstance(d => d.SetRole(role)); + + public static Generator WithRoles(this Generator generator, IEnumerable roles) + { + roles.ForEach((role, index) => + generator.ForIndex(index, s => s.SetRole(role))); + + return generator; + } + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.PublicationId) + .SetDefault(p => p.UserId); + + public static InstanceSetters SetPublication( + this InstanceSetters setters, + Publication Publication) + => setters.Set(d => d.Publication, Publication); + + public static InstanceSetters SetUser( + this InstanceSetters setters, + User user) + => setters.Set(d => d.User, user); + + public static InstanceSetters SetRole( + this InstanceSetters setters, + PublicationRole role) + => setters.Set(d => d.Role, role); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserReleaseRoleGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserReleaseRoleGeneratorExtensions.cs new file mode 100644 index 00000000000..9e578dcc949 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/UserReleaseRoleGeneratorExtensions.cs @@ -0,0 +1,62 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class UserReleaseRoleGeneratorExtensions +{ + public static Generator DefaultUserReleaseRole(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static Generator WithRelease(this Generator generator, Release release) + => generator.ForInstance(d => d.SetRelease(release)); + + public static Generator WithReleases(this Generator generator, IEnumerable releases) + { + releases.ForEach((release, index) => + generator.ForIndex(index, s => s.SetRelease(release))); + + return generator; + } + + public static Generator WithUser(this Generator generator, User user) + => generator.ForInstance(d => d.SetUser(user)); + + public static Generator WithRole(this Generator generator, ReleaseRole role) + => generator.ForInstance(d => d.SetRole(role)); + + public static Generator WithRoles(this Generator generator, IEnumerable roles) + { + roles.ForEach((role, index) => + generator.ForIndex(index, s => s.SetRole(role))); + + return generator; + } + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.ReleaseId) + .SetDefault(p => p.UserId); + + public static InstanceSetters SetRelease( + this InstanceSetters setters, + Release release) + => setters.Set(d => d.Release, release); + + public static InstanceSetters SetUser( + this InstanceSetters setters, + User user) + => setters.Set(d => d.User, user); + + public static InstanceSetters SetRole( + this InstanceSetters setters, + ReleaseRole role) + => setters.Set(d => d.Role, role); +} From 1fb27689bb886fe62647ba3f2f4297c9a132c13d Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Mon, 14 Aug 2023 12:52:21 +0100 Subject: [PATCH 03/22] EES-4448 - added new services and API endpoints for listing MethodologyVersions for Approval --- .../MethodologyControllerTests.cs | 39 ++- .../Methodologies/MethodologyServiceTests.cs | 296 ++++++++++++++++++ .../Methodologies/MethodologyController.cs | 14 +- .../Methodologies/IMethodologyService.cs | 2 + .../Methodologies/MethodologyService.cs | 23 ++ .../MethodologyGeneratorExtensions.cs | 92 ++++++ .../MethodologyVersionGeneratorExtensions.cs | 44 +++ 7 files changed, 505 insertions(+), 5 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyVersionGeneratorExtensions.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs index 2d060b394b0..3aa33e77dca 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs @@ -6,6 +6,7 @@ using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodology; using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using Microsoft.AspNetCore.Mvc; using Moq; @@ -245,15 +246,45 @@ public async void DeleteMethodologyVersion_Returns_NoContent() result.AssertNoContent(); } + [Fact] + public async void ListMethodologyVersionsForApproval() + { + var userId = Guid.NewGuid(); + var methodologyVersions = ListOf(new MethodologyVersionViewModel + { + Id = Guid.NewGuid() + }); + + var userService = new Mock(Strict); + var methodologyService = new Mock(Strict); + + userService + .Setup(s => s.GetUserId()) + .Returns(userId); + + methodologyService + .Setup(s => s.ListMethodologyVersionsForApproval(userId)) + .ReturnsAsync(methodologyVersions); + + var controller = SetupMethodologyController( + methodologyService.Object, + userService: userService.Object); + + var result = await controller.ListMethodologyVersionsForApproval(); + VerifyAllMocks(userService, methodologyService); + + result.AssertOkResult(methodologyVersions); + } + private static MethodologyController SetupMethodologyController( IMethodologyService? methodologyService = null, - IMethodologyAmendmentService? methodologyAmendmentService = null - ) + IMethodologyAmendmentService? methodologyAmendmentService = null, + IUserService? userService = null) { return new( methodologyService ?? Mock.Of(Strict), - methodologyAmendmentService ?? Mock.Of(Strict) - ); + methodologyAmendmentService ?? Mock.Of(Strict), + userService ?? Mock.Of(Strict)); } } } 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 c4faff6a4f9..967f397814c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -9,10 +9,12 @@ using GovUk.Education.ExploreEducationStatistics.Common.Model; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Common.Utils; 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.Model.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Content.Services.Interfaces.Cache; using GovUk.Education.ExploreEducationStatistics.Content.Services.ViewModels; using Microsoft.AspNetCore.Mvc; @@ -2338,6 +2340,300 @@ public async Task GetMethodologyStatuses_NoStatuses() } } + public class ListMethodologyVersionsForApproval + { + private readonly DataFixture _fixture = new(); + + [Fact] + public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPublication_Included() + { + var user = new User(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(HigherLevelReview) + .Generate(1)) + .Generate(); + + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithPublication(publication) + .WithRole(PublicationRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListMethodologyVersionsForApproval(user.Id); + var methodologyVersionsForApproval = result.AssertRight(); + + var methodologyForApproval = Assert.Single(methodologyVersionsForApproval); + Assert.Equal(methodology.Versions[0].Id, methodologyForApproval.Id); + } + } + + [Fact] + public async Task ListMethodologyVersionsForApproval_MethodologyVersionNotInHigherReview_NotIncluded() + { + var user = new User(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + // Generate 2 Methodologies that are not in Higher Review. + var methodologies = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .ForIndex(0, s => s.SetMethodologyVersions(_fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(Draft) + .Generate(1))) + .ForIndex(1, s => s.SetMethodologyVersions(_fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(Approved) + .Generate(1))) + .GenerateList(); + + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithPublication(publication) + .WithRole(PublicationRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodologies); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListMethodologyVersionsForApproval(user.Id); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task ListMethodologyVersionsForApproval_UserIsApproverButOnAdoptingPublication_NotIncluded() + { + var user = new User(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + // Create a Methodology that has only been adopted by the User's Publication. + var methodology = _fixture + .DefaultMethodology() + .WithAdoptingPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(HigherLevelReview) + .Generate(1)) + .Generate(); + + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithPublication(publication) + .WithRole(PublicationRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListMethodologyVersionsForApproval(user.Id); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task ListMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPublication_NotIncluded() + { + var user = new User(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(HigherLevelReview) + .Generate(1)) + .Generate(); + + // Set up the User as an Owner on the Methodology's Publication rather than an Approver. + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .WithPublication(publication) + .WithRole(PublicationRole.Owner) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListMethodologyVersionsForApproval(user.Id); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task ListMethodologyVersionsForApproval_DifferentUserIsApproverOnOwningPublication_NotIncluded() + { + // Set up a different User as the Approver for the owning Publication. + var otherUser = new User(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(HigherLevelReview) + .Generate(1)) + .Generate(); + + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(otherUser) + .WithPublication(publication) + .WithRole(PublicationRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListMethodologyVersionsForApproval(Guid.NewGuid()); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() + { + var user = new User(); + + var publications = _fixture + .DefaultPublication() + .GenerateList(2); + + var owningPublication = publications[0]; + var adoptingPublication = publications[1]; + + var ownedMethodologies = _fixture + .DefaultMethodology() + .WithOwningPublication(owningPublication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatuses(ListOf(Approved, HigherLevelReview, Draft)) + .GenerateList()) + .GenerateList(2); + + var adoptedMethodologies = _fixture + .DefaultMethodology() + .WithAdoptingPublication(adoptingPublication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatuses(ListOf(Approved, HigherLevelReview, Draft)) + .GenerateList()) + .Generate(2); + + var publicationRolesForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(user) + .ForIndex(0, s => s + .SetPublication(owningPublication) + .SetRole(PublicationRole.Approver)) + .ForIndex(1, s => s + .SetPublication(owningPublication) + .SetRole(PublicationRole.Owner)) + .ForIndex(2, s => s + .SetPublication(adoptingPublication) + .SetRole(PublicationRole.Approver)) + .ForIndex(3, s => s + .SetPublication(adoptingPublication) + .SetRole(PublicationRole.Owner)) + .GenerateList(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(ownedMethodologies); + await context.Methodologies.AddRangeAsync(adoptedMethodologies); + await context.UserPublicationRoles.AddRangeAsync(publicationRolesForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListMethodologyVersionsForApproval(user.Id); + var methodologyVersionsForApproval = result.AssertRight(); + + // Assert that we just get the 2 MethodologyVersions where the user is the Approver of the Owning + // Publication and their statuses are Higher Review. + Assert.Equal(2, methodologyVersionsForApproval.Count); + Assert.Equal(ownedMethodologies[0].Versions[1].Id, methodologyVersionsForApproval[0].Id); + Assert.Equal(ownedMethodologies[1].Versions[1].Id, methodologyVersionsForApproval[1].Id); + } + } + } + private static MethodologyService SetupMethodologyService( ContentDbContext contentDbContext, IPersistenceHelper? persistenceHelper = null, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs index 0f98ab49a3b..a6c6db372e4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs @@ -7,6 +7,7 @@ using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodology; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,13 +20,16 @@ public class MethodologyController : ControllerBase { private readonly IMethodologyService _methodologyService; private readonly IMethodologyAmendmentService _methodologyAmendmentService; + private readonly IUserService _userService; public MethodologyController( IMethodologyService methodologyService, - IMethodologyAmendmentService methodologyAmendmentService) + IMethodologyAmendmentService methodologyAmendmentService, + IUserService userService) { _methodologyService = methodologyService; _methodologyAmendmentService = methodologyAmendmentService; + _userService = userService; } [HttpPut("publication/{publicationId:guid}/methodology/{methodologyId:guid}")] @@ -119,5 +123,13 @@ public Task>> GetMethodologyStatus .GetMethodologyStatuses(methodologyVersionId) .HandleFailuresOrOk(); } + + [HttpGet("methodology/approvals")] + public async Task>> ListMethodologyVersionsForApproval() + { + return await _methodologyService + .ListMethodologyVersionsForApproval(_userService.GetUserId()) + .HandleFailuresOrOk(); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs index c05736e71cc..b1ab03eb0cd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs @@ -40,5 +40,7 @@ Task BuildMethodologyVersionViewModel( MethodologyVersion methodologyVersion); Task>> GetMethodologyStatuses(Guid methodologyVersionId); + + Task>> ListMethodologyVersionsForApproval(Guid userId); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index 26b39b04d49..70da939a895 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -415,6 +415,29 @@ public Task>> GetMethodolo }); } + public async Task>> ListMethodologyVersionsForApproval(Guid userId) + { + var publicationIdsForApprover = _context + .UserPublicationRoles + .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) + .Select(role => role.PublicationId); + + var methodologiesToApprove = _context + .MethodologyVersions + .Include(methodologyVersion => methodologyVersion.Methodology) + .ThenInclude(methodology => methodology.Publications) + .ToList() + .Where(methodologyVersion => + methodologyVersion.Status == MethodologyApprovalStatus.HigherLevelReview + && methodologyVersion.Methodology.Publications.Any( + publicationMethodology => + publicationMethodology.Owner + && publicationIdsForApprover.Contains(publicationMethodology.PublicationId))) + // .ToListAsync(); + ; + return methodologiesToApprove.Select(_mapper.Map).ToList(); + } + private async Task> DeleteVersion(MethodologyVersion methodologyVersion, bool forceDelete = false) { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs new file mode 100644 index 00000000000..363f0b87267 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyGeneratorExtensions.cs @@ -0,0 +1,92 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class MethodologyGeneratorExtensions +{ + public static Generator DefaultMethodology(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.Slug); + + public static Generator WithOwningPublication( + this Generator generator, + Publication publication) + => generator.ForInstance(s => s.SetOwningPublication(publication)); + + public static Generator WithAdoptingPublication( + this Generator generator, + Publication publication) + => generator.ForInstance(s => s.SetAdoptingPublication(publication)); + + public static Generator WithMethodologyVersions( + this Generator generator, + IEnumerable methodologyVersions) + => generator.ForInstance(s => s.SetMethodologyVersions(methodologyVersions)); + + public static Generator WithMethodologyVersions( + this Generator generator, + Func> methodologyVersions) + => generator.ForInstance(s => s.SetMethodologyVersions(methodologyVersions.Invoke)); + + public static InstanceSetters SetMethodologyVersions( + this InstanceSetters setters, + IEnumerable methodologyVersions) + => setters.SetMethodologyVersions(_ => methodologyVersions); + + private static InstanceSetters SetMethodologyVersions( + this InstanceSetters setters, + Func> methodologyVersions) + => setters.Set( + m => m.Versions, + (_, methodology, context) => + { + var list = methodologyVersions.Invoke(context).ToList(); + + list.ForEach(methodologyVersion => methodologyVersion.Methodology = methodology); + + return list; + } + ); + + public static InstanceSetters SetOwningPublication( + this InstanceSetters setters, + Publication publication) + => setters.SetPublication(_ => publication, owner: true); + + public static InstanceSetters SetAdoptingPublication( + this InstanceSetters setters, + Publication publication) + => setters.SetPublication(_ => publication, owner: false); + + private static InstanceSetters SetPublication( + this InstanceSetters setters, + Func publication, + bool owner) + => setters.Set( + m => m.Publications, + (_, methodology, context) => + { + var newPublication = publication.Invoke(context); + return methodology + .Publications + .Append(new PublicationMethodology + { + Methodology = methodology, + Publication = newPublication, + Owner = owner + }) + .ToList(); + } + ); +} diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyVersionGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyVersionGeneratorExtensions.cs new file mode 100644 index 00000000000..9db9ccbc1cb --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/MethodologyVersionGeneratorExtensions.cs @@ -0,0 +1,44 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; + +namespace GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Fixtures; + +public static class MethodologyVersionGeneratorExtensions +{ + public static Generator DefaultMethodologyVersion(this DataFixture fixture) + => fixture.Generator().WithDefaults(); + + public static Generator WithDefaults(this Generator generator) + => generator.ForInstance(d => d.SetDefaults()); + + public static Generator WithApprovalStatus( + this Generator generator, + MethodologyApprovalStatus approvalStatus) + => generator.ForInstance(d => d.SetApprovalStatus(approvalStatus)); + + public static Generator WithApprovalStatuses( + this Generator generator, + IEnumerable approvalStatuses) + { + approvalStatuses.ForEach((status, index) => + generator.ForIndex(index, s => s.SetApprovalStatus(status))); + + return generator; + } + + public static InstanceSetters SetDefaults(this InstanceSetters setters) + => setters + .SetDefault(p => p.Id) + .SetDefault(p => p.Slug) + .SetDefault(p => p.Title) + .SetDefault(p => p.Version); + + public static InstanceSetters SetApprovalStatus( + this InstanceSetters setters, + MethodologyApprovalStatus approvalStatus) + => setters.Set(mv => mv.Status, approvalStatus); +} From 8bb0ebec388e88cee8b355602de428089e90514f Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Tue, 22 Aug 2023 16:48:05 +0100 Subject: [PATCH 04/22] EES-4448 - added "IsApprover" to global permissions to allow the new "Your approvals" tab to be included in the Admin Dashboard. Amended Admin TestStartup.cs to extend Startup.cs to make real services available to integration tests. Added new integration tests for PermissionsController to test global permission assignments. --- .../Security/PermissionsControllerTests.cs | 196 ++++++++++++++++++ .../Statistics/TableBuilderControllerTests.cs | 20 +- ...loreEducationStatistics.Admin.Tests.csproj | 7 + .../Security/Utils/ClaimsPrincipalUtils.cs | 70 ++++++- .../TestStartup.cs | 172 ++++++++++----- .../integration-test-settings.json | 8 + .../Api/Security/PermissionsController.cs | 28 ++- .../Startup.cs | 138 +++++++----- .../ViewModels/GlobalPermissionsViewModel.cs | 3 +- .../Fixtures/TestApplicationFactory.cs | 35 +++- 10 files changed, 549 insertions(+), 128 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs new file mode 100644 index 00000000000..0cecba9a2cb --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs @@ -0,0 +1,196 @@ +#nullable enable +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels; +using GovUk.Education.ExploreEducationStatistics.Common.Services.Security; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; +using GovUk.Education.ExploreEducationStatistics.Content.Model; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils; +using Microsoft.AspNetCore.Mvc.Testing; +using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; + +namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Controllers.Api.Security; + +public class PermissionsControllerTests : IClassFixture> +{ + private readonly WebApplicationFactory _testApp; + + public PermissionsControllerTests(TestApplicationFactory testApp) + { + _testApp = testApp; + } + + [Fact] + public async Task GetGlobalPermissions_AuthenticatedUser() + { + var user = AuthenticatedUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: false, + CanAccessAllImports: false, + CanAccessPrereleasePages: false, + CanManageAllTaxonomy: false, + IsBauUser: false, + IsApprover: false); + + var client = SetupApp().SetUser(user).CreateClient(); + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_BauUser() + { + var user = BauUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: true, + CanAccessAllImports: true, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: true, + IsBauUser: true, + // Expect "IsApprover" to be false even for BAU as we don't expect BAU users to be assigned + // individual Approver roles on Releases or Publications. + IsApprover: false); + + var client = SetupApp().SetUser(user).CreateClient(); + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_AnalystUser_NotReleaseOrPublicationApprover() + { + var user = AnalystUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: true, + CanAccessAllImports: false, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: false, + IsBauUser: false, + // Expect this to be false if the user isn't an approver of any kind + IsApprover: false); + + var client = SetupApp() + .SetUser(user) + .AddContentDbTestData(context => + { + // Add test data that gives the user access to a Release without being an Approver. + context.UserReleaseRoles.Add(new UserReleaseRole + { + UserId = user.GetUserId(), + Role = ReleaseRole.Contributor + }); + + // Add test data that gives the user access to a Publication without being an Approver. + context.UserPublicationRoles.Add(new UserPublicationRole + { + UserId = user.GetUserId(), + Role = PublicationRole.Owner + }); + }) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_AnalystUser_ReleaseApprover() + { + var user = AnalystUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: true, + CanAccessAllImports: false, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: false, + IsBauUser: false, + // Expect this to be true if the user is a Release approver + IsApprover: true); + + var client = SetupApp() + .SetUser(user) + .AddContentDbTestData(context => + { + context.UserReleaseRoles.Add(new UserReleaseRole + { + UserId = user.GetUserId(), + Role = ReleaseRole.Approver + }); + }) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_AnalystUser_PublicationApprover() + { + var user = AnalystUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: true, + CanAccessAllImports: false, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: false, + IsBauUser: false, + // Expect this to be true if the user is a Publication approver + IsApprover: true); + + var client = SetupApp() + .SetUser(user) + .AddContentDbTestData(context => + { + context.UserPublicationRoles.Add(new UserPublicationRole + { + UserId = user.GetUserId(), + Role = PublicationRole.Approver + }); + }) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_PreReleaseUser() + { + var user = PreReleaseUser(); + + var expectedPermissions = new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: false, + CanAccessAllImports: false, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: false, + IsBauUser: false, + IsApprover: false); + + var client = SetupApp().SetUser(user).CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + response.AssertOk(expectedPermissions); + } + + [Fact] + public async Task GetGlobalPermissions_UnauthenticatedUser() + { + var response = await SetupApp().CreateClient().GetAsync("/api/permissions/access"); + response.AssertUnauthorized(); + } + + private WebApplicationFactory SetupApp() + { + return _testApp.ResetDbContexts(); + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs index 343f3c6749d..fb243cc2485 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs @@ -12,7 +12,6 @@ using GovUk.Education.ExploreEducationStatistics.Common.Model.Chart; using GovUk.Education.ExploreEducationStatistics.Common.Model.Data.Query; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; -using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Common.Utils; @@ -26,11 +25,11 @@ using Microsoft.Net.Http.Headers; using Moq; using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils.ClaimsPrincipalUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; using static Moq.MockBehavior; using static Newtonsoft.Json.JsonConvert; -using Release = GovUk.Education.ExploreEducationStatistics.Content.Model.Release; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Controllers.Api.Statistics { @@ -103,7 +102,9 @@ public async Task Query() It.IsAny())) .ReturnsAsync(_tableBuilderResults); - var client = SetupApp(tableBuilderService: tableBuilderService.Object) + var client = SetupApp( + tableBuilderService: tableBuilderService.Object) + .SetUser(AuthenticatedUser()) .CreateClient(); var response = await client.PostAsync( @@ -130,6 +131,7 @@ public async Task Query_Csv() (_, _, stream, _) => { stream.WriteText("Test csv"); }); var client = SetupApp(tableBuilderService: tableBuilderService.Object) + .SetUser(AuthenticatedUser()) .CreateClient(); var response = await client.PostAsync( @@ -277,18 +279,12 @@ private static (TableBuilderController controller, } private WebApplicationFactory SetupApp( - ITableBuilderService? tableBuilderService = null, - IUserService? userService = null) + ITableBuilderService? tableBuilderService = null) { return _testApp .ResetDbContexts() - .ConfigureServices( - services => - { - services.AddTransient(_ => tableBuilderService ?? Mock.Of(Strict));// - services.AddTransient(_ => userService ?? AlwaysTrueUserService().Object); - } - ); + .ConfigureServices(services => services + .AddTransient(_ => tableBuilderService ?? Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj index 6e73f75dc9a..f8e0cd847f6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj @@ -40,4 +40,11 @@ + + + + Always + + + diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs index dcd5bb20d05..265bb1e3421 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs @@ -1,12 +1,59 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Security.Claims; using GovUk.Education.ExploreEducationStatistics.Admin.Security; +using GovUk.Education.ExploreEducationStatistics.Common.Services; +using IdentityModel; +using static GovUk.Education.ExploreEducationStatistics.Admin.Models.GlobalRoles; +using static GovUk.Education.ExploreEducationStatistics.Admin.Models.GlobalRoles.RoleNames; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; +using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Security.Utils { public static class ClaimsPrincipalUtils { + public static ClaimsPrincipal AuthenticatedUser() + { + return CreateClaimsPrincipal( + Guid.NewGuid(), + SecurityClaim(ApplicationAccessGranted)); + } + + public static ClaimsPrincipal BauUser() + { + // Give the BAU User the "BAU User" role, plus every Claim from the SecurityClaimTypes enum. + var claims = + ListOf(RoleClaim(RoleNames.BauUser)) + .Concat(EnumUtil.GetEnumValues().Select(SecurityClaim)); + + return CreateClaimsPrincipal( + Guid.NewGuid(), + claims.ToArray()); + } + + public static ClaimsPrincipal AnalystUser() + { + return CreateClaimsPrincipal( + Guid.NewGuid(), + RoleClaim(Analyst), + SecurityClaim(ApplicationAccessGranted), + SecurityClaim(AnalystPagesAccessGranted), + SecurityClaim(PrereleasePagesAccessGranted), + SecurityClaim(CanViewPrereleaseContacts)); + } + + public static ClaimsPrincipal PreReleaseUser() + { + return CreateClaimsPrincipal( + Guid.NewGuid(), + RoleClaim(PrereleaseUser), + SecurityClaim(ApplicationAccessGranted), + SecurityClaim(PrereleasePagesAccessGranted)); + + } + public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId) { return CreateClaimsPrincipal(userId, new Claim[] { }); @@ -14,7 +61,12 @@ public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId) public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId, params Claim[] additionalClaims) { - var identity = new ClaimsIdentity(); + var identity = new ClaimsIdentity( + new List(), + "TestAuthenticationType", + JwtClaimTypes.Name, + JwtClaimTypes.Role); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); identity.AddClaims(additionalClaims); var user = new ClaimsPrincipal(identity); @@ -26,5 +78,21 @@ public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId, params Security return CreateClaimsPrincipal(userId, additionalClaims.Select(c => new Claim(c.ToString(), "")).ToArray()); } + + /// + /// Create a Claim representing a SecurityClaimTypes enum value. + /// + private static Claim SecurityClaim(SecurityClaimTypes type) + { + return new Claim(type.ToString(), ""); + } + + /// + /// Create a Claim representing a Global Role (i.e. an AspNetUserRoles assignment). + /// + private static Claim RoleClaim(string roleName) + { + return new Claim(JwtClaimTypes.Role, roleName); + } } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs index acba7527584..02bbc4ca38b 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs @@ -1,74 +1,101 @@ #nullable enable -using GovUk.Education.ExploreEducationStatistics.Admin.Security; -using GovUk.Education.ExploreEducationStatistics.Common.ModelBinding; +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils; -using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Model.Tests.Utils; -using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using static GovUk.Education.ExploreEducationStatistics.Common.Utils.StartupUtils; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests; /// /// Generic test application startup for use in integration tests. +/// +/// This startup inherits from the real production process to make available as realistic a +/// set of services as possible, but mocks out services that interact with Azure services and registers in-memory +/// DbContexts in place of the real DbContexts. Additionally this startup configuration does not attempt to start +/// up the SPA, which requires NPM and needs a more involved and lengthy startup process that is not useful for +/// integration tests. /// /// /// Use in combination with /// as a test class fixture. /// -public class TestStartup +// ReSharper disable once ClassNeverInstantiated.Global +public class TestStartup : Startup { - public void ConfigureServices(IServiceCollection services) + public const string AuthenticationScheme = "TestAuthenticationScheme"; + + public TestStartup( + IConfiguration configuration, + IHostEnvironment hostEnvironment) : base(configuration, hostEnvironment) { - services.AddMvcCore(options => { options.EnableEndpointRouting = false; }) - .AddNewtonsoftJson( - options => { options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; } - ); - - StartupSecurityConfiguration.ConfigureAuthorizationPolicies(services); - // TODO: EES-4338 Emulate non-test authentication/authorisation - services.AddAuthorization(options => - { - var policy = new AuthorizationPolicyBuilder() - .RequireAssertion(c => true) - .Build(); - - options.DefaultPolicy = policy; - }); - - services.AddControllers( - options => { options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(",")); } - ) - .AddApplicationPart(typeof(Startup).Assembly); - - services.AddDbContext( - options => - options.UseInMemoryDatabase( - "TestStatisticsDb", - b => b.EnableNullChecks(false))); - - services.AddDbContext( - options => - options.UseInMemoryDatabase( - "TestContentDb", - b => b.EnableNullChecks(false) - ) - ); - - AddPersistenceHelper(services); - AddPersistenceHelper(services); } - public void Configure(IApplicationBuilder app) + protected override IStorageQueueService GetStorageQueueService() + { + return Mock.Of(Strict); + } + + protected override ITableStorageService GetTableStorageService() + { + return Mock.Of(Strict); + } + + protected override IPrivateBlobStorageService GetPrivateBlobStorageService(IServiceProvider services) + { + return Mock.Of(Strict); + } + + protected override IPublicBlobStorageService GetPublicBlobStorageService(IServiceProvider services) { - app.UseMvc(); + return Mock.Of(Strict); + } + + protected override DbContextOptionsBuilder GetStatisticsDbContext(DbContextOptionsBuilder options) + { + return AddDbContext(options, "TestStatisticsDb"); + } + + protected override DbContextOptionsBuilder GetContentDbContext(DbContextOptionsBuilder options) + { + return AddDbContext(options, "TestContentDb"); + } + + protected override DbContextOptionsBuilder GetUsersAndRolesDbContext(DbContextOptionsBuilder options) + { + return AddDbContext(options, "TestUsersAndRolesDb"); + } + + protected override void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) + { + // Nothing to do here for in-memory database contexts. + } + + protected override void ConfigureDevelopmentSpaServer(IApplicationBuilder app, IWebHostEnvironment env) + { + // Don't attempt to configure the SPA server during integration tests. + } + + private static DbContextOptionsBuilder AddDbContext(DbContextOptionsBuilder options, string name) + { + return options.UseInMemoryDatabase( + name, b => b.EnableNullChecks(false)); } } @@ -80,4 +107,53 @@ public static WebApplicationFactory ResetDbContexts(this WebApplica .ResetContentDbContext() .ResetStatisticsDbContext(); } + + public static WebApplicationFactory SetUser( + this WebApplicationFactory testApp, + ClaimsPrincipal? user = null) + { + return testApp.WithWebHostBuilder(builder => builder + .ConfigureTestServices(services => + { + services + .AddAuthentication(TestStartup.AuthenticationScheme) + .AddScheme(TestStartup.AuthenticationScheme, _ => { }); + + if (user != null) + { + services.AddScoped(_ => user); + } + })); + } } + +/// +/// An AuthenticationHandler that allows the tests to make a ClaimsPrincipal available in the HttpContext +/// for authentication and authorization mechanisms to use. +/// +internal class TestAuthHandler : AuthenticationHandler +{ + private readonly ClaimsPrincipal? _claimsPrincipal; + + public TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock, + ClaimsPrincipal? claimsPrincipal) : base(options, logger, encoder, clock) + { + _claimsPrincipal = claimsPrincipal; + } + + protected override Task HandleAuthenticateAsync() + { + if (_claimsPrincipal == null) + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + var ticket = new AuthenticationTicket(_claimsPrincipal, TestStartup.AuthenticationScheme); + var result = AuthenticateResult.Success(ticket); + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json new file mode 100644 index 00000000000..25ab018279e --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json @@ -0,0 +1,8 @@ +{ + "OpenIdConnect": { + "ClientId": "ees-admin-client", + "ClientSecret": "eaa4cc70-e80b-4a7e-a20f-2856d97f470d", + "Authority": "http://ees.local:5030/auth/realms/ees-realm", + "RequireHttpsMetadata": false + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs index 9b18dd5382f..14daf24b9dc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs @@ -25,28 +25,38 @@ public class PermissionsController : ControllerBase private readonly IReleaseFileService _releaseFileService; private readonly IUserService _userService; private readonly IPreReleaseService _preReleaseService; + private readonly IUserPublicationRoleRepository _publicationRoleRepository; + private readonly IUserReleaseRoleRepository _releaseRoleRepository; - public PermissionsController(IPersistenceHelper persistenceHelper, + public PermissionsController( + IPersistenceHelper persistenceHelper, IReleaseFileService releaseFileService, IUserService userService, - IPreReleaseService preReleaseService) + IPreReleaseService preReleaseService, + IUserPublicationRoleRepository publicationRoleRepository, + IUserReleaseRoleRepository releaseRoleRepository) { _persistenceHelper = persistenceHelper; _releaseFileService = releaseFileService; _userService = userService; _preReleaseService = preReleaseService; + _publicationRoleRepository = publicationRoleRepository; + _releaseRoleRepository = releaseRoleRepository; } [HttpGet("permissions/access")] public async Task> GetGlobalPermissions() { + var isBauUser = await _userService.CheckIsBauUser().IsRight(); + return new GlobalPermissionsViewModel( CanAccessSystem: await _userService.CheckCanAccessSystem().IsRight(), CanAccessAnalystPages: await _userService.CheckCanAccessAnalystPages().IsRight(), CanAccessAllImports: await _userService.CheckCanViewAllImports().IsRight(), CanAccessPrereleasePages: await _userService.CheckCanAccessPrereleasePages().IsRight(), CanManageAllTaxonomy: await _userService.CheckCanManageAllTaxonomy().IsRight(), - IsBauUser: await _userService.CheckIsBauUser().IsRight()); + IsBauUser: isBauUser, + IsApprover: !isBauUser && (await IsReleaseApprover() || await IsPublicationApprover())); } [HttpGet("permissions/topic/{topicId:guid}/publication/create")] @@ -161,5 +171,17 @@ private async Task> CheckPolicyAgainstEntity( .OnSuccess(_ => new OkObjectResult(true)) .OrElse(() => new OkObjectResult(false)); } + + private async Task IsReleaseApprover() + { + return (await _releaseRoleRepository.GetDistinctRolesByUser(_userService.GetUserId())) + .Contains(ReleaseRole.Approver); + } + + private async Task IsPublicationApprover() + { + return (await _publicationRoleRepository.GetDistinctRolesByUser(_userService.GetUserId())) + .Contains(PublicationRole.Approver); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 15303fb85f0..1b4160180e8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -158,6 +158,7 @@ public void ConfigureServices(IServiceCollection services) .AddControllers( options => { options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(",")); } ) + .AddApplicationPart(typeof(Startup).Assembly) .AddControllersAsServices(); services.AddHttpContextAccessor(); @@ -188,39 +189,9 @@ public void ConfigureServices(IServiceCollection services) * Database contexts */ - services.AddDbContext(options => - options - .UseSqlServer(Configuration.GetConnectionString("ContentDb"), - providerOptions => - providerOptions - .MigrationsAssembly(typeof(Startup).Assembly.FullName) - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) - ); - - services.AddDbContext(options => - options - .UseSqlServer(Configuration.GetConnectionString("ContentDb"), - providerOptions => - providerOptions - .MigrationsAssembly(typeof(Startup).Assembly.FullName) - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) - ); - - services.AddDbContext(options => - options - .UseSqlServer(Configuration.GetConnectionString("StatisticsDb"), - providerOptions => - providerOptions - .MigrationsAssembly("GovUk.Education.ExploreEducationStatistics.Data.Model") - .AddBulkOperationSupport() - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) - ); + services.AddDbContext(options => GetUsersAndRolesDbContext(options)); + services.AddDbContext(options => GetContentDbContext(options)); + services.AddDbContext(options => GetStatisticsDbContext(options)); /* * Auth / IdentityServer @@ -413,7 +384,6 @@ public void ConfigureServices(IServiceCollection services) * Services */ - var coreStorageConnectionString = Configuration.GetValue("CoreStorage"); var publisherStorageConnectionString = Configuration.GetValue("PublisherStorage"); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); @@ -597,17 +567,12 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(_ => - new TableStorageService( - coreStorageConnectionString, - new StorageInstanceCreationUtil())); - services.AddTransient(_ => - new StorageQueueService( - coreStorageConnectionString, - new StorageInstanceCreationUtil())); + services.AddSingleton(GetPrivateBlobStorageService); + services.AddSingleton(GetPublicBlobStorageService); + + services.AddTransient(_ => GetTableStorageService()); + services.AddTransient(_ => GetStorageQueueService()); + services.AddTransient(); services.AddSingleton(); AddPersistenceHelper(services); @@ -671,6 +636,69 @@ public void ConfigureServices(IServiceCollection services) }); } + protected virtual IPrivateBlobStorageService GetPrivateBlobStorageService(IServiceProvider services) + { + return new PrivateBlobStorageService( + services.GetRequiredService>(), Configuration); + } + + protected virtual IPublicBlobStorageService GetPublicBlobStorageService(IServiceProvider services) + { + return new PublicBlobStorageService( + services.GetRequiredService>(), Configuration); + } + + protected virtual IStorageQueueService GetStorageQueueService() + { + return new StorageQueueService( + GetCoreStorageConnectionString(), + new StorageInstanceCreationUtil()); + } + + protected virtual ITableStorageService GetTableStorageService() + { + return new TableStorageService( + GetCoreStorageConnectionString(), + new StorageInstanceCreationUtil()); + } + + protected virtual DbContextOptionsBuilder GetStatisticsDbContext(DbContextOptionsBuilder options) + { + return options + .UseSqlServer(Configuration.GetConnectionString("StatisticsDb"), + providerOptions => + providerOptions + .MigrationsAssembly("GovUk.Education.ExploreEducationStatistics.Data.Model") + .AddBulkOperationSupport() + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); + } + + protected virtual DbContextOptionsBuilder GetContentDbContext(DbContextOptionsBuilder options) + { + return options + .UseSqlServer(Configuration.GetConnectionString("ContentDb"), + providerOptions => + providerOptions + .MigrationsAssembly(typeof(Startup).Assembly.FullName) + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); + } + + protected virtual DbContextOptionsBuilder GetUsersAndRolesDbContext(DbContextOptionsBuilder options) + { + return options + .UseSqlServer(Configuration.GetConnectionString("ContentDb"), + providerOptions => + providerOptions + .MigrationsAssembly(typeof(Startup).Assembly.FullName) + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); + } + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -679,15 +707,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // Enable caching and register any caching services. CacheAspect.Enabled = true; var privateCacheService = new BlobCacheService( - new PrivateBlobStorageService( - provider.GetRequiredService>(), - Configuration), + provider.GetRequiredService(), provider.GetRequiredService>() ); var publicCacheService = new BlobCacheService( - new PublicBlobStorageService( - provider.GetRequiredService>(), - Configuration), + provider.GetRequiredService(), provider.GetRequiredService>() ); BlobCacheAttribute.AddService("default", privateCacheService); @@ -791,6 +815,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) template: "{controller}/{action=Index}/{id?}"); }); + ConfigureDevelopmentSpaServer(app, env); + } + + protected virtual void ConfigureDevelopmentSpaServer(IApplicationBuilder app, IWebHostEnvironment env) + { app.UseSpa(spa => { if (env.IsDevelopment()) @@ -805,7 +834,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) .ForEach(address => Console.WriteLine($"Server listening on address: {address}")); } - private void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) + protected virtual void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) { using (var serviceScope = app.ApplicationServices.GetRequiredService() .CreateScope()) @@ -849,5 +878,10 @@ private static void ApplyCustomMigrations(params ICustomMigration[] migrations) migration.Apply(); } } + + private string GetCoreStorageConnectionString() + { + return Configuration.GetValue("CoreStorage"); + } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/GlobalPermissionsViewModel.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/GlobalPermissionsViewModel.cs index b10f1d97c4a..b992d3026c3 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/GlobalPermissionsViewModel.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/GlobalPermissionsViewModel.cs @@ -7,4 +7,5 @@ public record GlobalPermissionsViewModel( bool CanAccessAllImports, bool CanAccessPrereleasePages, bool CanManageAllTaxonomy, - bool IsBauUser); + bool IsBauUser, + bool IsApprover); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs index dee5a072633..c4e292086bc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; @@ -17,18 +18,30 @@ public class TestApplicationFactory : WebApplicationFactory { protected override IHostBuilder CreateHostBuilder() { - return Host.CreateDefaultBuilder() - .ConfigureLogging( - builder => + return Host + .CreateDefaultBuilder() + .ConfigureLogging( + builder => + { + builder + .AddFilter("Default", LogLevel.Warning) + .AddFilter("Microsoft", LogLevel.Warning); + } + ) + .ConfigureWebHostDefaults(builder => { builder - .AddFilter("Default", LogLevel.Warning) - .AddFilter("Microsoft", LogLevel.Warning); - } - ) - .ConfigureWebHostDefaults(builder => - { - builder.UseStartup().UseTestServer(); - }); + .UseStartup() + .UseEnvironment("Development") + .UseTestServer(); + }) + .ConfigureAppConfiguration(config => + { + config.AddConfiguration(new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile("appsettings.Development.json", optional: true) + .AddJsonFile("integration-test-settings.json", optional: true) + .Build()); + }); } } \ No newline at end of file From fa77d3fcbe734d9fdecdc9ed3f2a822972e31159 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Tue, 22 Aug 2023 16:49:23 +0100 Subject: [PATCH 05/22] EES-4448 - integrated frontend code for "Your approvals" tab with backend. --- .../Methodologies/MethodologyServiceTests.cs | 559 +++++++++--------- .../Services/ReleaseServiceTests.cs | 10 +- .../Methodologies/MethodologyService.cs | 13 +- .../Services/ReleaseService.cs | 7 +- .../admin-dashboard/AdminDashboardPage.tsx | 153 +---- .../components/ApprovalsTab.tsx | 2 +- .../src/queries/methodologyQueries.ts | 4 + .../src/queries/releaseQueries.ts | 8 +- .../src/services/methodologyService.ts | 5 + .../src/services/permissionService.ts | 1 + .../src/services/releaseService.ts | 8 +- 11 files changed, 341 insertions(+), 429 deletions(-) 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 967f397814c..33870c8e721 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -3,13 +3,17 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.Security; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Methodologies; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Methodologies; +using GovUk.Education.ExploreEducationStatistics.Admin.Validators; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodology; using GovUk.Education.ExploreEducationStatistics.Common.Model; +using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; @@ -21,16 +25,6 @@ using Microsoft.EntityFrameworkCore; using Moq; using Xunit; -using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityPolicies; -using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; -using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.MapperUtils; -using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; -using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; -using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyPublishingStrategy; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; -using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.Methodologies { @@ -59,21 +53,21 @@ public async Task AdoptMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - var methodologyCacheService = new Mock(Strict); + var methodologyCacheService = new Mock(MockBehavior.Strict); methodologyCacheService.Setup(mock => mock.UpdateSummariesTree()) .ReturnsAsync( new Either>( new List())); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -81,12 +75,12 @@ public async Task AdoptMethodology() var result = await service.AdoptMethodology(publication.Id, methodology.Id); - VerifyAllMocks(methodologyCacheService); + MockUtils.VerifyAllMocks(methodologyCacheService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -168,24 +162,24 @@ public async Task AdoptMethodology_AlreadyAdoptedByPublicationFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, methodology.Id); - result.AssertBadRequest(CannotAdoptMethodologyAlreadyLinkedToPublication); + result.AssertBadRequest(ValidationErrorMessages.CannotAdoptMethodologyAlreadyLinkedToPublication); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -223,24 +217,24 @@ public async Task AdoptMethodology_AdoptingOwnedMethodologyFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, methodology.Id); - result.AssertBadRequest(CannotAdoptMethodologyAlreadyLinkedToPublication); + result.AssertBadRequest(ValidationErrorMessages.CannotAdoptMethodologyAlreadyLinkedToPublication); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -271,13 +265,13 @@ public async Task AdoptMethodology_PublicationNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -295,22 +289,22 @@ public async Task AdoptMethodology_MethodologyNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(Strict); + var methodologyRepository = new Mock(MockBehavior.Strict); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, Guid.NewGuid()); - VerifyAllMocks(methodologyRepository); + MockUtils.VerifyAllMocks(methodologyRepository); result.AssertNotFound(); } @@ -326,15 +320,15 @@ public async Task CreateMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { - var methodologyVersionRepository = new Mock(Strict); + var methodologyVersionRepository = new Mock(MockBehavior.Strict); var service = SetupMethodologyService( contentDbContext: context, @@ -357,7 +351,7 @@ public async Task CreateMethodology() } } }, - Status = Draft + Status = MethodologyApprovalStatus.Draft }; methodologyVersionRepository @@ -367,7 +361,7 @@ public async Task CreateMethodology() context.Attach(createdMethodology); var viewModel = (await service.CreateMethodology(publication.Id)).AssertRight(); - VerifyAllMocks(methodologyVersionRepository); + MockUtils.VerifyAllMocks(methodologyVersionRepository); Assert.Equal(createdMethodology.Id, viewModel.Id); Assert.Equal("test-publication", viewModel.Slug); @@ -375,8 +369,8 @@ public async Task CreateMethodology() Assert.Null(viewModel.InternalReleaseNote); Assert.Equal(createdMethodology.Methodology.Id, viewModel.MethodologyId); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); - Assert.Equal(Draft, viewModel.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyApprovalStatus.Draft, viewModel.Status); Assert.Equal("Test publication", viewModel.Title); Assert.Equal(publication.Id, viewModel.OwningPublication.Id); @@ -410,21 +404,21 @@ public async Task DropMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - var methodologyCacheService = new Mock(Strict); + var methodologyCacheService = new Mock(MockBehavior.Strict); methodologyCacheService.Setup(mock => mock.UpdateSummariesTree()) .ReturnsAsync( new Either>( new List())); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -432,12 +426,12 @@ public async Task DropMethodology() var result = await service.DropMethodology(publication.Id, methodology.Id); - VerifyAllMocks(methodologyCacheService); + MockUtils.VerifyAllMocks(methodologyCacheService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -474,14 +468,14 @@ public async Task DropMethodology_DropMethodologyNotAdoptedFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -491,7 +485,7 @@ public async Task DropMethodology_DropMethodologyNotAdoptedFails() result.AssertNotFound(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -525,13 +519,13 @@ public async Task DropMethodology_PublicationNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -549,13 +543,13 @@ public async Task DropMethodology_MethodologyNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -592,8 +586,8 @@ public async Task GetAdoptableMethodologies() { Methodology = methodology, Published = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, AlternativeTitle = "Alternative title" }; @@ -608,7 +602,7 @@ public async Task GetAdoptableMethodologies() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddRangeAsync(publication, adoptingPublication); await context.Methodologies.AddAsync(methodology); @@ -617,17 +611,17 @@ public async Task GetAdoptableMethodologies() await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(Strict); - var methodologyVersionRepository = new Mock(Strict); + var methodologyRepository = new Mock(MockBehavior.Strict); + var methodologyVersionRepository = new Mock(MockBehavior.Strict); methodologyRepository.Setup(mock => - mock.GetPublishedMethodologiesUnrelatedToPublication(adoptingPublication.Id)) - .ReturnsAsync(ListOf(methodology)); + mock.GetUnrelatedToPublication(adoptingPublication.Id)) + .ReturnsAsync(CollectionUtils.ListOf(methodology)); methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) .ReturnsAsync(methodologyVersion); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -636,7 +630,7 @@ public async Task GetAdoptableMethodologies() var result = (await service.GetAdoptableMethodologies(adoptingPublication.Id)).AssertRight(); - VerifyAllMocks(methodologyRepository, methodologyVersionRepository); + MockUtils.VerifyAllMocks(methodologyRepository, methodologyVersionRepository); Assert.Single(result); @@ -648,8 +642,8 @@ public async Task GetAdoptableMethodologies() Assert.Equal("Test approval", viewModel.InternalReleaseNote); Assert.Equal(methodologyVersion.MethodologyId, viewModel.MethodologyId); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); - Assert.Equal(Draft, viewModel.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyApprovalStatus.Draft, viewModel.Status); Assert.Equal("Alternative title", viewModel.Title); Assert.Equal(publication.Id, viewModel.OwningPublication.Id); @@ -728,19 +722,19 @@ public async Task GetAdoptableMethodologies_NoUnrelatedMethodologies() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(Strict); + var methodologyRepository = new Mock(MockBehavior.Strict); methodologyRepository.Setup(mock => mock.GetPublishedMethodologiesUnrelatedToPublication(publication.Id)) .ReturnsAsync(new List()); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -748,7 +742,7 @@ public async Task GetAdoptableMethodologies_NoUnrelatedMethodologies() var result = (await service.GetAdoptableMethodologies(publication.Id)).AssertRight(); - VerifyAllMocks(methodologyRepository); + MockUtils.VerifyAllMocks(methodologyRepository); Assert.Empty(result); } @@ -778,7 +772,7 @@ public async Task GetMethodology() var adoptingPublication1 = new Publication { Title = "Adopting publication 1", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodology, @@ -790,7 +784,7 @@ public async Task GetMethodology() var adoptingPublication2 = new Publication { Title = "Adopting publication 2", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodology, @@ -803,14 +797,14 @@ public async Task GetMethodology() { Methodology = methodology, Published = new DateTime(2020, 5, 25), - PublishingStrategy = WithRelease, + PublishingStrategy = MethodologyPublishingStrategy.WithRelease, ScheduledWithRelease = new Release { Publication = owningPublication, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2021" }, - Status = Approved, + Status = MethodologyApprovalStatus.Approved, AlternativeTitle = "Alternative title" }; @@ -823,7 +817,7 @@ public async Task GetMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.Publications.AddRangeAsync(owningPublication, adoptingPublication1, adoptingPublication2); @@ -832,7 +826,7 @@ public async Task GetMethodology() await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: context); @@ -844,8 +838,8 @@ public async Task GetMethodology() Assert.Equal("Test approval", viewModel.InternalReleaseNote); Assert.Equal(methodologyVersion.MethodologyId, viewModel.MethodologyId); Assert.Equal(new DateTime(2020, 5, 25), viewModel.Published); - Assert.Equal(WithRelease, viewModel.PublishingStrategy); - Assert.Equal(Approved, viewModel.Status); + 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); @@ -876,36 +870,36 @@ public async Task GetUnpublishedReleasesUsingMethodology() var owningPublication = new Publication { Title = "Publication B", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = true } ), - Releases = ListOf( + Releases = CollectionUtils.ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2018" }, new Release { Published = null, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2021" }, new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2019" }, new Release { Published = null, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2020" } ) @@ -914,36 +908,36 @@ public async Task GetUnpublishedReleasesUsingMethodology() var adoptingPublication = new Publication { Title = "Publication A", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = false } ), - Releases = ListOf( + Releases = CollectionUtils.ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = FinancialYearQ3, + TimePeriodCoverage = TimeIdentifier.FinancialYearQ3, ReleaseName = "2020" }, new Release { Published = null, - TimePeriodCoverage = FinancialYearQ2, + TimePeriodCoverage = TimeIdentifier.FinancialYearQ2, ReleaseName = "2021" }, new Release { Published = null, - TimePeriodCoverage = FinancialYearQ4, + TimePeriodCoverage = TimeIdentifier.FinancialYearQ4, ReleaseName = "2020" }, new Release { Published = null, - TimePeriodCoverage = FinancialYearQ1, + TimePeriodCoverage = TimeIdentifier.FinancialYearQ1, ReleaseName = "2021" } ) @@ -951,14 +945,14 @@ public async Task GetUnpublishedReleasesUsingMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -968,19 +962,19 @@ public async Task GetUnpublishedReleasesUsingMethodology() // Check that only unpublished Releases are included and that they are in the correct order var expectedReleaseAtIndex0 = adoptingPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == FinancialYearQ2); + r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ2); var expectedReleaseAtIndex1 = adoptingPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == FinancialYearQ1); + r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ1); var expectedReleaseAtIndex2 = adoptingPublication.Releases.Single(r => - r.Year == 2020 && r.TimePeriodCoverage == FinancialYearQ4); + r.Year == 2020 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ4); var expectedReleaseAtIndex3 = owningPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == CalendarYear); + r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.CalendarYear); var expectedReleaseAtIndex4 = owningPublication.Releases.Single(r => - r.Year == 2020 && r.TimePeriodCoverage == CalendarYear); + r.Year == 2020 && r.TimePeriodCoverage == TimeIdentifier.CalendarYear); Assert.Equal(5, result.Count); @@ -1001,7 +995,7 @@ public async Task GetUnpublishedReleasesUsingMethodology() [Fact] public async Task GetUnpublishedReleasesUsingMethodology_MethodologyNotFound() { - await using var contentDbContext = InMemoryApplicationDbContext(); + await using var contentDbContext = DbUtils.InMemoryApplicationDbContext(); var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1021,7 +1015,7 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var owningPublication = new Publication { Title = "Owning publication", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, @@ -1033,7 +1027,7 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var adoptingPublication = new Publication { Title = "Adopting publication", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, @@ -1044,14 +1038,14 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1073,18 +1067,18 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var owningPublication = new Publication { Title = "Owning publication", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = true } ), - Releases = ListOf( + Releases = CollectionUtils.ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2021" } ) @@ -1093,18 +1087,18 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var adoptingPublication = new Publication { Title = "Adopting publication", - Methodologies = ListOf( + Methodologies = CollectionUtils.ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = false } ), - Releases = ListOf( + Releases = CollectionUtils.ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = CalendarYear, + TimePeriodCoverage = TimeIdentifier.CalendarYear, ReleaseName = "2021" } ) @@ -1112,14 +1106,14 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1143,7 +1137,7 @@ public async Task ListLatestMethodologyVersions() Version = 0, AlternativeTitle = "Methodology 1 Version 1", Published = new DateTime(2021, 1, 1), - Status = Approved + Status = MethodologyApprovalStatus.Approved } } }; @@ -1158,7 +1152,7 @@ public async Task ListLatestMethodologyVersions() Version = 0, AlternativeTitle = "Methodology 2 Version 1", Published = new DateTime(2021, 1, 1), - Status = Approved + Status = MethodologyApprovalStatus.Approved }, new() { @@ -1166,7 +1160,7 @@ public async Task ListLatestMethodologyVersions() Version = 1, AlternativeTitle = "Methodology 2 Version 2", Published = null, - Status = Draft + Status = MethodologyApprovalStatus.Draft } } }; @@ -1181,7 +1175,7 @@ public async Task ListLatestMethodologyVersions() Version = 0, AlternativeTitle = "Methodology 3 Version 1", Published = new DateTime(2021, 1, 1), - Status = Approved + Status = MethodologyApprovalStatus.Approved }, new() { @@ -1189,7 +1183,7 @@ public async Task ListLatestMethodologyVersions() Version = 1, AlternativeTitle = "Methodology 3 Version 2", Published = new DateTime(2022, 1, 1), - Status = Approved + Status = MethodologyApprovalStatus.Approved } } }; @@ -1221,13 +1215,13 @@ public async Task ListLatestMethodologyVersions() var contextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) { await contentDbContext.Publications.AddAsync(publication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) { var service = SetupMethodologyService(contentDbContext); @@ -1241,7 +1235,7 @@ public async Task ListLatestMethodologyVersions() Assert.False(viewModels[0].Amendment); Assert.True(viewModels[0].Owned); Assert.Equal(new DateTime(2021, 1, 1), viewModels[0].Published); - Assert.Equal(Approved, viewModels[0].Status); + Assert.Equal(MethodologyApprovalStatus.Approved, viewModels[0].Status); Assert.Equal("Methodology 1 Version 1", viewModels[0].Title); Assert.Equal(methodology1.Id, viewModels[0].MethodologyId); Assert.Null(viewModels[0].PreviousVersionId); @@ -1250,7 +1244,7 @@ public async Task ListLatestMethodologyVersions() Assert.True(viewModels[1].Amendment); Assert.False(viewModels[1].Owned); Assert.Null(viewModels[1].Published); - Assert.Equal(Draft, viewModels[1].Status); + Assert.Equal(MethodologyApprovalStatus.Draft, viewModels[1].Status); Assert.Equal("Methodology 2 Version 2", viewModels[1].Title); Assert.Equal(methodology2.Id, viewModels[1].MethodologyId); Assert.Equal(methodology2.Versions[0].Id, viewModels[1].PreviousVersionId); @@ -1259,7 +1253,7 @@ public async Task ListLatestMethodologyVersions() Assert.False(viewModels[2].Amendment); Assert.False(viewModels[2].Owned); Assert.Equal(new DateTime(2022, 1, 1), viewModels[2].Published); - Assert.Equal(Approved, viewModels[2].Status); + Assert.Equal(MethodologyApprovalStatus.Approved, viewModels[2].Status); Assert.Equal("Methodology 3 Version 2", viewModels[2].Title); Assert.Equal(methodology3.Id, viewModels[2].MethodologyId); Assert.Equal(methodology3.Versions[0].Id, viewModels[2].PreviousVersionId); @@ -1288,32 +1282,32 @@ public async Task ListLatestMethodologyVersions_VerifyPermissions() var contextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) { await contentDbContext.Publications.AddAsync(publication); await contentDbContext.SaveChangesAsync(); } - var userService = new Mock(Strict); + var userService = new Mock(MockBehavior.Strict); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanViewSpecificPublication)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanViewSpecificPublication)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanDeleteSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanDeleteSpecificMethodology)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanUpdateSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanUpdateSpecificMethodology)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanApproveSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanApproveSpecificMethodology)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanSubmitSpecificMethodologyToHigherReview)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanSubmitSpecificMethodologyToHigherReview)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanMarkSpecificMethodologyAsDraft)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanMarkSpecificMethodologyAsDraft)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanMakeAmendmentOfSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanMakeAmendmentOfSpecificMethodology)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanDropMethodologyLink)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanDropMethodologyLink)) .ReturnsAsync(true); - await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext, userService: userService.Object); @@ -1321,7 +1315,7 @@ public async Task ListLatestMethodologyVersions_VerifyPermissions() var result = await service.ListLatestMethodologyVersions(publication.Id); var viewModels = result.AssertRight(); - VerifyAllMocks(userService); + MockUtils.VerifyAllMocks(userService); var viewModel = Assert.Single(viewModels); var permissions = viewModel.Permissions; @@ -1348,14 +1342,14 @@ public async Task UpdateMethodology() var methodologyVersion = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Id = Guid.NewGuid(), Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1366,20 +1360,20 @@ public async Task UpdateMethodology() var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Title = "Updated Methodology Title" }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1389,7 +1383,7 @@ public async Task UpdateMethodology() Assert.Equal("updated-methodology-title", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); Assert.Null(viewModel.ScheduledWithRelease); Assert.Equal(request.Status, viewModel.Status); Assert.Equal(request.Title, viewModel.Title); @@ -1398,7 +1392,7 @@ public async Task UpdateMethodology() Assert.Empty(viewModel.OtherPublications); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1406,8 +1400,8 @@ public async Task UpdateMethodology() .SingleAsync(m => m.Id == methodologyVersion.Id); Assert.Null(updatedMethodology.Published); - Assert.Equal(Draft, updatedMethodology.Status); - Assert.Equal(Immediately, updatedMethodology.PublishingStrategy); + 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("updated-methodology-title", updatedMethodology.Slug); @@ -1429,13 +1423,13 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() var methodologyVersion = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1447,20 +1441,20 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Title = "Updated Methodology Title" }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1470,7 +1464,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() Assert.Equal("test-publication", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); Assert.Null(viewModel.ScheduledWithRelease); Assert.Equal(request.Status, viewModel.Status); Assert.Equal(request.Title, viewModel.Title); @@ -1479,7 +1473,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() Assert.Empty(viewModel.OtherPublications); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1487,8 +1481,8 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() .SingleAsync(m => m.Id == methodologyVersion.Id); Assert.Null(updatedMethodology.Published); - Assert.Equal(Draft, updatedMethodology.Status); - Assert.Equal(Immediately, updatedMethodology.PublishingStrategy); + 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); @@ -1511,13 +1505,13 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl { Id = Guid.NewGuid(), AlternativeTitle = "Alternative Methodology Title", - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1528,20 +1522,20 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Title = "Test publication" }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1551,7 +1545,7 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl Assert.Equal("test-publication", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); Assert.Null(viewModel.ScheduledWithRelease); Assert.Equal(request.Status, viewModel.Status); Assert.Equal(request.Title, viewModel.Title); @@ -1560,7 +1554,7 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl Assert.Empty(viewModel.OtherPublications); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1568,8 +1562,8 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl .SingleAsync(m => m.Id == methodologyVersion.Id); Assert.Null(updatedMethodology.Published); - Assert.Equal(Draft, updatedMethodology.Status); - Assert.Equal(Immediately, updatedMethodology.PublishingStrategy); + Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodology.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodology.PublishingStrategy); Assert.Equal(publication.Title, updatedMethodology.Title); // Test explicitly that AlternativeTitle has been unset. @@ -1595,13 +1589,13 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse { Id = Guid.NewGuid(), AlternativeTitle = "Alternative Methodology Title", - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Slug = "alternative-methodology-title", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1613,20 +1607,20 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Title = publication.Title }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1636,7 +1630,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse Assert.Equal("alternative-methodology-title", viewModel.Slug); Assert.Null(viewModel.InternalReleaseNote); Assert.Null(viewModel.Published); - Assert.Equal(Immediately, viewModel.PublishingStrategy); + Assert.Equal(MethodologyPublishingStrategy.Immediately, viewModel.PublishingStrategy); Assert.Null(viewModel.ScheduledWithRelease); Assert.Equal(request.Status, viewModel.Status); Assert.Equal(request.Title, viewModel.Title); @@ -1645,7 +1639,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse Assert.Empty(viewModel.OtherPublications); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1653,8 +1647,8 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse .SingleAsync(m => m.Id == methodologyVersion.Id); Assert.Null(updatedMethodology.Published); - Assert.Equal(Draft, updatedMethodology.Status); - Assert.Equal(Immediately, updatedMethodology.PublishingStrategy); + Assert.Equal(MethodologyApprovalStatus.Draft, updatedMethodology.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, updatedMethodology.PublishingStrategy); Assert.Equal(publication.Title, updatedMethodology.Title); // Test that the AlternativeTitle has explicitly be set to null. @@ -1679,13 +1673,13 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() var methodologyVersion = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1696,8 +1690,8 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() // This pre-existing Methodology has a slug that the update will clash with. var methodologyWithTargetSlug = new MethodologyVersion { - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Slug = "updated-methodology-title", @@ -1708,28 +1702,28 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Title = "Updated Methodology Title" }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddRangeAsync(methodologyVersion, methodologyWithTargetSlug); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); var result = await service.UpdateMethodology(methodologyVersion.Id, request); - result.AssertBadRequest(SlugNotUnique); + result.AssertBadRequest(ValidationErrorMessages.SlugNotUnique); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var notUpdatedMethodology = await context .MethodologyVersions @@ -1737,8 +1731,8 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() .SingleAsync(m => m.Id == methodologyVersion.Id); Assert.Null(notUpdatedMethodology.Published); - Assert.Equal(Draft, notUpdatedMethodology.Status); - Assert.Equal(Immediately, notUpdatedMethodology.PublishingStrategy); + Assert.Equal(MethodologyApprovalStatus.Draft, notUpdatedMethodology.Status); + Assert.Equal(MethodologyPublishingStrategy.Immediately, notUpdatedMethodology.PublishingStrategy); Assert.Equal("Test publication", notUpdatedMethodology.Title); Assert.Null(notUpdatedMethodology.AlternativeTitle); Assert.Equal("test-publication", notUpdatedMethodology.Slug); @@ -1759,14 +1753,14 @@ public async Task UpdateMethodology_StatusUpdate() var methodologyVersion = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Id = Guid.NewGuid(), Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1777,20 +1771,20 @@ public async Task UpdateMethodology_StatusUpdate() var request = new MethodologyUpdateRequest { LatestInternalReleaseNote = "Approved", - PublishingStrategy = Immediately, - Status = Approved, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Approved, Title = "Updated Methodology Title" }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var methodologyApprovalService = new Mock(); @@ -1805,10 +1799,10 @@ public async Task UpdateMethodology_StatusUpdate() await service.UpdateMethodology(methodologyVersion.Id, request); // Verify that the call to update the approval status happened. - VerifyAllMocks(methodologyApprovalService); + MockUtils.VerifyAllMocks(methodologyApprovalService); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1862,20 +1856,20 @@ public async Task DeleteMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that the Methodology and MethodologyVersions were created. Assert.Equal(1, context.Methodologies.Count()); Assert.Equal(4, context.MethodologyVersions.Count()); } - var methodologyImageService = new Mock(Strict); + var methodologyImageService = new Mock(MockBehavior.Strict); // Since the MethodologyVersions should be deleted in sequence, expect a call to delete images for each of the // versions in the same sequence @@ -1902,19 +1896,19 @@ public async Task DeleteMethodology() .Setup(s => s.DeleteAll(methodologyVersion1Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); var result = await service.DeleteMethodology(methodology.Id); - VerifyAllMocks(methodologyImageService); + MockUtils.VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the methodology and the versions have been successfully deleted Assert.Equal(0, context.Methodologies.Count()); @@ -1930,14 +1924,14 @@ public async Task DeleteMethodologyVersion() var methodologyVersion = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Methodology = new Methodology { Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Publications = ListOf(new PublicationMethodology + Publications = CollectionUtils.ListOf(new PublicationMethodology { MethodologyId = methodologyId, PublicationId = Guid.NewGuid(), @@ -1948,13 +1942,13 @@ public async Task DeleteMethodologyVersion() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that a Methodology, a MethodologyVersion and a PublicationMethodology row were // created. @@ -1966,24 +1960,24 @@ public async Task DeleteMethodologyVersion() .SingleAsync(m => m.MethodologyId == methodologyId)); } - var methodologyImageService = new Mock(Strict); + var methodologyImageService = new Mock(MockBehavior.Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodologyVersion.Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); var result = await service.DeleteMethodologyVersion(methodologyVersion.Id); - VerifyAllMocks(methodologyImageService); + MockUtils.VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the version has successfully been deleted and as it was the only version on the // methodology, the methodology is deleted too. @@ -2007,29 +2001,29 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf(new MethodologyVersion + Versions = CollectionUtils.ListOf(new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }, new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }) }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that there is a methodology with two versions. Assert.NotNull(await context.Methodologies.AsQueryable() @@ -2040,12 +2034,12 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() .SingleAsync(m => m.Id == methodology.Versions[1].Id)); } - var methodologyImageService = new Mock(Strict); + var methodologyImageService = new Mock(MockBehavior.Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodology.Versions[1].Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: context, methodologyImageService: methodologyImageService.Object); @@ -2054,12 +2048,12 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() // Verify that the Methodology Image Service was called to remove only the Methodology Files linked to // the version being deleted. - VerifyAllMocks(methodologyImageService); + MockUtils.VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the version has successfully been deleted and as there was another version attached // to the methodology, the methodology itself is not deleted, or the other version. @@ -2082,11 +2076,11 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf(new MethodologyVersion + Versions = CollectionUtils.ListOf(new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }) }; @@ -2095,28 +2089,28 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf(new MethodologyVersion + Versions = CollectionUtils.ListOf(new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }) }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.SaveChangesAsync(); } - var methodologyImageService = new Mock(Strict); + var methodologyImageService = new Mock(MockBehavior.Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodology.Versions[0].Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); @@ -2125,12 +2119,12 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() // Verify that the Methodology Image Service was called to remove only the Methodology Files linked to // the version being deleted. - VerifyAllMocks(methodologyImageService); + MockUtils.VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the methodology and its version are deleted, but the unrelated methodology is unaffected. Assert.False(context.MethodologyVersions.Any(m => m.Id == methodology.Versions[0].Id)); @@ -2156,17 +2150,17 @@ public async Task GetMethodologyStatuses() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf( + Versions = CollectionUtils.ListOf( new MethodologyVersion { - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, Version = 0, }, new MethodologyVersion { - PublishingStrategy = WithRelease, - Status = Approved, + PublishingStrategy = MethodologyPublishingStrategy.WithRelease, + Status = MethodologyApprovalStatus.Approved, Version = 1, } ), @@ -2177,11 +2171,11 @@ public async Task GetMethodologyStatuses() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf(new MethodologyVersion + Versions = CollectionUtils.ListOf(new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }) }; @@ -2191,7 +2185,7 @@ public async Task GetMethodologyStatuses() { MethodologyVersion = methodology.Versions[0], InternalReleaseNote = "Status 1 note", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2000, 1, 1), CreatedById = UserId, }, @@ -2199,7 +2193,7 @@ public async Task GetMethodologyStatuses() { MethodologyVersion = methodology.Versions[1], InternalReleaseNote = "Status 2 note", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2001, 1, 1), CreatedById = UserId, }, @@ -2207,7 +2201,7 @@ public async Task GetMethodologyStatuses() { MethodologyVersion = unrelatedMethodology.Versions[0], InternalReleaseNote = "Unrelated note", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2002, 1, 1), CreatedById = Guid.NewGuid(), } @@ -2220,7 +2214,7 @@ public async Task GetMethodologyStatuses() }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.MethodologyStatus.AddRangeAsync(methodologyStatuses); @@ -2228,7 +2222,7 @@ public async Task GetMethodologyStatuses() await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2259,7 +2253,7 @@ public async Task GetMethodologyStatuses() public async Task GetMethodologyStatuses_NoMethodologyVersion() { var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2280,16 +2274,16 @@ public async Task GetMethodologyStatuses_NoStatuses() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf( + Versions = CollectionUtils.ListOf( new MethodologyVersion { - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, }, new MethodologyVersion { - PublishingStrategy = WithRelease, - Status = Approved, + PublishingStrategy = MethodologyPublishingStrategy.WithRelease, + Status = MethodologyApprovalStatus.Approved, } ), }; @@ -2299,11 +2293,11 @@ public async Task GetMethodologyStatuses_NoStatuses() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = ListOf(new MethodologyVersion + Versions = CollectionUtils.ListOf(new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = Immediately, - Status = Draft + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft }) }; @@ -2313,21 +2307,21 @@ public async Task GetMethodologyStatuses_NoStatuses() { MethodologyVersion = unrelatedMethodology.Versions[0], InternalReleaseNote = "Unrelated note", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2002, 1, 1), CreatedById = Guid.NewGuid(), }, }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.MethodologyStatus.AddRangeAsync(methodologyStatuses); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2358,7 +2352,7 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPubli .WithOwningPublication(publication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatus(HigherLevelReview) + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) .Generate(1)) .Generate(); @@ -2370,14 +2364,14 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPubli .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2386,6 +2380,9 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPubli var methodologyForApproval = Assert.Single(methodologyVersionsForApproval); Assert.Equal(methodology.Versions[0].Id, methodologyForApproval.Id); + + // Assert that we have a populated view model, including the owning Publication details. + Assert.Equal(publication.Title, methodologyForApproval.OwningPublication.Title); } } @@ -2404,11 +2401,11 @@ public async Task ListMethodologyVersionsForApproval_MethodologyVersionNotInHigh .WithOwningPublication(publication) .ForIndex(0, s => s.SetMethodologyVersions(_fixture .DefaultMethodologyVersion() - .WithApprovalStatus(Draft) + .WithApprovalStatus(MethodologyApprovalStatus.Draft) .Generate(1))) .ForIndex(1, s => s.SetMethodologyVersions(_fixture .DefaultMethodologyVersion() - .WithApprovalStatus(Approved) + .WithApprovalStatus(MethodologyApprovalStatus.Approved) .Generate(1))) .GenerateList(); @@ -2420,14 +2417,14 @@ public async Task ListMethodologyVersionsForApproval_MethodologyVersionNotInHigh .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodologies); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); var result = await service.ListMethodologyVersionsForApproval(user.Id); @@ -2450,7 +2447,7 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverButOnAdopting .WithAdoptingPublication(publication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatus(HigherLevelReview) + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) .Generate(1)) .Generate(); @@ -2462,14 +2459,14 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverButOnAdopting .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2492,7 +2489,7 @@ public async Task ListMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPubl .WithOwningPublication(publication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatus(HigherLevelReview) + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) .Generate(1)) .Generate(); @@ -2505,14 +2502,14 @@ public async Task ListMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPubl .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); var result = await service.ListMethodologyVersionsForApproval(user.Id); @@ -2535,7 +2532,7 @@ public async Task ListMethodologyVersionsForApproval_DifferentUserIsApproverOnOw .WithOwningPublication(publication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatus(HigherLevelReview) + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) .Generate(1)) .Generate(); @@ -2547,14 +2544,14 @@ public async Task ListMethodologyVersionsForApproval_DifferentUserIsApproverOnOw .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); var result = await service.ListMethodologyVersionsForApproval(Guid.NewGuid()); @@ -2579,7 +2576,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .WithOwningPublication(owningPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(Approved, HigherLevelReview, Draft)) + .WithApprovalStatuses(CollectionUtils.ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .GenerateList(2); @@ -2588,7 +2585,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .WithAdoptingPublication(adoptingPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(Approved, HigherLevelReview, Draft)) + .WithApprovalStatuses(CollectionUtils.ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .Generate(2); @@ -2610,7 +2607,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .GenerateList(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(ownedMethodologies); await context.Methodologies.AddRangeAsync(adoptedMethodologies); @@ -2618,7 +2615,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() await context.SaveChangesAsync(); } - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2647,13 +2644,13 @@ private static MethodologyService SetupMethodologyService( return new( persistenceHelper ?? new PersistenceHelper(contentDbContext), contentDbContext, - AdminMapper(), - methodologyVersionRepository ?? Mock.Of(Strict), - methodologyRepository ?? Mock.Of(Strict), - methodologyImageService ?? Mock.Of(Strict), - methodologyApprovalService ?? Mock.Of(Strict), - methodologyCacheService ?? Mock.Of(Strict), - userService ?? AlwaysTrueUserService(UserId).Object); + MapperUtils.AdminMapper(), + methodologyVersionRepository ?? Mock.Of(MockBehavior.Strict), + methodologyRepository ?? Mock.Of(MockBehavior.Strict), + methodologyImageService ?? Mock.Of(MockBehavior.Strict), + methodologyApprovalService ?? Mock.Of(MockBehavior.Strict), + methodologyCacheService ?? Mock.Of(MockBehavior.Strict), + userService ?? MockUtils.AlwaysTrueUserService(UserId).Object); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs index cd6eda0d5e1..568bcdfd7a6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs @@ -1706,8 +1706,14 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() // Assert that the only Release returned for this user is the Release where they have a direct // Approver role on and it is in Higher Review. - Assert.Single(viewModels); - Assert.Equal(higherReviewReleaseWithApproverRoleForUser.Id, viewModels[0].Id); + var viewModel = Assert.Single(viewModels); + Assert.Equal(higherReviewReleaseWithApproverRoleForUser.Id, viewModel.Id); + + // Assert that we have a fully populated ReleaseViewModel, including details from the owning + // Publication. + Assert.Equal( + higherReviewReleaseWithApproverRoleForUser.Publication.Title, + viewModel.PublicationTitle); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index 70da939a895..9df1042319f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using System.Collections.Generic; using System.Linq; @@ -422,20 +422,21 @@ public async Task>> ListM .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) .Select(role => role.PublicationId); - var methodologiesToApprove = _context + var methodologiesToApprove = await _context .MethodologyVersions .Include(methodologyVersion => methodologyVersion.Methodology) .ThenInclude(methodology => methodology.Publications) - .ToList() .Where(methodologyVersion => methodologyVersion.Status == MethodologyApprovalStatus.HigherLevelReview && methodologyVersion.Methodology.Publications.Any( publicationMethodology => publicationMethodology.Owner && publicationIdsForApprover.Contains(publicationMethodology.PublicationId))) - // .ToListAsync(); - ; - return methodologiesToApprove.Select(_mapper.Map).ToList(); + .ToListAsync(); + + return (await methodologiesToApprove + .SelectAsync(BuildMethodologyVersionViewModel)) + .ToList(); } private async Task> DeleteVersion(MethodologyVersion methodologyVersion, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs index 26234de9e44..d2e805fbbf8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs @@ -466,17 +466,14 @@ public async Task>> ListReleasesForA var releasesForApproval = await _context .Releases + .Include(release => release.Publication) .Where(release => release.ApprovalStatus == ReleaseApprovalStatus.HigherLevelReview && releaseIdsForApproval.Contains(release.Id)) .ToListAsync(); return releasesForApproval - .Select(release => { - var viewModel = _mapper.Map(release); - // TODO DW - need any permissions adding? - return viewModel; - }) + .Select(_mapper.Map) .ToList(); } diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx index cf4fd063738..0bf8165cefc 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx @@ -8,157 +8,50 @@ import PublicationsTab from '@admin/pages/admin-dashboard/components/Publication import ScheduledReleasesTab from '@admin/pages/admin-dashboard/components/ScheduledReleasesTab'; import releaseQueries from '@admin/queries/releaseQueries'; import loginService from '@admin/services/loginService'; -import { MethodologyVersion } from '@admin/services/methodologyService'; -import { Release } from '@admin/services/releaseService'; import RelatedInformation from '@common/components/RelatedInformation'; import Tabs from '@common/components/Tabs'; import TabsSection from '@common/components/TabsSection'; import WarningMessage from '@common/components/WarningMessage'; import React from 'react'; import { useQuery } from '@tanstack/react-query'; - -const testMethodologies: MethodologyVersion[] = [ - { - id: 'c8c911e3-39c1-452b-801f-25bb79d1deb7', - methodologyId: 'b8bd000c-f9d8-4319-a2b3-6bc18675e5ac', - owningPublication: { - id: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', - title: 'Permanent and fixed-period exclusions in England', - }, - otherPublications: [], - published: '2018-08-25T00:00:00', - publishingStrategy: 'Immediately', - slug: 'permanent-and-fixed-period-exclusions-in-england', - status: 'Approved', - title: 'Pupil exclusion statistics: methodology', - amendment: false, - }, -]; - -const testReleases: Release[] = [ - { - id: 'test-id', - title: 'Academic year 2016/17', - slug: '2024-25', - publicationId: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', - publicationTitle: 'Permanent and fixed-period exclusions in England', - publicationSummary: '', - publicationSlug: 'pub-slug', - year: 2024, - yearTitle: '2024/25', - nextReleaseDate: { - year: '2200', - month: '1', - day: '', - }, - live: false, - timePeriodCoverage: { - value: 'AY', - label: 'Academic year', - }, - preReleaseAccessList: '

Test public access list

', - preReleaseUsersOrInvitesAdded: false, - previousVersionId: 'f', - latestRelease: false, - type: 'NationalStatistics', - contact: { - teamName: 'UI test team name', - teamEmail: 'ui_test@test.com', - contactName: 'UI test contact name', - contactTelNo: '1234 1234', - }, - approvalStatus: 'Approved', - notifySubscribers: false, - latestInternalReleaseNote: 'Approved by UI tests', - amendment: false, - permissions: { - canAddPrereleaseUsers: true, - canViewRelease: true, - canUpdateRelease: true, - canDeleteRelease: false, - canMakeAmendmentOfRelease: false, - }, - updatePublishedDate: false, - }, - { - id: '86d868cf-ff4b-4325-ef26-08d93c9b5089', - title: 'Academic year 2024/25', - slug: '2024-25', - publicationId: '959bd40c-4685-46ff-396d-08d93c9b5159', - publicationTitle: - 'UI tests - Publication and Release UI Permissions Publication Owner', - publicationSummary: '', - publicationSlug: - 'ui-tests-publication-and-release-ui-permissions-publication-owner', - year: 2024, - yearTitle: '2024/25', - nextReleaseDate: { - year: '2200', - month: '1', - day: '', - }, - publishScheduled: '2048-11-16', - live: false, - timePeriodCoverage: { - value: 'AY', - label: 'Academic year', - }, - preReleaseAccessList: '

Test public access list

', - preReleaseUsersOrInvitesAdded: false, - previousVersionId: 'f', - latestRelease: false, - type: 'NationalStatistics', - contact: { - teamName: 'UI test team name', - teamEmail: 'ui_test@test.com', - contactName: 'UI test contact name', - contactTelNo: '1234 1234', - }, - approvalStatus: 'Approved', - notifySubscribers: false, - latestInternalReleaseNote: 'Approved by UI tests', - amendment: false, - permissions: { - canAddPrereleaseUsers: true, - canViewRelease: true, - canUpdateRelease: true, - canDeleteRelease: false, - canMakeAmendmentOfRelease: false, - }, - updatePublishedDate: false, - }, -]; +import methodologyQueries from '@admin/queries/methodologyQueries'; const AdminDashboardPage = () => { - // TO DO EES-4448 replace with real permission - const isApprover = true; const { user } = useAuthContext(); const isBauUser = user?.permissions.isBauUser ?? false; + const isApprover = user?.permissions.isApprover ?? false; const { data: draftReleases = [], isLoading: isLoadingDraftReleases, refetch: reloadDraftReleases, } = useQuery(releaseQueries.listDraftReleases); + const { data: scheduledReleases = [], isLoading: isLoadingScheduledReleases, } = useQuery(releaseQueries.listScheduledReleases); - // TO DO EES-4448 fetch approvals data here and remove test data - // const { - // data: methodologyApprovals = [], - // isLoading: isLoadingMethodologyApprovals, - // } = useQuery(TBC); - // const { - // data: releaseApprovals = [], - // isLoading: isLoadingReleaseApprovals, - // } = useQuery(TBC); - const methodologyApprovals = testMethodologies; - const releaseApprovals = testReleases; - const isLoadingApprovals = false; + const { data: releaseApprovals = [], isLoading: isLoadingReleaseApprovals } = + useQuery({ + ...releaseQueries.listReleasesForApproval, + enabled: isApprover, + }); + + const { + data: methodologyApprovals = [], + isLoading: isLoadingMethodologyApprovals, + } = useQuery({ + ...methodologyQueries.listMethodologiesForApproval, + enabled: isApprover, + }); + + const isLoadingApprovals = + isLoadingReleaseApprovals || isLoadingMethodologyApprovals; - const totalApprovals = methodologyApprovals.length + releaseApprovals.length; + const totalApprovals = + !isLoadingApprovals && + methodologyApprovals.length + releaseApprovals.length; return ( @@ -243,7 +136,7 @@ const AdminDashboardPage = () => { lazy id="approvals" data-testid="publication-approvals" - title={`My approvals (${totalApprovals})`} + title={`Your approvals (${totalApprovals})`} >
-

My approvals

+

Your approvals

Here you can view any releases or methodologies awaiting approval.

diff --git a/src/explore-education-statistics-admin/src/queries/methodologyQueries.ts b/src/explore-education-statistics-admin/src/queries/methodologyQueries.ts index 0adf2ac16a2..0ba48496f10 100644 --- a/src/explore-education-statistics-admin/src/queries/methodologyQueries.ts +++ b/src/explore-education-statistics-admin/src/queries/methodologyQueries.ts @@ -8,6 +8,10 @@ const methodologyQueries = createQueryKeys('methodology', { queryFn: () => methodologyService.getMethodologyStatuses(methodologyId), }; }, + listMethodologiesForApproval: { + queryKey: null, + queryFn: () => methodologyService.listMethodologiesForApproval(), + }, }); export default methodologyQueries; diff --git a/src/explore-education-statistics-admin/src/queries/releaseQueries.ts b/src/explore-education-statistics-admin/src/queries/releaseQueries.ts index 33763ee8071..bd204eeeada 100644 --- a/src/explore-education-statistics-admin/src/queries/releaseQueries.ts +++ b/src/explore-education-statistics-admin/src/queries/releaseQueries.ts @@ -4,11 +4,15 @@ import releaseService from '@admin/services/releaseService'; const releaseQueries = createQueryKeys('release', { listDraftReleases: { queryKey: null, - queryFn: () => releaseService.getDraftReleases(), + queryFn: () => releaseService.listDraftReleases(), }, listScheduledReleases: { queryKey: null, - queryFn: () => releaseService.getScheduledReleases(), + queryFn: () => releaseService.listScheduledReleases(), + }, + listReleasesForApproval: { + queryKey: null, + queryFn: () => releaseService.listReleasesForApproval(), }, }); diff --git a/src/explore-education-statistics-admin/src/services/methodologyService.ts b/src/explore-education-statistics-admin/src/services/methodologyService.ts index 105803b9ef4..cffa9727c33 100644 --- a/src/explore-education-statistics-admin/src/services/methodologyService.ts +++ b/src/explore-education-statistics-admin/src/services/methodologyService.ts @@ -1,5 +1,6 @@ import client from '@admin/services/utils/service'; import { IdTitlePair } from '@admin/services/types/common'; +import { Release } from '@admin/services/releaseService'; export type MethodologyApprovalStatus = | 'Draft' @@ -89,6 +90,10 @@ const methodologyService = { return client.get(`/publication/${publicationId}/methodologies`); }, + listMethodologiesForApproval(): Promise { + return client.get('/methodology/approvals'); + }, + createMethodologyAmendment( methodologyId: string, ): Promise { diff --git a/src/explore-education-statistics-admin/src/services/permissionService.ts b/src/explore-education-statistics-admin/src/services/permissionService.ts index 03cb6928819..241fcb4df57 100644 --- a/src/explore-education-statistics-admin/src/services/permissionService.ts +++ b/src/explore-education-statistics-admin/src/services/permissionService.ts @@ -9,6 +9,7 @@ export interface GlobalPermissions { canAccessAllImports: boolean; canManageAllTaxonomy: boolean; isBauUser: boolean; + isApprover: boolean; } export interface ReleaseStatusPermissions { diff --git a/src/explore-education-statistics-admin/src/services/releaseService.ts b/src/explore-education-statistics-admin/src/services/releaseService.ts index f1a7acad8c2..6e246cbe100 100644 --- a/src/explore-education-statistics-admin/src/services/releaseService.ts +++ b/src/explore-education-statistics-admin/src/services/releaseService.ts @@ -228,14 +228,18 @@ const releaseService = { return client.delete(`/release/${releaseId}`); }, - getDraftReleases(): Promise { + listDraftReleases(): Promise { return client.get('/releases/draft'); }, - getScheduledReleases(): Promise { + listScheduledReleases(): Promise { return client.get('/releases/scheduled'); }, + listReleasesForApproval(): Promise { + return client.get('/releases/approvals'); + }, + getReleasePublicationStatus( releaseId: string, ): Promise { From 5152435a7c1513d8010a221774e2bc8ed8260147 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Wed, 23 Aug 2023 16:11:57 +0100 Subject: [PATCH 06/22] EES-4448 fix tsc errors --- .../src/contexts/AuthContext.tsx | 1 + .../src/pages/admin-dashboard/AdminDashboardPage.tsx | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/explore-education-statistics-admin/src/contexts/AuthContext.tsx b/src/explore-education-statistics-admin/src/contexts/AuthContext.tsx index 5f3928f6b83..a5f407e2454 100644 --- a/src/explore-education-statistics-admin/src/contexts/AuthContext.tsx +++ b/src/explore-education-statistics-admin/src/contexts/AuthContext.tsx @@ -26,6 +26,7 @@ const defaultPermissions: GlobalPermissions = { canAccessAnalystPages: false, canAccessAllImports: false, canManageAllTaxonomy: false, + isApprover: false, isBauUser: false, }; diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx index 0bf8165cefc..0b0280aadcf 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx @@ -49,9 +49,9 @@ const AdminDashboardPage = () => { const isLoadingApprovals = isLoadingReleaseApprovals || isLoadingMethodologyApprovals; - const totalApprovals = - !isLoadingApprovals && - methodologyApprovals.length + releaseApprovals.length; + const totalApprovals = !isLoadingApprovals + ? methodologyApprovals.length + releaseApprovals.length + : 0; return ( From 2143ac4eacc8422a464d71b01bdcccaec1eb2303 Mon Sep 17 00:00:00 2001 From: N-moh Date: Wed, 6 Sep 2023 14:17:47 +0100 Subject: [PATCH 07/22] EES-4493 Ui tests to check Approvals tab --- .../admin-dashboard/AdminDashboardPage.tsx | 2 +- .../components/ApprovalsTable.tsx | 2 +- .../analyst/analyst_absence_permissions.robot | 3 +- ...ge_approvals_as_publication_approver.robot | 61 +++++++++++++++++++ .../robot-tests/tests/libs/admin-common.robot | 11 ++++ 5 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx index 0b0280aadcf..ca033799ebf 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx @@ -75,7 +75,7 @@ const AdminDashboardPage = () => {

{isApprover && totalApprovals > 0 && ( - + You have outstanding approvals )} diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx index 340964c9f5b..ad69ca7235e 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -74,7 +74,7 @@ export default function ApprovalsTable({ } return ( - +
diff --git a/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot b/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot index 16e355d9afa..266cf8ac799 100644 --- a/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot +++ b/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot @@ -21,9 +21,10 @@ Validate Analyst1 can see correct themes and topics user waits until parent contains element ${EXCLUSION_PUBLICATIONS} ... link:Permanent and fixed-period exclusions in England -Validate Analyst1 can see correct draft and scheduled releases tabs +Validate Analyst1 can see correct draft,approvals and scheduled releases tabs user checks element should contain id:draft-releases-tab Draft releases user checks element should contain id:scheduled-releases-tab Approved scheduled releases + user checks element should contain id:approvals-tab Your approvals Validate Analyst1 cannot create a publication user checks page does not contain element link:Create new publication diff --git a/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot b/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot new file mode 100644 index 00000000000..8b1e7124089 --- /dev/null +++ b/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot @@ -0,0 +1,61 @@ +*** Settings *** +Library ../../libs/admin_api.py +Resource ../../libs/admin-common.robot +Resource ../../libs/common.robot +Resource ../../libs/admin/manage-content-common.robot +Resource ../../libs/admin/analyst/role_ui_permissions.robot + +Suite Setup user signs in as bau1 +Suite Teardown user closes the browser +Test Setup fail test fast if required + +Force Tags Admin Local Dev AltersData Footnotes + + +*** Variables *** +${PUBLICATION_NAME} UI tests - manage approvals as publication approver %{RUN_IDENTIFIER} +${RELEASE_TYPE} Academic year 2026/27 +${RELEASE_NAME} ${PUBLICATION_NAME} - ${RELEASE_TYPE} +${SUBJECT_NAME} UI test subject + + +*** Test Cases *** +Create new publication and release via API + ${PUBLICATION_ID} user creates test publication via api ${PUBLICATION_NAME} + Set suite variable ${PUBLICATION_ID} + user gives analyst publication approver access ${PUBLICATION_NAME} + user creates test release via api ${PUBLICATION_ID} AY 2026 + +Add headline text block to Content page + user navigates to draft release page from dashboard ${PUBLICATION_NAME} + ... ${RELEASE_TYPE} + user navigates to content page ${PUBLICATION_NAME} + user adds headlines text block + user adds content to headlines text block Headline text block text + +Change release status + user puts release into higher level review + +Create methodology for publication + user creates methodology for publication ${PUBLICATION_NAME} + user changes methodology status to Higher level review + +Sign in as Analyst1 User1 (publication approver) + user changes to analyst1 + +Check Approvals warning + user checks page contains element testid:outstanding-approvals-warning + +Check Analyst can see correct tabs + user checks element should contain id:publications-tab Your publications + user checks element should contain id:draft-releases-tab Draft releases + user checks element should contain id:approvals-tab Your approvals + user checks element should contain id:scheduled-releases-tab Approved scheduled releases + +Validate if Your approvals page is correct + user clicks link approvals + user waits until h2 is visible Your approvals + user waits until page contains Here you can view any releases or methodologies awaiting approval. + user checks table column heading contains 1 1 Publication / Page testid:your-approvals + user checks table column heading contains 1 2 Page type testid:your-approvals + user checks table column heading contains 1 3 Actions testid:your-approvals diff --git a/tests/robot-tests/tests/libs/admin-common.robot b/tests/robot-tests/tests/libs/admin-common.robot index ebccdf21302..5e5001439ea 100644 --- a/tests/robot-tests/tests/libs/admin-common.robot +++ b/tests/robot-tests/tests/libs/admin-common.robot @@ -671,6 +671,17 @@ user changes methodology status to Draft user waits until h2 is visible Sign off user checks page contains tag In Draft +user changes methodology status to Higher level review + user clicks link Sign off + user waits until h2 is visible Sign off + + user clicks button Edit status + user clicks element id:methodologyStatusForm-status-HigherLevelReview + user clicks button Update status + user waits until h2 is visible Sign off + #user waits until element is visible id:CurrentReleaseStatus-Awaiting higher review + user checks page contains tag In Review + user gives analyst publication owner access [Arguments] ${PUBLICATION_NAME} ${ANALYST_EMAIL}=EES-test.ANALYST1@education.gov.uk user gives publication access to analyst ${PUBLICATION_NAME} Owner ${ANALYST_EMAIL} From c7b07b03eb44dc6d6b3d0b3418abdc389784da73 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 8 Sep 2023 14:41:39 +0100 Subject: [PATCH 08/22] EES-4448 - amended TestStartup and Startup changes in Admin to no longer require Startup to be structured so as to have methods be overridable by TestStartup. Replacement of DbContexts and Azure-dependent services is now done via ConfigureServices with the ability to replace DbContext and service descriptors to prevent the original service implementation from being initialised before being replaced. --- .../Security/PermissionsControllerTests.cs | 2 +- .../Statistics/TableBuilderControllerTests.cs | 5 +- .../TestStartup.cs | 111 +++++------ .../Startup.cs | 181 ++++++++---------- .../Extensions/ServiceCollectionExtensions.cs | 69 +++++++ .../WebApplicationFactoryExtensions.cs | 9 + 6 files changed, 206 insertions(+), 171 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs index 0cecba9a2cb..31c64d786e4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs @@ -191,6 +191,6 @@ public async Task GetGlobalPermissions_UnauthenticatedUser() private WebApplicationFactory SetupApp() { - return _testApp.ResetDbContexts(); + return _testApp.Initialise(); } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs index fb243cc2485..6f4dd76e593 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs @@ -21,7 +21,6 @@ using GovUk.Education.ExploreEducationStatistics.Data.Services.ViewModels; using GovUk.Education.ExploreEducationStatistics.Data.Services.ViewModels.Meta; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Moq; using Xunit; @@ -282,9 +281,9 @@ private WebApplicationFactory SetupApp( ITableBuilderService? tableBuilderService = null) { return _testApp - .ResetDbContexts() + .Initialise() .ConfigureServices(services => services - .AddTransient(_ => tableBuilderService ?? Mock.Of(Strict))); + .ReplaceService(tableBuilderService ?? Mock.Of(Strict))); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs index 02bbc4ca38b..4abb42744f8 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs @@ -1,25 +1,22 @@ #nullable enable -using System; using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; +using GovUk.Education.ExploreEducationStatistics.Admin.Areas.Identity.Data; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; +using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; +using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils; +using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; using GovUk.Education.ExploreEducationStatistics.Data.Model.Tests.Utils; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.AspNetCore.TestHost; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Moq; -using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests; @@ -28,7 +25,10 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests; /// /// This startup inherits from the real production process to make available as realistic a /// set of services as possible, but mocks out services that interact with Azure services and registers in-memory -/// DbContexts in place of the real DbContexts. Additionally this startup configuration does not attempt to start +/// DbContexts in place of the real DbContexts. It also suppresses the applying of migrations to the DbContexts, +/// which is not compatible with in-memory databases. +/// +/// Additionally this startup configuration does not attempt to start /// up the SPA, which requires NPM and needs a more involved and lengthy startup process that is not useful for /// integration tests. /// @@ -39,69 +39,46 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests; // ReSharper disable once ClassNeverInstantiated.Global public class TestStartup : Startup { - public const string AuthenticationScheme = "TestAuthenticationScheme"; - public TestStartup( IConfiguration configuration, - IHostEnvironment hostEnvironment) : base(configuration, hostEnvironment) - { - } - - protected override IStorageQueueService GetStorageQueueService() - { - return Mock.Of(Strict); - } - - protected override ITableStorageService GetTableStorageService() - { - return Mock.Of(Strict); - } - - protected override IPrivateBlobStorageService GetPrivateBlobStorageService(IServiceProvider services) - { - return Mock.Of(Strict); - } - - protected override IPublicBlobStorageService GetPublicBlobStorageService(IServiceProvider services) - { - return Mock.Of(Strict); - } - - protected override DbContextOptionsBuilder GetStatisticsDbContext(DbContextOptionsBuilder options) - { - return AddDbContext(options, "TestStatisticsDb"); - } - - protected override DbContextOptionsBuilder GetContentDbContext(DbContextOptionsBuilder options) - { - return AddDbContext(options, "TestContentDb"); - } - - protected override DbContextOptionsBuilder GetUsersAndRolesDbContext(DbContextOptionsBuilder options) - { - return AddDbContext(options, "TestUsersAndRolesDb"); - } - - protected override void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) + IHostEnvironment hostEnvironment) : base( + configuration, + hostEnvironment, + applyDatabaseMigrations: false, + configureSpa: false) { - // Nothing to do here for in-memory database contexts. } +} - protected override void ConfigureDevelopmentSpaServer(IApplicationBuilder app, IWebHostEnvironment env) +public static class TestStartupExtensions +{ + public static WebApplicationFactory Initialise( + this WebApplicationFactory testApp) { - // Don't attempt to configure the SPA server during integration tests. + return testApp + .ReplaceExternalDependencies() + .RegisterControllers() + .ResetDbContexts(); } - - private static DbContextOptionsBuilder AddDbContext(DbContextOptionsBuilder options, string name) + + private static WebApplicationFactory ReplaceExternalDependencies( + this WebApplicationFactory testApp) { - return options.UseInMemoryDatabase( - name, b => b.EnableNullChecks(false)); + return testApp.WithWebHostBuilder(builder => builder + .ConfigureServices(services => + { + services + .ReplaceDbContext() + .ReplaceDbContext() + .ReplaceDbContext() + .ReplaceService() + .ReplaceService() + .ReplaceService() + .ReplaceService(); + })); } -} - -public static class TestStartupExtensions -{ - public static WebApplicationFactory ResetDbContexts(this WebApplicationFactory testApp) + + private static WebApplicationFactory ResetDbContexts(this WebApplicationFactory testApp) { return testApp .ResetContentDbContext() @@ -113,11 +90,11 @@ public static WebApplicationFactory SetUser( ClaimsPrincipal? user = null) { return testApp.WithWebHostBuilder(builder => builder - .ConfigureTestServices(services => + .ConfigureServices(services => { services - .AddAuthentication(TestStartup.AuthenticationScheme) - .AddScheme(TestStartup.AuthenticationScheme, _ => { }); + .AddAuthentication(TestAuthHandler.AuthenticationScheme) + .AddScheme(TestAuthHandler.AuthenticationScheme, _ => { }); if (user != null) { @@ -133,6 +110,8 @@ public static WebApplicationFactory SetUser( /// internal class TestAuthHandler : AuthenticationHandler { + public const string AuthenticationScheme = "TestAuthenticationScheme"; + private readonly ClaimsPrincipal? _claimsPrincipal; public TestAuthHandler( @@ -152,7 +131,7 @@ protected override Task HandleAuthenticateAsync() return Task.FromResult(AuthenticateResult.NoResult()); } - var ticket = new AuthenticationTicket(_claimsPrincipal, TestStartup.AuthenticationScheme); + var ticket = new AuthenticationTicket(_claimsPrincipal, AuthenticationScheme); var result = AuthenticateResult.Success(ticket); return Task.FromResult(result); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 1b4160180e8..0af5b55833f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -118,7 +118,15 @@ public class Startup private readonly List _adminUrlAndAliases; - public Startup(IConfiguration configuration, IHostEnvironment hostEnvironment) + private readonly bool _applyDatabaseMigrations; + + private readonly bool _configureSpa; + + public Startup( + IConfiguration configuration, + IHostEnvironment hostEnvironment, + bool applyDatabaseMigrations = true, + bool configureSpa = true) { Configuration = configuration; HostEnvironment = hostEnvironment; @@ -128,6 +136,9 @@ public Startup(IConfiguration configuration, IHostEnvironment hostEnvironment) { _adminUrlAndAliases.AddRange(DevelopmentAdminUrlAliases); } + + _applyDatabaseMigrations = applyDatabaseMigrations; + _configureSpa = configureSpa; } // This method gets called by the runtime. Use this method to add services to the container. @@ -158,7 +169,6 @@ public void ConfigureServices(IServiceCollection services) .AddControllers( options => { options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(",")); } ) - .AddApplicationPart(typeof(Startup).Assembly) .AddControllersAsServices(); services.AddHttpContextAccessor(); @@ -189,9 +199,39 @@ public void ConfigureServices(IServiceCollection services) * Database contexts */ - services.AddDbContext(options => GetUsersAndRolesDbContext(options)); - services.AddDbContext(options => GetContentDbContext(options)); - services.AddDbContext(options => GetStatisticsDbContext(options)); + services.AddDbContext(options => + options + .UseSqlServer(Configuration.GetConnectionString("ContentDb"), + providerOptions => + providerOptions + .MigrationsAssembly(typeof(Startup).Assembly.FullName) + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) + ); + + services.AddDbContext(options => + options + .UseSqlServer(Configuration.GetConnectionString("ContentDb"), + providerOptions => + providerOptions + .MigrationsAssembly(typeof(Startup).Assembly.FullName) + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) + ); + + services.AddDbContext(options => + options + .UseSqlServer(Configuration.GetConnectionString("StatisticsDb"), + providerOptions => + providerOptions + .MigrationsAssembly("GovUk.Education.ExploreEducationStatistics.Data.Model") + .AddBulkOperationSupport() + .EnableCustomRetryOnFailure() + ) + .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()) + ); /* * Auth / IdentityServer @@ -384,6 +424,7 @@ public void ConfigureServices(IServiceCollection services) * Services */ + var coreStorageConnectionString = Configuration.GetValue("CoreStorage"); var publisherStorageConnectionString = Configuration.GetValue("PublisherStorage"); services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); @@ -567,12 +608,17 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); - services.AddSingleton(GetPrivateBlobStorageService); - services.AddSingleton(GetPublicBlobStorageService); - - services.AddTransient(_ => GetTableStorageService()); - services.AddTransient(_ => GetStorageQueueService()); - + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(_ => + new TableStorageService( + coreStorageConnectionString, + new StorageInstanceCreationUtil())); + services.AddTransient(_ => + new StorageQueueService( + coreStorageConnectionString, + new StorageInstanceCreationUtil())); services.AddTransient(); services.AddSingleton(); AddPersistenceHelper(services); @@ -636,69 +682,6 @@ public void ConfigureServices(IServiceCollection services) }); } - protected virtual IPrivateBlobStorageService GetPrivateBlobStorageService(IServiceProvider services) - { - return new PrivateBlobStorageService( - services.GetRequiredService>(), Configuration); - } - - protected virtual IPublicBlobStorageService GetPublicBlobStorageService(IServiceProvider services) - { - return new PublicBlobStorageService( - services.GetRequiredService>(), Configuration); - } - - protected virtual IStorageQueueService GetStorageQueueService() - { - return new StorageQueueService( - GetCoreStorageConnectionString(), - new StorageInstanceCreationUtil()); - } - - protected virtual ITableStorageService GetTableStorageService() - { - return new TableStorageService( - GetCoreStorageConnectionString(), - new StorageInstanceCreationUtil()); - } - - protected virtual DbContextOptionsBuilder GetStatisticsDbContext(DbContextOptionsBuilder options) - { - return options - .UseSqlServer(Configuration.GetConnectionString("StatisticsDb"), - providerOptions => - providerOptions - .MigrationsAssembly("GovUk.Education.ExploreEducationStatistics.Data.Model") - .AddBulkOperationSupport() - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); - } - - protected virtual DbContextOptionsBuilder GetContentDbContext(DbContextOptionsBuilder options) - { - return options - .UseSqlServer(Configuration.GetConnectionString("ContentDb"), - providerOptions => - providerOptions - .MigrationsAssembly(typeof(Startup).Assembly.FullName) - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); - } - - protected virtual DbContextOptionsBuilder GetUsersAndRolesDbContext(DbContextOptionsBuilder options) - { - return options - .UseSqlServer(Configuration.GetConnectionString("ContentDb"), - providerOptions => - providerOptions - .MigrationsAssembly(typeof(Startup).Assembly.FullName) - .EnableCustomRetryOnFailure() - ) - .EnableSensitiveDataLogging(HostEnvironment.IsDevelopment()); - } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { @@ -707,17 +690,24 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // Enable caching and register any caching services. CacheAspect.Enabled = true; var privateCacheService = new BlobCacheService( - provider.GetRequiredService(), + new PrivateBlobStorageService( + provider.GetRequiredService>(), + Configuration), provider.GetRequiredService>() ); var publicCacheService = new BlobCacheService( - provider.GetRequiredService(), + new PublicBlobStorageService( + provider.GetRequiredService>(), + Configuration), provider.GetRequiredService>() ); BlobCacheAttribute.AddService("default", privateCacheService); BlobCacheAttribute.AddService("public", publicCacheService); - UpdateDatabase(app, env); + if (_applyDatabaseMigrations) + { + UpdateDatabase(app, env); + } if (env.IsDevelopment()) { @@ -815,43 +805,37 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) template: "{controller}/{action=Index}/{id?}"); }); - ConfigureDevelopmentSpaServer(app, env); - } - - protected virtual void ConfigureDevelopmentSpaServer(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseSpa(spa => + if (_configureSpa) { - if (env.IsDevelopment()) + app.UseSpa(spa => { - spa.Options.SourcePath = "../explore-education-statistics-admin"; - spa.UseReactDevelopmentServer("start"); - } - }); - - app.ServerFeatures.Get() - ?.Addresses - .ForEach(address => Console.WriteLine($"Server listening on address: {address}")); + if (env.IsDevelopment()) + { + spa.Options.SourcePath = "../explore-education-statistics-admin"; + spa.UseReactDevelopmentServer("start"); + } + }); + } } - protected virtual void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) + private void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) { using (var serviceScope = app.ApplicationServices.GetRequiredService() .CreateScope()) { - using (var context = serviceScope.ServiceProvider.GetService()) + using (var context = serviceScope.ServiceProvider.GetRequiredService()) { context.Database.SetCommandTimeout(int.MaxValue); context.Database.Migrate(); } - using (var context = serviceScope.ServiceProvider.GetService()) + using (var context = serviceScope.ServiceProvider.GetRequiredService()) { context.Database.SetCommandTimeout(int.MaxValue); context.Database.Migrate(); } - using (var context = serviceScope.ServiceProvider.GetService()) + using (var context = serviceScope.ServiceProvider.GetRequiredService()) { context.Database.SetCommandTimeout(int.MaxValue); context.Database.Migrate(); @@ -878,10 +862,5 @@ private static void ApplyCustomMigrations(params ICustomMigration[] migrations) migration.Apply(); } } - - private string GetCoreStorageConnectionString() - { - return Configuration.GetValue("CoreStorage"); - } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000000..e8715e0ddee --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Linq; +using GovUk.Education.ExploreEducationStatistics.Common.ModelBinding; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using static Moq.MockBehavior; + +namespace GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection ReplaceDbContext(this IServiceCollection services) + where TDbContext : DbContext + { + // Remove the default DbContext descriptor that was provided by Startup.cs. + var descriptor = services + .Single(d => d.ServiceType == typeof(DbContextOptions)); + + services.Remove(descriptor); + + // Add the new In-Memory replacement. + return services.AddDbContext( + options => options + .UseInMemoryDatabase(nameof(TDbContext), builder => builder.EnableNullChecks(false))); + } + + public static IServiceCollection ReplaceService( + this IServiceCollection services, + TService replacement) + where TService : class + { + // Remove the default service descriptor that was provided by Startup.cs. + var descriptor = services + .Single(d => d.ServiceType == typeof(TService)); + + services.Remove(descriptor); + + // Add the replacement. + return descriptor.Lifetime switch + { + ServiceLifetime.Singleton => services.AddSingleton(_ => replacement), + ServiceLifetime.Scoped => services.AddScoped(_ => replacement), + ServiceLifetime.Transient => services.AddTransient(_ => replacement), + _ => throw new ArgumentOutOfRangeException( + $"Cannot register test service with ${nameof(ServiceLifetime)} {descriptor.Lifetime}") + }; + } + + public static IServiceCollection ReplaceService( + this IServiceCollection services) + where TService : class + { + return services.ReplaceService(Mock.Of(Strict)); + } + + public static IServiceCollection RegisterControllers( + this IServiceCollection services) + where TStartup : class + { + services.AddControllers( + options => { options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(",")); } + ) + .AddApplicationPart(typeof(TStartup).Assembly) + .AddControllersAsServices(); + + return services; + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs index ab76a1649c2..0b9eea9b796 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs @@ -33,4 +33,13 @@ Action testData { return app.WithWebHostBuilder(builder => builder.AddTestData(testData)); } + + public static WebApplicationFactory RegisterControllers( + this WebApplicationFactory app + ) + where TEntrypoint : class + where TStartup : class + { + return app.ConfigureServices(services => services.RegisterControllers()); + } } From 32ae359a51b7fa26042789be39c2f572b0286780 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 8 Sep 2023 14:52:50 +0100 Subject: [PATCH 09/22] EES-4448 - slight tweak to message text if no releases or methodologies for approval exist --- .../src/pages/admin-dashboard/components/ApprovalsTable.tsx | 4 +++- .../components/__tests__/ApprovalsTable.test.tsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx index ad69ca7235e..b962d9e7275 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -70,7 +70,9 @@ export default function ApprovalsTable({ ); if (!Object.keys(allApprovalsByPublication).length) { - return

There are no pages awaiting your approval.

; + return ( +

There are no releases or methodologies awaiting your approval.

+ ); } return ( diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx index defddd396ca..310e07536e7 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx @@ -177,7 +177,9 @@ describe('ApprovalsTable', () => { await waitFor(() => { expect( - screen.getByText('There are no pages awaiting your approval.'), + screen.getByText( + 'There are no releases or methodologies awaiting your approval.', + ), ).toBeInTheDocument(); }); }); From c5f51f07b8d88332561fc9712c399c6d7a661069 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 8 Sep 2023 16:36:14 +0100 Subject: [PATCH 10/22] EES-4448 - tidy-up for PR --- ...loreEducationStatistics.Admin.Tests.csproj | 3 +-- .../Security/Utils/ClaimsPrincipalUtils.cs | 16 ++++++------- .../TestStartup.cs | 22 +++++++++++++---- ...json => appsettings.IntegrationTests.json} | 0 .../Extensions/ServiceCollectionExtensions.cs | 24 ++++++++++++++++--- .../WebApplicationFactoryExtensions.cs | 6 +++++ .../Fixtures/TestApplicationFactory.cs | 3 ++- .../analyst/analyst_absence_permissions.robot | 2 +- 8 files changed, 57 insertions(+), 19 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/{integration-test-settings.json => appsettings.IntegrationTests.json} (100%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj index f8e0cd847f6..c9c620e311a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj @@ -41,8 +41,7 @@ - - + Always diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs index 265bb1e3421..c9d145057cc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/Utils/ClaimsPrincipalUtils.cs @@ -38,10 +38,10 @@ public static ClaimsPrincipal AnalystUser() return CreateClaimsPrincipal( Guid.NewGuid(), RoleClaim(Analyst), - SecurityClaim(ApplicationAccessGranted), - SecurityClaim(AnalystPagesAccessGranted), - SecurityClaim(PrereleasePagesAccessGranted), - SecurityClaim(CanViewPrereleaseContacts)); + SecurityClaim(ApplicationAccessGranted), + SecurityClaim(AnalystPagesAccessGranted), + SecurityClaim(PrereleasePagesAccessGranted), + SecurityClaim(CanViewPrereleaseContacts)); } public static ClaimsPrincipal PreReleaseUser() @@ -62,10 +62,10 @@ public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId) public static ClaimsPrincipal CreateClaimsPrincipal(Guid userId, params Claim[] additionalClaims) { var identity = new ClaimsIdentity( - new List(), - "TestAuthenticationType", - JwtClaimTypes.Name, - JwtClaimTypes.Role); + claims: new List(), + authenticationType: "TestAuthenticationType", + nameType: JwtClaimTypes.Name, + roleType: JwtClaimTypes.Role); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId.ToString())); identity.AddClaims(additionalClaims); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs index 4abb42744f8..7e83985a407 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs @@ -21,16 +21,15 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests; /// -/// Generic test application startup for use in integration tests. +/// Test application Startup for use in Admin integration tests. /// /// This startup inherits from the real production process to make available as realistic a /// set of services as possible, but mocks out services that interact with Azure services and registers in-memory /// DbContexts in place of the real DbContexts. It also suppresses the applying of migrations to the DbContexts, /// which is not compatible with in-memory databases. /// -/// Additionally this startup configuration does not attempt to start -/// up the SPA, which requires NPM and needs a more involved and lengthy startup process that is not useful for -/// integration tests. +/// Additionally this startup configuration does not attempt to start up the SPA, which requires NPM and needs a +/// more involved and lengthy startup process that is not useful for integration tests. /// /// /// Use in combination with @@ -52,6 +51,12 @@ public TestStartup( public static class TestStartupExtensions { + /// + /// Call this method when using this TestStartup to replace DbContexts with in-memory equivalents and services + /// which have external dependencies with mocks. + /// + /// + /// public static WebApplicationFactory Initialise( this WebApplicationFactory testApp) { @@ -85,6 +90,15 @@ private static WebApplicationFactory ResetDbContexts(this WebApplic .ResetStatisticsDbContext(); } + /// + /// This method adds an authenticated User in the form of a ClaimsPrincipal to the HttpContext. + /// + /// This User will subsequently be available for the Identity Framework as well as our own UserService, and any + /// other production code that looks up the User from the currect HttpContext. + /// + /// + /// + /// public static WebApplicationFactory SetUser( this WebApplicationFactory testApp, ClaimsPrincipal? user = null) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/integration-test-settings.json rename to src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs index e8715e0ddee..a6aa039eafc 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs @@ -8,8 +8,15 @@ namespace GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; +/// +/// This class contains a number of extension methods for IServiceCollection that are useful when setting up +/// integration tests that require a customised Startup class. +/// public static class ServiceCollectionExtensions { + /// + /// This method replaces a DcContext that has been registered in Startup with an in-memory equivalent. + /// public static IServiceCollection ReplaceDbContext(this IServiceCollection services) where TDbContext : DbContext { @@ -25,6 +32,10 @@ public static IServiceCollection ReplaceDbContext(this IServiceColle .UseInMemoryDatabase(nameof(TDbContext), builder => builder.EnableNullChecks(false))); } + /// + /// This method replaces a service that has been registered in Startup with a new implementation. The same + /// lifecycle that was registered in Startup will be used to register the new service. + /// public static IServiceCollection ReplaceService( this IServiceCollection services, TService replacement) @@ -47,6 +58,10 @@ public static IServiceCollection ReplaceService( }; } + /// + /// This method replaces a service that has been registered in Startup with a Strict Mock. The same + /// lifecycle that was registered in Startup will be used to register the new service. + /// public static IServiceCollection ReplaceService( this IServiceCollection services) where TService : class @@ -54,13 +69,16 @@ public static IServiceCollection ReplaceService( return services.ReplaceService(Mock.Of(Strict)); } + /// + /// This method registers all Controllers found in the class's assembly. + /// public static IServiceCollection RegisterControllers( this IServiceCollection services) where TStartup : class { - services.AddControllers( - options => { options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(",")); } - ) + services + .AddControllers(options => + options.ModelBinderProviders.Insert(0, new SeparatedQueryModelBinderProvider(","))) .AddApplicationPart(typeof(TStartup).Assembly) .AddControllersAsServices(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs index 0b9eea9b796..e33b1cd2b03 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/WebApplicationFactoryExtensions.cs @@ -34,6 +34,12 @@ Action testData return app.WithWebHostBuilder(builder => builder.AddTestData(testData)); } + /// + /// This method registers all Controllers found in the class's assembly. + /// + /// Typically, will be the TestStartup type, and will be the + /// production Startup type. + /// public static WebApplicationFactory RegisterControllers( this WebApplicationFactory app ) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs index c4e292086bc..46d0dd7ddea 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs @@ -40,7 +40,8 @@ protected override IHostBuilder CreateHostBuilder() config.AddConfiguration(new ConfigurationBuilder() .AddJsonFile("appsettings.json", optional: true) .AddJsonFile("appsettings.Development.json", optional: true) - .AddJsonFile("integration-test-settings.json", optional: true) + .AddJsonFile("appsettings.Local.json", optional: true) + .AddJsonFile("appsettings.IntegrationTests.json", optional: true) .Build()); }); } diff --git a/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot b/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot index 266cf8ac799..b8117c3b755 100644 --- a/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot +++ b/tests/robot-tests/tests/admin/analyst/analyst_absence_permissions.robot @@ -21,7 +21,7 @@ Validate Analyst1 can see correct themes and topics user waits until parent contains element ${EXCLUSION_PUBLICATIONS} ... link:Permanent and fixed-period exclusions in England -Validate Analyst1 can see correct draft,approvals and scheduled releases tabs +Validate Analyst1 can see correct draft, approvals and scheduled releases tabs user checks element should contain id:draft-releases-tab Draft releases user checks element should contain id:scheduled-releases-tab Approved scheduled releases user checks element should contain id:approvals-tab Your approvals From bc294fd7cfaa65416056fb565b810f83c2bb8d59 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Tue, 12 Sep 2023 10:04:30 +0100 Subject: [PATCH 11/22] EES-4448 - adding additional testids to components and additional tests to Your Approvals UI tests --- .../components/ApprovalsTable.tsx | 10 +++++-- ...ge_approvals_as_publication_approver.robot | 30 +++++++++++++++++-- .../robot-tests/tests/libs/admin-common.robot | 1 - 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx index b962d9e7275..dd66b936d73 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -115,7 +115,10 @@ function PublicationRow({
{releases?.map(release => ( - + ))} {methodologies?.map(methodology => ( - + - {orderBy(Object.keys(allApprovalsByPublication)).map(publication => ( + {orderBy(publications).map(publication => ( - diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx index 3308e5c2b29..090f31d504e 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx @@ -42,7 +42,7 @@ const DraftReleaseRow = ({ isBauUser, release, onDelete }: Props) => { {release.title} - {release.amendment && ( + {release.amendment && release.previousVersionId && ( (releaseSummaryRoute.path, { diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx index 310e07536e7..4a95b70661c 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx @@ -48,18 +48,10 @@ describe('ApprovalsTable', () => { }, preReleaseAccessList: '

Test public access list

', preReleaseUsersOrInvitesAdded: false, - previousVersionId: 'f', latestRelease: false, type: 'NationalStatistics', - contact: { - teamName: 'UI test team name', - teamEmail: 'ui_test@test.com', - contactName: 'UI test contact name', - contactTelNo: '1234 1234', - }, approvalStatus: 'Approved', notifySubscribers: false, - latestInternalReleaseNote: 'Approved by UI tests', amendment: false, permissions: { canAddPrereleaseUsers: true, @@ -95,18 +87,10 @@ describe('ApprovalsTable', () => { }, preReleaseAccessList: '

Test public access list

', preReleaseUsersOrInvitesAdded: false, - previousVersionId: 'f', latestRelease: false, type: 'NationalStatistics', - contact: { - teamName: 'UI test team name', - teamEmail: 'ui_test@test.com', - contactName: 'UI test contact name', - contactTelNo: '1234 1234', - }, approvalStatus: 'Approved', notifySubscribers: false, - latestInternalReleaseNote: 'Approved by UI tests', amendment: false, permissions: { canAddPrereleaseUsers: true, diff --git a/src/explore-education-statistics-admin/src/services/releaseService.ts b/src/explore-education-statistics-admin/src/services/releaseService.ts index 6e246cbe100..870ce56a07d 100644 --- a/src/explore-education-statistics-admin/src/services/releaseService.ts +++ b/src/explore-education-statistics-admin/src/services/releaseService.ts @@ -31,7 +31,7 @@ export interface Release { published?: string; nextReleaseDate?: PartialDate; latestInternalReleaseNote?: string; - previousVersionId: string; + previousVersionId?: string; preReleaseAccessList: string; preReleaseUsersOrInvitesAdded?: boolean; year: number; From 6064913e0e85275ef7cd92660fbdcc654987bbc3 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Thu, 21 Sep 2023 09:31:04 +0100 Subject: [PATCH 17/22] EES-4448 fix tsc error --- .../components/__tests__/ApprovalsTable.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx index 4a95b70661c..984e05fa923 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx @@ -32,7 +32,6 @@ describe('ApprovalsTable', () => { slug: '2024-25', publicationId: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', publicationTitle: 'Permanent and fixed-period exclusions in England', - publicationSummary: '', publicationSlug: 'pub-slug', year: 2024, yearTitle: '2024/25', @@ -69,7 +68,6 @@ describe('ApprovalsTable', () => { publicationId: '959bd40c-4685-46ff-396d-08d93c9b5159', publicationTitle: 'UI tests - Publication and Release UI Permissions Publication Owner', - publicationSummary: '', publicationSlug: 'ui-tests-publication-and-release-ui-permissions-publication-owner', year: 2024, From 01518a627def81be89aff1df94572909ef392170 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Wed, 20 Sep 2023 16:38:41 +0100 Subject: [PATCH 18/22] EES-4448 - responding to PR comments regarding improvements to integration test startup. Disabling additional Startup configuration (Swagger, IdentityServer, Application Insights) in integration tests, and swapped use of flags for use of explicit Host Environment for Integration Tests instead. Removed explicit instantiation of TableStorageService instances in favour of looking them up from the service collection, which allows us to pare back appsettings configuration for integration tests to a bare minimum (no need to provide connection strings) and stops real service configuration from creeping into services under test. Instead we now follow the pattern of Private/Public services and cache services with new Publisher/CoreTableStorageService classes, which in turn allows us to simplify DI condiguration in Startup. --- ...pecificCommentAuthorizationHandlerTests.cs | 4 +- ...ePublishingStatusServicePermissionTests.cs | 4 +- .../TestStartup.cs | 22 +- .../appsettings.IntegrationTests.json | 12 + .../ReleasePublishingStatusRepository.cs | 4 +- .../ReleasePublishingStatusService.cs | 4 +- .../Startup.cs | 347 ++++++++---------- .../Fixtures/TestApplicationFactory.cs | 9 +- .../Extensions/HostEnvironmentExtensions.cs | 20 + .../Services/CoreTableStorageService.cs | 11 + .../Interfaces/ICoreTableStorageService.cs | 3 + .../IPublisherTableStorageService.cs | 3 + .../Services/PublisherTableStorageService.cs | 11 + .../Services/TableStorageService.cs | 10 +- .../ReleasePublishingStatusService.cs | 4 +- .../Startup.cs | 5 +- 16 files changed, 235 insertions(+), 238 deletions(-) create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/ICoreTableStorageService.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/IPublisherTableStorageService.cs create mode 100644 src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs 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 13a8bdeed3b..341ee6ed2be 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Security/AuthorizationHandlers/DeleteSpecificCommentAuthorizationHandlerTests.cs @@ -28,7 +28,7 @@ public async Task CanDeleteCommentAuthorizationHandler() ApprovalStatus = ReleaseApprovalStatus.Draft, Content = new List { - new ReleaseContentSection + new() { ContentSection = new ContentSection { @@ -50,7 +50,7 @@ await AssertHandlerOnlySucceedsWithReleaseRoles contentDbContext.Add(release), contentDbContext => new DeleteSpecificCommentAuthorizationHandler( contentDbContext, - new ReleasePublishingStatusRepository(Mock.Of()), + new ReleasePublishingStatusRepository(Mock.Of()), new AuthorizationHandlerResourceRoleService( new UserReleaseRoleRepository(contentDbContext), new UserPublicationRoleRepository(contentDbContext), diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleasePublishingStatusServicePermissionTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleasePublishingStatusServicePermissionTests.cs index e4dace723d9..5240549184c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleasePublishingStatusServicePermissionTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleasePublishingStatusServicePermissionTests.cs @@ -41,14 +41,14 @@ private ReleasePublishingStatusService BuildReleaseStatusService( IMapper mapper = null, IUserService userService = null, IPersistenceHelper persistenceHelper = null, - ITableStorageService publisherTableStorageService = null) + IPublisherTableStorageService publisherTableStorageService = null) { return new ReleasePublishingStatusService( mapper ?? MapperUtils.AdminMapper(), userService ?? new Mock().Object, persistenceHelper ?? MockUtils.MockPersistenceHelper(_release.Id, _release) .Object, - publisherTableStorageService ?? new Mock().Object + publisherTableStorageService ?? new Mock().Object ); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs index 4aa958d66c6..325db220e59 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs @@ -40,9 +40,7 @@ public TestStartup( IConfiguration configuration, IHostEnvironment hostEnvironment) : base( configuration, - hostEnvironment, - applyDatabaseMigrations: false, - configureSpa: false) + hostEnvironment) { } @@ -55,10 +53,15 @@ public override void ConfigureServices(IServiceCollection services) .UseInMemoryDbContext() .UseInMemoryDbContext() .MockService() - .MockService() + .MockService() + .MockService() .MockService() .MockService() .RegisterControllers(); + + services + .AddAuthentication(TestAuthHandler.AuthenticationScheme) + .AddScheme(TestAuthHandler.AuthenticationScheme, _ => { }); } } @@ -78,14 +81,7 @@ public static WebApplicationFactory SetUser( ClaimsPrincipal user) { return testApp.WithWebHostBuilder(builder => builder - .ConfigureServices(services => - { - services - .AddAuthentication(TestAuthHandler.AuthenticationScheme) - .AddScheme(TestAuthHandler.AuthenticationScheme, _ => { }); - - services.AddScoped(_ => user); - })); + .ConfigureServices(services => services.AddScoped(_ => user))); } } @@ -104,7 +100,7 @@ public TestAuthHandler( ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, - ClaimsPrincipal? claimsPrincipal) : base(options, logger, encoder, clock) + ClaimsPrincipal? claimsPrincipal = null) : base(options, logger, encoder, clock) { _claimsPrincipal = claimsPrincipal; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json index 757d9b09aa7..bc56aca8439 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json @@ -2,5 +2,17 @@ "OpenIdConnect": { "ClientId": "test-client-id", "Authority": "https://example.com" + }, + "enableSwagger": false, + "IdentityServer": { + "Key": { + "Type": "Development" + } + }, + "PreReleaseAccess": { + "AccessWindow": { + "MinutesBeforeReleaseTimeStart": 1440, + "MinutesBeforeReleaseTimeEnd": 1 + } } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs index 122684a63bf..afb6574e973 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusRepository.cs @@ -12,9 +12,9 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services { public class ReleasePublishingStatusRepository : IReleasePublishingStatusRepository { - private readonly ITableStorageService _publisherTableStorageService; + private readonly IPublisherTableStorageService _publisherTableStorageService; - public ReleasePublishingStatusRepository(ITableStorageService publisherTableStorageService) + public ReleasePublishingStatusRepository(IPublisherTableStorageService publisherTableStorageService) { _publisherTableStorageService = publisherTableStorageService; } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusService.cs index e8178d912d6..834433abec5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleasePublishingStatusService.cs @@ -21,7 +21,7 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Services public class ReleasePublishingStatusService : IReleasePublishingStatusService { private readonly IMapper _mapper; - private readonly ITableStorageService _publisherTableStorageService; + private readonly IPublisherTableStorageService _publisherTableStorageService; private readonly IUserService _userService; private readonly IPersistenceHelper _persistenceHelper; @@ -29,7 +29,7 @@ public ReleasePublishingStatusService( IMapper mapper, IUserService userService, IPersistenceHelper persistenceHelper, - ITableStorageService publisherTableStorageService) + IPublisherTableStorageService publisherTableStorageService) { _mapper = mapper; _userService = userService; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 3961b9aa422..f552c9caba6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -54,7 +54,6 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Authorization; @@ -118,15 +117,9 @@ public class Startup private readonly List _adminUrlAndAliases; - private readonly bool _applyDatabaseMigrations; - - private readonly bool _configureSpa; - public Startup( IConfiguration configuration, - IHostEnvironment hostEnvironment, - bool applyDatabaseMigrations = true, - bool configureSpa = true) + IHostEnvironment hostEnvironment) { Configuration = configuration; HostEnvironment = hostEnvironment; @@ -136,9 +129,6 @@ public Startup( { _adminUrlAndAliases.AddRange(DevelopmentAdminUrlAliases); } - - _applyDatabaseMigrations = applyDatabaseMigrations; - _configureSpa = configureSpa; } // This method gets called by the runtime. Use this method to add services to the container. @@ -186,10 +176,7 @@ public virtual void ConfigureServices(IServiceCollection services) }); // Adds Brotli and Gzip compressing - services.AddResponseCompression(options => - { - options.EnableForHttps = true; - }); + services.AddResponseCompression(options => { options.EnableForHttps = true; }); // In production, the React files will be served from this directory services.AddSpaStaticFiles(configuration => { configuration.RootPath = "wwwroot"; }); @@ -247,127 +234,130 @@ public virtual void ConfigureServices(IServiceCollection services) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - var identityServerConfig = services - .AddIdentityServer(options => - { - options.Events.RaiseErrorEvents = true; - options.Events.RaiseInformationEvents = true; - options.Events.RaiseFailureEvents = true; - options.Events.RaiseSuccessEvents = true; - }) - .AddApiAuthorization(options => - { - var spaClient = options - .Clients - .First(client => client.ClientId == OpenIdConnectSpaClientId); - - var clientConfig = Configuration.GetSection("OpenIdConnectSpaClient"); - - if (clientConfig == null) + if (!HostEnvironment.IsIntegrationTests()) + { + var identityServerConfig = services + .AddIdentityServer(options => { - return; - } - - var allowRefreshTokens = clientConfig.GetValue("AllowOfflineAccess", false); - - if (allowRefreshTokens) + options.Events.RaiseErrorEvents = true; + options.Events.RaiseInformationEvents = true; + options.Events.RaiseFailureEvents = true; + options.Events.RaiseSuccessEvents = true; + }) + .AddApiAuthorization(options => { - // Allow the use of refresh tokens to add persistent access to the service and enable the silent - // login flow. - spaClient.AllowOfflineAccess = true; - spaClient.AllowedScopes = spaClient - .AllowedScopes - .Append(OpenIdConnectScope.OfflineAccess) - .ToList(); + var spaClient = options + .Clients + .First(client => client.ClientId == OpenIdConnectSpaClientId); - spaClient.UpdateAccessTokenClaimsOnRefresh = true; + var clientConfig = Configuration.GetSection("OpenIdConnectSpaClient"); - var tokenUsage = clientConfig.GetValue("RefreshTokenUsage"); + if (clientConfig == null) + { + return; + } - spaClient.RefreshTokenUsage = tokenUsage != null - ? EnumUtil.GetFromString(tokenUsage) - : TokenUsage.OneTimeOnly; + var allowRefreshTokens = clientConfig.GetValue("AllowOfflineAccess", false); - var tokenExpiration = clientConfig.GetValue("RefreshTokenExpiration"); + if (allowRefreshTokens) + { + // Allow the use of refresh tokens to add persistent access to the service and enable the silent + // login flow. + spaClient.AllowOfflineAccess = true; + spaClient.AllowedScopes = spaClient + .AllowedScopes + .Append(OpenIdConnectScope.OfflineAccess) + .ToList(); - spaClient.RefreshTokenExpiration = tokenExpiration != null - ? EnumUtil.GetFromString(tokenExpiration) - : TokenExpiration.Absolute; - } - }) - .AddProfileService(); + spaClient.UpdateAccessTokenClaimsOnRefresh = true; - if (HostEnvironment.IsDevelopment()) - { - identityServerConfig.AddDeveloperSigningCredential(); - } - else - { - identityServerConfig.AddSigningCredentials(); - } + var tokenUsage = clientConfig.GetValue("RefreshTokenUsage"); - services.Configure( - IdentityServerJwtConstants.IdentityServerJwtBearerScheme, - options => - { - options.TokenValidationParameters = new TokenValidationParameters - { - // When the user returns from logging into the Identity Provider (e.g. Azure AD, Keycloak etc) - // the external login portion of the Open ID Connect flow is completed, and then the Admin - // SPA and Identity Server (the locally running implementation of an Open ID Connect IdP) enter - // into a conversation, effectively swapping the access token issued from Azure or Keycloak - // for a new access token issued from Identity Server itself specifically for the SPA's use. - // - // The "Issuer" of this access token is by default whatever URL that the SPA used to initiate - // the conversation with Identity Server. So if the external IdP returns the user to - // https://localhost:5021, then the SPA will use https://localhost:5021 as a basis for - // negotiating with Identity Server, and thus Identity Server will issue its access tokens with - // the "Issuer" set to "https://localhost:5021". - // - // However, by default Identity Server will set its "TokenValidationParameters.ValidIssuer" - // property to the "applicationUrl" value in "launchSettings.json" - locally for instance, - // this would be "https://0.0.0.0:5021". This complicates matters further as access tokens - // that it issues would otherwise be immediately invalidated by this setting, regardless of what - // URL the user was hitting the site on. The answer is to manually let Identity Server know - // which URLs are appropriate for each environment. - // - // As locally it's possible to access the service under an alternative URL like - // "https://ees.local:5021", then we need to ensure that both "https://localhost:5021" and - // "https://ees.local:5021" are *both* valid "Issuer" values on the access token provided by - // Identity Server. - ValidIssuers = _adminUrlAndAliases - }; - }); + spaClient.RefreshTokenUsage = tokenUsage != null + ? EnumUtil.GetFromString(tokenUsage) + : TokenUsage.OneTimeOnly; - services - .AddAuthentication() - .AddOpenIdConnect(options => Configuration.GetSection("OpenIdConnect").Bind(options)) - .AddIdentityServerJwt(); + var tokenExpiration = clientConfig.GetValue("RefreshTokenExpiration"); + + spaClient.RefreshTokenExpiration = tokenExpiration != null + ? EnumUtil.GetFromString(tokenExpiration) + : TokenExpiration.Absolute; + } + }) + .AddProfileService(); - services.Configure( - IdentityServerJwtConstants.IdentityServerJwtBearerScheme, - options => + if (HostEnvironment.IsDevelopment()) { - var originalOnMessageReceived = options.Events.OnMessageReceived; + identityServerConfig.AddDeveloperSigningCredential(); + } + else + { + identityServerConfig.AddSigningCredentials(); + } - options.Events.OnMessageReceived = async context => + services.Configure( + IdentityServerJwtConstants.IdentityServerJwtBearerScheme, + options => { - await originalOnMessageReceived(context); - - if (!context.Token.IsNullOrEmpty()) + options.TokenValidationParameters = new TokenValidationParameters { - return; - } + // When the user returns from logging into the Identity Provider (e.g. Azure AD, Keycloak etc) + // the external login portion of the Open ID Connect flow is completed, and then the Admin + // SPA and Identity Server (the locally running implementation of an Open ID Connect IdP) enter + // into a conversation, effectively swapping the access token issued from Azure or Keycloak + // for a new access token issued from Identity Server itself specifically for the SPA's use. + // + // The "Issuer" of this access token is by default whatever URL that the SPA used to initiate + // the conversation with Identity Server. So if the external IdP returns the user to + // https://localhost:5021, then the SPA will use https://localhost:5021 as a basis for + // negotiating with Identity Server, and thus Identity Server will issue its access tokens with + // the "Issuer" set to "https://localhost:5021". + // + // However, by default Identity Server will set its "TokenValidationParameters.ValidIssuer" + // property to the "applicationUrl" value in "launchSettings.json" - locally for instance, + // this would be "https://0.0.0.0:5021". This complicates matters further as access tokens + // that it issues would otherwise be immediately invalidated by this setting, regardless of what + // URL the user was hitting the site on. The answer is to manually let Identity Server know + // which URLs are appropriate for each environment. + // + // As locally it's possible to access the service under an alternative URL like + // "https://ees.local:5021", then we need to ensure that both "https://localhost:5021" and + // "https://ees.local:5021" are *both* valid "Issuer" values on the access token provided by + // Identity Server. + ValidIssuers = _adminUrlAndAliases + }; + }); + + services + .AddAuthentication() + .AddOpenIdConnect(options => Configuration.GetSection("OpenIdConnect").Bind(options)) + .AddIdentityServerJwt(); + + services.Configure( + IdentityServerJwtConstants.IdentityServerJwtBearerScheme, + options => + { + var originalOnMessageReceived = options.Events.OnMessageReceived; - // Allows requests with `access_token` query parameter to authenticate. - // Only really needed for websockets as we unfortunately can't set any - // headers in the browser for the initial handshake. - if (context.Request.Query.ContainsKey("access_token")) + options.Events.OnMessageReceived = async context => { - context.Token = context.Request.Query["access_token"]; - } - }; - }); + await originalOnMessageReceived(context); + + if (!context.Token.IsNullOrEmpty()) + { + return; + } + + // Allows requests with `access_token` query parameter to authenticate. + // Only really needed for websockets as we unfortunately can't set any + // headers in the browser for the initial handshake. + if (context.Request.Query.ContainsKey("access_token")) + { + context.Token = context.Request.Query["access_token"]; + } + }; + }); + } // This configuration has to occur after the AddAuthentication() block as it is otherwise overridden. services.Configure(options => @@ -463,45 +453,14 @@ public virtual void ConfigureServices(IServiceCollection services) new StorageInstanceCreationUtil()), provider.GetService(), provider.GetRequiredService>())); - services.AddTransient(s => - new ReleasePublishingStatusService( - s.GetService(), - s.GetService(), - s.GetService>(), - new TableStorageService( - publisherStorageConnectionString, - new StorageInstanceCreationUtil()))); - services.AddTransient(_ => - new ReleasePublishingStatusRepository( - new TableStorageService( - publisherStorageConnectionString, - new StorageInstanceCreationUtil()) - ) - ); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(provider => - new PublicationService( - context: provider.GetRequiredService(), - mapper: provider.GetRequiredService(), - persistenceHelper: provider.GetRequiredService>(), - userService: provider.GetRequiredService(), - publicationRepository: provider.GetRequiredService(), - methodologyVersionRepository: provider.GetRequiredService(), - methodologyCacheService: provider.GetRequiredService(), - publicationCacheService: provider.GetRequiredService() - ) - ); + services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(provider => - new LegacyReleaseService( - context: provider.GetRequiredService(), - mapper: provider.GetRequiredService(), - userService: provider.GetRequiredService(), - persistenceHelper: provider.GetRequiredService>(), - publicationCacheService: provider.GetRequiredService()) - ); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -517,19 +476,7 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(provider => - new MethodologyApprovalService( - context: provider.GetRequiredService(), - persistenceHelper: provider.GetRequiredService>(), - methodologyContentService: provider.GetRequiredService(), - methodologyFileRepository: provider.GetRequiredService(), - methodologyImageService: provider.GetRequiredService(), - methodologyVersionRepository: provider.GetRequiredService(), - publishingService: provider.GetRequiredService(), - userService: provider.GetRequiredService(), - userReleaseRoleService: provider.GetRequiredService(), - methodologyCacheService: provider.GetRequiredService(), - emailTemplateService: provider.GetRequiredService())); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); @@ -611,10 +558,8 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); - services.AddTransient(_ => - new TableStorageService( - coreStorageConnectionString, - new StorageInstanceCreationUtil())); + services.AddTransient(); + services.AddTransient(); services.AddTransient(_ => new StorageQueueService( coreStorageConnectionString, @@ -656,30 +601,32 @@ public virtual void ConfigureServices(IServiceCollection services) /* * Swagger */ - - services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", - new OpenApiInfo { Title = "Explore education statistics - Admin API", Version = "v1" }); - c.CustomSchemaIds((type) => type.FullName); - c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme - { - Description = "Please enter into field the word 'Bearer' followed by a space and the JWT contents", - Name = "Authorization", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey - }); - c.AddSecurityRequirement(new OpenApiSecurityRequirement + + if (Configuration.GetValue("enableSwagger")) + services.AddSwaggerGen(c => { + c.SwaggerDoc("v1", + new OpenApiInfo {Title = "Explore education statistics - Admin API", Version = "v1"}); + c.CustomSchemaIds((type) => type.FullName); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = + "Please enter into field the word 'Bearer' followed by a space and the JWT contents", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement { - new OpenApiSecurityScheme { - Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } - }, - new[] { string.Empty } - } + new OpenApiSecurityScheme + { + Reference = new OpenApiReference {Type = ReferenceType.SecurityScheme, Id = "Bearer"} + }, + new[] {string.Empty} + } + }); }); - }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -690,21 +637,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) // Enable caching and register any caching services. CacheAspect.Enabled = true; var privateCacheService = new BlobCacheService( - new PrivateBlobStorageService( - provider.GetRequiredService>(), - Configuration), + app.ApplicationServices.GetRequiredService(), provider.GetRequiredService>() ); var publicCacheService = new BlobCacheService( - new PublicBlobStorageService( - provider.GetRequiredService>(), - Configuration), + app.ApplicationServices.GetRequiredService(), provider.GetRequiredService>() ); BlobCacheAttribute.AddService("default", privateCacheService); BlobCacheAttribute.AddService("public", publicCacheService); - if (_applyDatabaseMigrations) + if (!env.IsIntegrationTests()) { UpdateDatabase(app, env); } @@ -771,9 +714,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseHealthChecks("/api/health"); app.UseAuthentication(); - app.UseIdentityServer(); app.UseAuthorization(); + if (!env.IsIntegrationTests()) + { + app.UseIdentityServer(); + } + // Deny access to all /Identity routes other than: // // /Identity/Account/Login @@ -805,7 +752,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) template: "{controller}/{action=Index}/{id?}"); }); - if (_configureSpa) + if (!env.IsIntegrationTests()) { app.UseSpa(spa => { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs index 46d0dd7ddea..79b9fb9c1eb 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Fixtures/TestApplicationFactory.cs @@ -1,4 +1,5 @@ #nullable enable +using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.AspNetCore.TestHost; @@ -6,6 +7,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Console; +using static GovUk.Education.ExploreEducationStatistics.Common.Extensions.HostEnvironmentExtensions; namespace GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; @@ -32,16 +34,13 @@ protected override IHostBuilder CreateHostBuilder() { builder .UseStartup() - .UseEnvironment("Development") + .UseIntegrationTestEnvironment() .UseTestServer(); }) .ConfigureAppConfiguration(config => { config.AddConfiguration(new ConfigurationBuilder() - .AddJsonFile("appsettings.json", optional: true) - .AddJsonFile("appsettings.Development.json", optional: true) - .AddJsonFile("appsettings.Local.json", optional: true) - .AddJsonFile("appsettings.IntegrationTests.json", optional: true) + .AddJsonFile($"appsettings.{IntegrationTestEnvironment}.json", optional: true) .Build()); }); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs new file mode 100644 index 00000000000..be5ba99b7ce --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs @@ -0,0 +1,20 @@ +#nullable enable +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace GovUk.Education.ExploreEducationStatistics.Common.Extensions; + +public static class HostEnvironmentExtensions +{ + public const string IntegrationTestEnvironment = "IntegrationTests"; + + public static bool IsIntegrationTests(this IHostEnvironment? hostEnvironment) => + hostEnvironment?.IsEnvironment(IntegrationTestEnvironment) ?? throw new ArgumentNullException(nameof(hostEnvironment)); + + public static IWebHostBuilder UseIntegrationTestEnvironment( + this IWebHostBuilder hostBuilder) + { + return hostBuilder.UseSetting(WebHostDefaults.EnvironmentKey, IntegrationTestEnvironment); + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs new file mode 100644 index 00000000000..a7c76b028d6 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs @@ -0,0 +1,11 @@ +using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; + +namespace GovUk.Education.ExploreEducationStatistics.Common.Services; + +public class CoreTableStorageService : TableStorageService, ICoreTableStorageService +{ + public CoreTableStorageService() + : base("CoreStorage") + { + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/ICoreTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/ICoreTableStorageService.cs new file mode 100644 index 00000000000..520b1f0eee1 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/ICoreTableStorageService.cs @@ -0,0 +1,3 @@ +namespace GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; + +public interface ICoreTableStorageService : ITableStorageService {} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/IPublisherTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/IPublisherTableStorageService.cs new file mode 100644 index 00000000000..cc7b801ea13 --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/Interfaces/IPublisherTableStorageService.cs @@ -0,0 +1,3 @@ +namespace GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; + +public interface IPublisherTableStorageService : ITableStorageService {} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs new file mode 100644 index 00000000000..dba307d422d --- /dev/null +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs @@ -0,0 +1,11 @@ +using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; + +namespace GovUk.Education.ExploreEducationStatistics.Common.Services; + +public class PublisherTableStorageService : TableStorageService, IPublisherTableStorageService +{ + public PublisherTableStorageService() + : base("PublisherStorage") + { + } +} \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs index 7ceb64772d5..cc1aa6dc247 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs @@ -7,18 +7,16 @@ namespace GovUk.Education.ExploreEducationStatistics.Common.Services { - public class TableStorageService : ITableStorageService + public abstract class TableStorageService : ITableStorageService { private readonly CloudTableClient _client; - private readonly StorageInstanceCreationUtil _storageInstanceCreationUtil; + private readonly StorageInstanceCreationUtil _storageInstanceCreationUtil = new(); - public TableStorageService( - string connectionString, - StorageInstanceCreationUtil storageInstanceCreationUtil) + protected TableStorageService( + string connectionString) { var account = CloudStorageAccount.Parse(connectionString); _client = account.CreateCloudTableClient(); - _storageInstanceCreationUtil = storageInstanceCreationUtil; } /// diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleasePublishingStatusService.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleasePublishingStatusService.cs index a384dfe9d46..8fb156f7ec1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleasePublishingStatusService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Services/ReleasePublishingStatusService.cs @@ -20,11 +20,11 @@ public class ReleasePublishingStatusService : IReleasePublishingStatusService { private readonly ContentDbContext _context; private readonly ILogger _logger; - private readonly ITableStorageService _tableStorageService; + private readonly IPublisherTableStorageService _tableStorageService; public ReleasePublishingStatusService(ContentDbContext context, ILogger logger, - ITableStorageService tableStorageService) + IPublisherTableStorageService tableStorageService) { _context = context; _logger = logger; diff --git a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs index 9cb176b55a0..4bd304df0c4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Publisher/Startup.cs @@ -82,10 +82,7 @@ public override void Configure(IFunctionsHostBuilder builder) methodologyCacheService: provider.GetRequiredService(), publicationCacheService: provider.GetRequiredService())) .AddScoped() - .AddScoped(provider => - new TableStorageService( - GetConfigurationValue(provider, "PublisherStorage"), - new StorageInstanceCreationUtil())) + .AddScoped() .AddScoped() .AddScoped() .AddScoped() From 42411e35bd10e7c570a44cef4fb0ba0c6edb73ed Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Thu, 21 Sep 2023 12:25:04 +0100 Subject: [PATCH 19/22] EES-4448 - updated IntegrationsTests to be singular from PR suggestion. --- ...ducation.ExploreEducationStatistics.Admin.Tests.csproj | 2 +- ...grationTests.json => appsettings.IntegrationTest.json} | 0 .../Startup.cs | 8 ++++---- .../Extensions/HostEnvironmentExtensions.cs | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/{appsettings.IntegrationTests.json => appsettings.IntegrationTest.json} (100%) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj index c9c620e311a..394e4b95378 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/GovUk.Education.ExploreEducationStatistics.Admin.Tests.csproj @@ -41,7 +41,7 @@ - + Always diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json similarity index 100% rename from src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json rename to src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTest.json diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index f552c9caba6..bee289733b1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -234,7 +234,7 @@ public virtual void ConfigureServices(IServiceCollection services) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - if (!HostEnvironment.IsIntegrationTests()) + if (!HostEnvironment.IsIntegrationTest()) { var identityServerConfig = services .AddIdentityServer(options => @@ -647,7 +647,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) BlobCacheAttribute.AddService("default", privateCacheService); BlobCacheAttribute.AddService("public", publicCacheService); - if (!env.IsIntegrationTests()) + if (!env.IsIntegrationTest()) { UpdateDatabase(app, env); } @@ -716,7 +716,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseAuthentication(); app.UseAuthorization(); - if (!env.IsIntegrationTests()) + if (!env.IsIntegrationTest()) { app.UseIdentityServer(); } @@ -752,7 +752,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) template: "{controller}/{action=Index}/{id?}"); }); - if (!env.IsIntegrationTests()) + if (!env.IsIntegrationTest()) { app.UseSpa(spa => { diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs index be5ba99b7ce..2f56a4ebd16 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Extensions/HostEnvironmentExtensions.cs @@ -7,9 +7,9 @@ namespace GovUk.Education.ExploreEducationStatistics.Common.Extensions; public static class HostEnvironmentExtensions { - public const string IntegrationTestEnvironment = "IntegrationTests"; + public const string IntegrationTestEnvironment = "IntegrationTest"; - public static bool IsIntegrationTests(this IHostEnvironment? hostEnvironment) => + public static bool IsIntegrationTest(this IHostEnvironment? hostEnvironment) => hostEnvironment?.IsEnvironment(IntegrationTestEnvironment) ?? throw new ArgumentNullException(nameof(hostEnvironment)); public static IWebHostBuilder UseIntegrationTestEnvironment( From 26657b995586064727daf7f6e93ed95ea9c061d8 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Wed, 20 Sep 2023 22:42:11 +0100 Subject: [PATCH 20/22] EES-4448 - updating Draft Releases, Scheduled Releases and Your Approvals Releases to use ReleaseSummaryViewModel as opposed to ReleaseViewModel for increased performance. An optional Publication field has been added to ReleaseSummaryViewModel to support this. --- .../Api/ReleasesControllerTests.cs | 2 +- .../Services/ReleaseServiceTests.cs | 4 +- .../Controllers/Api/ReleasesController.cs | 6 +- .../Mappings/MappingProfiles.cs | 1 + .../Services/Interfaces/IReleaseService.cs | 6 +- .../Methodologies/MethodologyService.cs | 2 +- .../Services/ReleaseRepository.cs | 2 + .../Services/ReleaseService.cs | 15 +-- .../ViewModels/ReleaseViewModels.cs | 6 +- .../components/ApprovalsTab.tsx | 4 +- .../components/ApprovalsTable.tsx | 40 +++---- .../components/DraftReleaseRow.tsx | 13 ++- .../components/DraftReleasesTab.tsx | 4 +- .../components/DraftReleasesTable.tsx | 30 ++--- .../components/ScheduledReleasesTab.tsx | 4 +- .../components/ScheduledReleasesTable.tsx | 32 ++--- .../__tests__/ApprovalsTable.test.tsx | 55 ++++----- .../__tests__/DraftReleasesTable.test.tsx | 109 ++++++++++++++---- .../__tests__/ScheduledReleasesTable.test.tsx | 80 ++++++++++--- .../publication/__data__/testReleases.ts | 3 + .../PublicationDraftReleases.test.tsx | 1 + .../PublicationPublishedReleases.test.tsx | 1 + .../PublicationReleaseAccess.test.tsx | 1 + .../PublicationScheduledReleases.test.tsx | 1 + .../PublicationUnpublishedReleases.test.tsx | 3 + .../src/services/releaseService.ts | 21 ++-- 26 files changed, 295 insertions(+), 151 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs index 1c39feeac17..c472286761d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs @@ -338,7 +338,7 @@ public async Task CreateReleaseStatus() [Fact] public async Task ListReleasesForApproval() { - var releases = ListOf(new ReleaseViewModel + var releases = ListOf(new ReleaseSummaryViewModel { Id = Guid.NewGuid() }); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs index 9ab386fc645..50871bf0c3e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs @@ -1708,11 +1708,11 @@ public async Task UserHasApproverRoleOnRelease() var viewModel = Assert.Single(viewModels); Assert.Equal(higherReviewReleaseWithApproverRoleForUser.Id, viewModel.Id); - // Assert that we have a fully populated ReleaseViewModel, including details from the owning + // Assert that we have a fully populated ReleaseSummaryViewModel, including details from the owning // Publication. Assert.Equal( higherReviewReleaseWithApproverRoleForUser.Publication.Title, - viewModel.PublicationTitle); + viewModel.Publication!.Title); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs index e6c9712973f..818ac949ea9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs @@ -199,7 +199,7 @@ public async Task> GetTemplateRelease( } [HttpGet("releases/draft")] - public async Task>> ListDraftReleases() + public async Task>> ListDraftReleases() { return await _releaseService .ListReleasesWithStatuses(ReleaseApprovalStatus.Draft, ReleaseApprovalStatus.HigherLevelReview) @@ -207,7 +207,7 @@ public async Task>> ListDraftReleases() } [HttpGet("releases/approvals")] - public async Task>> ListUsersReleasesForApproval() + public async Task>> ListUsersReleasesForApproval() { return await _releaseService .ListUsersReleasesForApproval() @@ -215,7 +215,7 @@ public async Task>> ListUsersReleasesForAppr } [HttpGet("releases/scheduled")] - public async Task>> ListScheduledReleases() + public async Task>> ListScheduledReleases() { return await _releaseService .ListScheduledReleases() diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs index 204757d703b..2651c0b9b39 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Mappings/MappingProfiles.cs @@ -73,6 +73,7 @@ public MappingProfiles() CreateMap(); CreateMap(); + CreateMap(); CreateMap() .ForMember( dest => dest.Theme, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs index 3da354fa3b9..0484701065a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs @@ -31,12 +31,12 @@ Task> UpdateReleasePublished(Guid releaseId, Task> GetLatestPublishedRelease(Guid publicationId); - Task>> ListReleasesWithStatuses( + Task>> ListReleasesWithStatuses( params ReleaseApprovalStatus[] releaseApprovalStatues); - Task>> ListUsersReleasesForApproval(); + Task>> ListUsersReleasesForApproval(); - Task>> ListScheduledReleases(); + Task>> ListScheduledReleases(); Task> GetDeleteDataFilePlan(Guid releaseId, Guid fileId); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index 1632b391438..04b6e2a3997 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -263,7 +263,7 @@ await _context.Entry(loadedMethodologyVersion) if (loadedMethodologyVersion.ScheduledWithRelease != null) { await _context.Entry(loadedMethodologyVersion.ScheduledWithRelease) - .Reference(r => r!.Publication) + .Reference(r => r.Publication) .LoadAsync(); var title = diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseRepository.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseRepository.cs index b8b6326dfb7..1b9000327db 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseRepository.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseRepository.cs @@ -55,12 +55,14 @@ public ReleaseRepository( .Where(r => r.UserId == userId) .Select(r => r.PublicationId) .ToListAsync(); + var userPublicationRoleReleaseIds = await _contentDbContext .Releases .AsQueryable() .Where(r => userPublicationIds.Contains(r.PublicationId)) .Select(r => r.Id) .ToListAsync(); + userReleaseIds.AddRange(userPublicationRoleReleaseIds); userReleaseIds = userReleaseIds.Distinct().ToList(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs index 34b8551cc94..dee14fcc312 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs @@ -415,7 +415,7 @@ public async Task> GetLatestPublishedRele }); } - public async Task>> ListReleasesWithStatuses( + public async Task>> ListReleasesWithStatuses( params ReleaseApprovalStatus[] releaseApprovalStatuses) { return await _userService @@ -435,7 +435,7 @@ public async Task>> ListReleasesWith .ToAsyncEnumerable() .SelectAwait(async release => { - var releaseViewModel = _mapper.Map(release); + var releaseViewModel = _mapper.Map(release); releaseViewModel.Permissions = await PermissionsUtils.GetReleasePermissions(_userService, release); return releaseViewModel; @@ -443,7 +443,7 @@ public async Task>> ListReleasesWith }); } - public async Task>> ListUsersReleasesForApproval() + public async Task>> ListUsersReleasesForApproval() { var userId = _userService.GetUserId(); @@ -471,10 +471,10 @@ public async Task>> ListUsersRelease && releaseIdsForApproval.Contains(release.Id)) .ToListAsync(); - return _mapper.Map>(releasesForApproval); + return _mapper.Map>(releasesForApproval); } - public async Task>> ListScheduledReleases() + public async Task>> ListScheduledReleases() { return await _userService .CheckCanAccessSystem() @@ -493,7 +493,7 @@ public async Task>> ListScheduledRel .ToAsyncEnumerable() .SelectAwait(async release => { - var releaseViewModel = _mapper.Map(release); + var releaseViewModel = _mapper.Map(release); releaseViewModel.Permissions = await PermissionsUtils.GetReleasePermissions(_userService, release); return releaseViewModel; @@ -643,7 +643,8 @@ public static IQueryable HydrateRelease(IQueryable values) { // Require publication / release graph to be able to work out: // If the release is the latest - return values.Include(r => r.Publication) + return values + .Include(r => r.Publication) .Include(r => r.ReleaseStatuses); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ReleaseViewModels.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ReleaseViewModels.cs index 8cad7e8c5b0..48df5374583 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ReleaseViewModels.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/ViewModels/ReleaseViewModels.cs @@ -117,7 +117,7 @@ public class ReleaseUpdateRequest public int Year { get; init; } } - public class ReleaseSummaryViewModel + public record ReleaseSummaryViewModel { public Guid Id { get; init; } @@ -149,8 +149,12 @@ public class ReleaseSummaryViewModel public bool Amendment { get; init; } + public bool LatestRelease { get; init; } + public Guid? PreviousVersionId { get; init; } public ReleasePermissions? Permissions { get; set; } + + public PublicationSummaryViewModel? Publication { get; set; } } } diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx index 316036f30ad..5e5a04753f3 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx @@ -1,5 +1,5 @@ import ApprovalsTable from '@admin/pages/admin-dashboard/components/ApprovalsTable'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import { MethodologyVersion } from '@admin/services/methodologyService'; import LoadingSpinner from '@common/components/LoadingSpinner'; import React from 'react'; @@ -7,7 +7,7 @@ import React from 'react'; interface Props { isLoading: boolean; methodologyApprovals: MethodologyVersion[]; - releaseApprovals: Release[]; + releaseApprovals: DashboardReleaseSummary[]; } export default function ApprovalsTab({ diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx index 0b986a9987c..729c2f52311 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -1,6 +1,6 @@ import Link from '@admin/components/Link'; import { MethodologyVersion } from '@admin/services/methodologyService'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import { MethodologyRouteParams, methodologyContentRoute, @@ -18,7 +18,7 @@ import merge from 'lodash/merge'; interface Props { methodologyApprovals: MethodologyVersion[]; - releaseApprovals: Release[]; + releaseApprovals: DashboardReleaseSummary[]; } export default function ApprovalsTable({ @@ -26,25 +26,25 @@ export default function ApprovalsTable({ releaseApprovals, }: Props) { const releasesByPublication: Dictionary<{ - releases?: Release[]; + releases: DashboardReleaseSummary[]; }> = useMemo(() => { - return releaseApprovals.reduce>( - (acc, release) => { - if (acc[release.publicationTitle]) { - acc[release.publicationTitle].releases.push(release); - } else { - acc[release.publicationTitle] = { - releases: [release], - }; - } - return acc; - }, - {}, - ); + return releaseApprovals.reduce< + Dictionary<{ releases: DashboardReleaseSummary[] }> + >((acc, release) => { + if (acc[release.publication.title]) { + acc[release.publication.title].releases.push(release); + } else { + acc[release.publication.title] = { + ...acc[release.publication.title], + releases: [release], + }; + } + return acc; + }, {}); }, [releaseApprovals]); const methodologiesByPublication: Dictionary<{ - methodologies?: MethodologyVersion[]; + methodologies: MethodologyVersion[]; }> = useMemo(() => { return methodologyApprovals.reduce< Dictionary<{ methodologies: MethodologyVersion[] }> @@ -103,8 +103,8 @@ export default function ApprovalsTable({ interface PublicationRowProps { publication: string; - methodologies?: MethodologyVersion[]; - releases?: Release[]; + methodologies: MethodologyVersion[]; + releases: DashboardReleaseSummary[]; } function PublicationRow({ @@ -129,7 +129,7 @@ function PublicationRow({
Publication / Page
{release.title} Release @@ -132,7 +135,10 @@ function PublicationRow({
{methodology.title} Methodology diff --git a/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot b/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot index 8b1e7124089..6859c51f813 100644 --- a/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot +++ b/tests/robot-tests/tests/admin/analyst/manage_approvals_as_publication_approver.robot @@ -21,7 +21,7 @@ ${SUBJECT_NAME} UI test subject *** Test Cases *** Create new publication and release via API - ${PUBLICATION_ID} user creates test publication via api ${PUBLICATION_NAME} + ${PUBLICATION_ID}= user creates test publication via api ${PUBLICATION_NAME} Set suite variable ${PUBLICATION_ID} user gives analyst publication approver access ${PUBLICATION_NAME} user creates test release via api ${PUBLICATION_ID} AY 2026 @@ -52,10 +52,36 @@ Check Analyst can see correct tabs user checks element should contain id:approvals-tab Your approvals user checks element should contain id:scheduled-releases-tab Approved scheduled releases -Validate if Your approvals page is correct +Validate if Your approvals tab is correct user clicks link approvals user waits until h2 is visible Your approvals user waits until page contains Here you can view any releases or methodologies awaiting approval. user checks table column heading contains 1 1 Publication / Page testid:your-approvals user checks table column heading contains 1 2 Page type testid:your-approvals user checks table column heading contains 1 3 Actions testid:your-approvals + + # Check for release and methodology + user checks page contains element testid:release-${RELEASE_NAME} + # Methodology title is inherited from publication + user checks page contains element testid:methodology-${PUBLICATION_NAME} - ${PUBLICATION_NAME} + +Check that release link takes user to the correct release + ${RELEASE_ROW}= get webelement testid:release-${RELEASE_NAME} + user clicks link by visible text Review this page ${RELEASE_ROW} + + user waits until h1 is visible ${PUBLICATION_NAME} %{WAIT_MEDIUM} + user waits until page contains title caption Edit release for Academic year 2026/27 + user checks page contains tag In Review + +Check that Your approvals tab methodology link takes user to the correct methodology + user navigates to admin dashboard + + user clicks link approvals + user waits until h2 is visible Your approvals + + ${METHODOLOGY_ROW}= get webelement testid:methodology-${PUBLICATION_NAME} - ${PUBLICATION_NAME} + user clicks link by visible text Review this page ${METHODOLOGY_ROW} + + user waits until h1 is visible ${PUBLICATION_NAME} + user waits until page contains title caption Edit methodology + user checks page contains tag In Review diff --git a/tests/robot-tests/tests/libs/admin-common.robot b/tests/robot-tests/tests/libs/admin-common.robot index 5e5001439ea..540bbe87e18 100644 --- a/tests/robot-tests/tests/libs/admin-common.robot +++ b/tests/robot-tests/tests/libs/admin-common.robot @@ -679,7 +679,6 @@ user changes methodology status to Higher level review user clicks element id:methodologyStatusForm-status-HigherLevelReview user clicks button Update status user waits until h2 is visible Sign off - #user waits until element is visible id:CurrentReleaseStatus-Awaiting higher review user checks page contains tag In Review user gives analyst publication owner access From de99624ba1021a87e5361e7fe4db26f4d320ac90 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Wed, 13 Sep 2023 10:58:08 +0100 Subject: [PATCH 12/22] EES-4448 - addressing PR comments. Internalised the fetching of userId into service methods to avoid missing permission checks and misuse of the user id. Removing unnecessary EF Includes. Adding code comments to make certain logic decisions easier to understand. --- .../MethodologyControllerTests.cs | 16 +- .../Api/ReleasesControllerTests.cs | 20 +- .../Methodologies/MethodologyServiceTests.cs | 426 +++++++++--------- .../Services/ReleaseServiceTests.cs | 94 ++-- .../Identity/Data/UsersAndRolesDbContext.cs | 2 + .../Methodologies/MethodologyController.cs | 8 +- .../Controllers/Api/ReleasesController.cs | 12 +- .../Api/Security/PermissionsController.cs | 9 +- .../Services/Interfaces/IReleaseService.cs | 2 +- .../Methodologies/IMethodologyService.cs | 2 +- .../Methodologies/MethodologyService.cs | 6 +- .../Services/ReleaseService.cs | 10 +- .../src/services/methodologyService.ts | 1 - 13 files changed, 288 insertions(+), 320 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs index 3aa33e77dca..45d00645e8e 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Methodologies/MethodologyControllerTests.cs @@ -249,7 +249,6 @@ public async void DeleteMethodologyVersion_Returns_NoContent() [Fact] public async void ListMethodologyVersionsForApproval() { - var userId = Guid.NewGuid(); var methodologyVersions = ListOf(new MethodologyVersionViewModel { Id = Guid.NewGuid() @@ -258,17 +257,12 @@ public async void ListMethodologyVersionsForApproval() var userService = new Mock(Strict); var methodologyService = new Mock(Strict); - userService - .Setup(s => s.GetUserId()) - .Returns(userId); - methodologyService - .Setup(s => s.ListMethodologyVersionsForApproval(userId)) + .Setup(s => s.ListUsersMethodologyVersionsForApproval()) .ReturnsAsync(methodologyVersions); var controller = SetupMethodologyController( - methodologyService.Object, - userService: userService.Object); + methodologyService.Object); var result = await controller.ListMethodologyVersionsForApproval(); VerifyAllMocks(userService, methodologyService); @@ -278,13 +272,11 @@ public async void ListMethodologyVersionsForApproval() private static MethodologyController SetupMethodologyController( IMethodologyService? methodologyService = null, - IMethodologyAmendmentService? methodologyAmendmentService = null, - IUserService? userService = null) + IMethodologyAmendmentService? methodologyAmendmentService = null) { return new( methodologyService ?? Mock.Of(Strict), - methodologyAmendmentService ?? Mock.Of(Strict), - userService ?? Mock.Of(Strict)); + methodologyAmendmentService ?? Mock.Of(Strict)); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs index 2309c7d1c7a..1c39feeac17 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/ReleasesControllerTests.cs @@ -10,7 +10,6 @@ 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.Tests.Extensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -339,29 +338,22 @@ public async Task CreateReleaseStatus() [Fact] public async Task ListReleasesForApproval() { - var userId = Guid.NewGuid(); var releases = ListOf(new ReleaseViewModel { Id = Guid.NewGuid() }); - var userService = new Mock(Strict); var releaseService = new Mock(Strict); - userService - .Setup(s => s.GetUserId()) - .Returns(userId); - releaseService - .Setup(s => s.ListReleasesForApproval(userId)) + .Setup(s => s.ListUsersReleasesForApproval()) .ReturnsAsync(releases); var controller = BuildController( - userService: userService.Object, releaseService: releaseService.Object); - var result = await controller.ListReleasesForApproval(); - VerifyAllMocks(userService, releaseService); + var result = await controller.ListUsersReleasesForApproval(); + VerifyAllMocks(releaseService); result.AssertOkResult(releases); } @@ -382,8 +374,7 @@ private static ReleasesController BuildController( IReleaseDataFileService? releaseDataFileService = null, IReleasePublishingStatusService? releaseStatusService = null, IReleaseChecklistService? releaseChecklistService = null, - IDataImportService? importService = null, - IUserService? userService = null) + IDataImportService? importService = null) { return new ReleasesController( releaseService ?? Mock.Of(Strict), @@ -391,8 +382,7 @@ private static ReleasesController BuildController( releaseDataFileService ?? Mock.Of(Strict), releaseStatusService ?? Mock.Of(Strict), releaseChecklistService ?? Mock.Of(Strict), - importService ?? Mock.Of(Strict), - userService ?? Mock.Of(Strict)); + importService ?? Mock.Of(Strict)); } } } 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 33870c8e721..35ffbbdfb02 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -3,17 +3,13 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using GovUk.Education.ExploreEducationStatistics.Admin.Security; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.Methodologies; using GovUk.Education.ExploreEducationStatistics.Admin.Services.Methodologies; -using GovUk.Education.ExploreEducationStatistics.Admin.Validators; using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodology; using GovUk.Education.ExploreEducationStatistics.Common.Model; -using GovUk.Education.ExploreEducationStatistics.Common.Services; using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; -using GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using GovUk.Education.ExploreEducationStatistics.Content.Model; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; @@ -25,12 +21,22 @@ using Microsoft.EntityFrameworkCore; using Moq; using Xunit; +using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityPolicies; +using static GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.DbUtils; +using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; +using static GovUk.Education.ExploreEducationStatistics.Common.Model.TimeIdentifier; +using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; +using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; +using static Moq.MockBehavior; namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services.Methodologies { public class MethodologyServiceTests { - private static readonly Guid UserId = Guid.NewGuid(); + private static readonly User User = new() + { + Id = Guid.NewGuid() + }; [Fact] public async Task AdoptMethodology() @@ -53,21 +59,21 @@ public async Task AdoptMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - var methodologyCacheService = new Mock(MockBehavior.Strict); + var methodologyCacheService = new Mock(Strict); methodologyCacheService.Setup(mock => mock.UpdateSummariesTree()) .ReturnsAsync( new Either>( new List())); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -75,12 +81,12 @@ public async Task AdoptMethodology() var result = await service.AdoptMethodology(publication.Id, methodology.Id); - MockUtils.VerifyAllMocks(methodologyCacheService); + VerifyAllMocks(methodologyCacheService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -162,24 +168,24 @@ public async Task AdoptMethodology_AlreadyAdoptedByPublicationFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, methodology.Id); - result.AssertBadRequest(ValidationErrorMessages.CannotAdoptMethodologyAlreadyLinkedToPublication); + result.AssertBadRequest(CannotAdoptMethodologyAlreadyLinkedToPublication); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -217,24 +223,24 @@ public async Task AdoptMethodology_AdoptingOwnedMethodologyFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, methodology.Id); - result.AssertBadRequest(ValidationErrorMessages.CannotAdoptMethodologyAlreadyLinkedToPublication); + result.AssertBadRequest(CannotAdoptMethodologyAlreadyLinkedToPublication); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -265,13 +271,13 @@ public async Task AdoptMethodology_PublicationNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -289,22 +295,22 @@ public async Task AdoptMethodology_MethodologyNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(MockBehavior.Strict); + var methodologyRepository = new Mock(Strict); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); var result = await service.AdoptMethodology(publication.Id, Guid.NewGuid()); - MockUtils.VerifyAllMocks(methodologyRepository); + VerifyAllMocks(methodologyRepository); result.AssertNotFound(); } @@ -320,15 +326,15 @@ public async Task CreateMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - var methodologyVersionRepository = new Mock(MockBehavior.Strict); + var methodologyVersionRepository = new Mock(Strict); var service = SetupMethodologyService( contentDbContext: context, @@ -355,13 +361,13 @@ public async Task CreateMethodology() }; methodologyVersionRepository - .Setup(s => s.CreateMethodologyForPublication(publication.Id, UserId)) + .Setup(s => s.CreateMethodologyForPublication(publication.Id, User.Id)) .ReturnsAsync(createdMethodology); context.Attach(createdMethodology); var viewModel = (await service.CreateMethodology(publication.Id)).AssertRight(); - MockUtils.VerifyAllMocks(methodologyVersionRepository); + VerifyAllMocks(methodologyVersionRepository); Assert.Equal(createdMethodology.Id, viewModel.Id); Assert.Equal("test-publication", viewModel.Slug); @@ -404,21 +410,21 @@ public async Task DropMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - var methodologyCacheService = new Mock(MockBehavior.Strict); + var methodologyCacheService = new Mock(Strict); methodologyCacheService.Setup(mock => mock.UpdateSummariesTree()) .ReturnsAsync( new Either>( new List())); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -426,12 +432,12 @@ public async Task DropMethodology() var result = await service.DropMethodology(publication.Id, methodology.Id); - MockUtils.VerifyAllMocks(methodologyCacheService); + VerifyAllMocks(methodologyCacheService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -468,14 +474,14 @@ public async Task DropMethodology_DropMethodologyNotAdoptedFails() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -485,7 +491,7 @@ public async Task DropMethodology_DropMethodologyNotAdoptedFails() result.AssertNotFound(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var publicationMethodologies = await context.PublicationMethodologies .AsQueryable() @@ -519,13 +525,13 @@ public async Task DropMethodology_PublicationNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -543,13 +549,13 @@ public async Task DropMethodology_MethodologyNotFound() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context); @@ -602,7 +608,7 @@ public async Task GetAdoptableMethodologies() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddRangeAsync(publication, adoptingPublication); await context.Methodologies.AddAsync(methodology); @@ -611,17 +617,17 @@ public async Task GetAdoptableMethodologies() await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(MockBehavior.Strict); - var methodologyVersionRepository = new Mock(MockBehavior.Strict); + var methodologyRepository = new Mock(Strict); + var methodologyVersionRepository = new Mock(Strict); methodologyRepository.Setup(mock => mock.GetUnrelatedToPublication(adoptingPublication.Id)) - .ReturnsAsync(CollectionUtils.ListOf(methodology)); + .ReturnsAsync(ListOf(methodology)); methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) .ReturnsAsync(methodologyVersion); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -630,7 +636,7 @@ public async Task GetAdoptableMethodologies() var result = (await service.GetAdoptableMethodologies(adoptingPublication.Id)).AssertRight(); - MockUtils.VerifyAllMocks(methodologyRepository, methodologyVersionRepository); + VerifyAllMocks(methodologyRepository, methodologyVersionRepository); Assert.Single(result); @@ -722,19 +728,19 @@ public async Task GetAdoptableMethodologies_NoUnrelatedMethodologies() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Publications.AddAsync(publication); await context.SaveChangesAsync(); } - var methodologyRepository = new Mock(MockBehavior.Strict); + var methodologyRepository = new Mock(Strict); methodologyRepository.Setup(mock => mock.GetPublishedMethodologiesUnrelatedToPublication(publication.Id)) .ReturnsAsync(new List()); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService( contentDbContext: context, @@ -742,7 +748,7 @@ public async Task GetAdoptableMethodologies_NoUnrelatedMethodologies() var result = (await service.GetAdoptableMethodologies(publication.Id)).AssertRight(); - MockUtils.VerifyAllMocks(methodologyRepository); + VerifyAllMocks(methodologyRepository); Assert.Empty(result); } @@ -772,7 +778,7 @@ public async Task GetMethodology() var adoptingPublication1 = new Publication { Title = "Adopting publication 1", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodology, @@ -784,7 +790,7 @@ public async Task GetMethodology() var adoptingPublication2 = new Publication { Title = "Adopting publication 2", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodology, @@ -801,7 +807,7 @@ public async Task GetMethodology() ScheduledWithRelease = new Release { Publication = owningPublication, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2021" }, Status = MethodologyApprovalStatus.Approved, @@ -817,7 +823,7 @@ public async Task GetMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.Publications.AddRangeAsync(owningPublication, adoptingPublication1, adoptingPublication2); @@ -826,7 +832,7 @@ public async Task GetMethodology() await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: context); @@ -870,36 +876,36 @@ public async Task GetUnpublishedReleasesUsingMethodology() var owningPublication = new Publication { Title = "Publication B", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = true } ), - Releases = CollectionUtils.ListOf( + Releases = ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2018" }, new Release { Published = null, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2021" }, new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2019" }, new Release { Published = null, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2020" } ) @@ -908,36 +914,36 @@ public async Task GetUnpublishedReleasesUsingMethodology() var adoptingPublication = new Publication { Title = "Publication A", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = false } ), - Releases = CollectionUtils.ListOf( + Releases = ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = TimeIdentifier.FinancialYearQ3, + TimePeriodCoverage = FinancialYearQ3, ReleaseName = "2020" }, new Release { Published = null, - TimePeriodCoverage = TimeIdentifier.FinancialYearQ2, + TimePeriodCoverage = FinancialYearQ2, ReleaseName = "2021" }, new Release { Published = null, - TimePeriodCoverage = TimeIdentifier.FinancialYearQ4, + TimePeriodCoverage = FinancialYearQ4, ReleaseName = "2020" }, new Release { Published = null, - TimePeriodCoverage = TimeIdentifier.FinancialYearQ1, + TimePeriodCoverage = FinancialYearQ1, ReleaseName = "2021" } ) @@ -945,14 +951,14 @@ public async Task GetUnpublishedReleasesUsingMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -962,19 +968,19 @@ public async Task GetUnpublishedReleasesUsingMethodology() // Check that only unpublished Releases are included and that they are in the correct order var expectedReleaseAtIndex0 = adoptingPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ2); + r.Year == 2021 && r.TimePeriodCoverage == FinancialYearQ2); var expectedReleaseAtIndex1 = adoptingPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ1); + r.Year == 2021 && r.TimePeriodCoverage == FinancialYearQ1); var expectedReleaseAtIndex2 = adoptingPublication.Releases.Single(r => - r.Year == 2020 && r.TimePeriodCoverage == TimeIdentifier.FinancialYearQ4); + r.Year == 2020 && r.TimePeriodCoverage == FinancialYearQ4); var expectedReleaseAtIndex3 = owningPublication.Releases.Single(r => - r.Year == 2021 && r.TimePeriodCoverage == TimeIdentifier.CalendarYear); + r.Year == 2021 && r.TimePeriodCoverage == CalendarYear); var expectedReleaseAtIndex4 = owningPublication.Releases.Single(r => - r.Year == 2020 && r.TimePeriodCoverage == TimeIdentifier.CalendarYear); + r.Year == 2020 && r.TimePeriodCoverage == CalendarYear); Assert.Equal(5, result.Count); @@ -995,7 +1001,7 @@ public async Task GetUnpublishedReleasesUsingMethodology() [Fact] public async Task GetUnpublishedReleasesUsingMethodology_MethodologyNotFound() { - await using var contentDbContext = DbUtils.InMemoryApplicationDbContext(); + await using var contentDbContext = InMemoryApplicationDbContext(); var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1015,7 +1021,7 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var owningPublication = new Publication { Title = "Owning publication", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, @@ -1027,7 +1033,7 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var adoptingPublication = new Publication { Title = "Adopting publication", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, @@ -1038,14 +1044,14 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoRelea var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1067,18 +1073,18 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var owningPublication = new Publication { Title = "Owning publication", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = true } ), - Releases = CollectionUtils.ListOf( + Releases = ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2021" } ) @@ -1087,18 +1093,18 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var adoptingPublication = new Publication { Title = "Adopting publication", - Methodologies = CollectionUtils.ListOf( + Methodologies = ListOf( new PublicationMethodology { Methodology = methodologyVersion.Methodology, Owner = false } ), - Releases = CollectionUtils.ListOf( + Releases = ListOf( new Release { Published = DateTime.UtcNow, - TimePeriodCoverage = TimeIdentifier.CalendarYear, + TimePeriodCoverage = CalendarYear, ReleaseName = "2021" } ) @@ -1106,14 +1112,14 @@ public async Task GetUnpublishedReleasesUsingMethodology_PublicationsHaveNoUnpub var contentDbContextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { await contentDbContext.MethodologyVersions.AddAsync(methodologyVersion); await contentDbContext.Publications.AddRangeAsync(owningPublication, adoptingPublication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext); @@ -1215,13 +1221,13 @@ public async Task ListLatestMethodologyVersions() var contextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) { await contentDbContext.Publications.AddAsync(publication); await contentDbContext.SaveChangesAsync(); } - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) { var service = SetupMethodologyService(contentDbContext); @@ -1282,32 +1288,32 @@ public async Task ListLatestMethodologyVersions_VerifyPermissions() var contextId = Guid.NewGuid().ToString(); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) { await contentDbContext.Publications.AddAsync(publication); await contentDbContext.SaveChangesAsync(); } - var userService = new Mock(MockBehavior.Strict); + var userService = new Mock(Strict); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanViewSpecificPublication)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanViewSpecificPublication)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanDeleteSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanDeleteSpecificMethodology)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanUpdateSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanUpdateSpecificMethodology)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanApproveSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanApproveSpecificMethodology)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanSubmitSpecificMethodologyToHigherReview)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanSubmitSpecificMethodologyToHigherReview)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanMarkSpecificMethodologyAsDraft)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanMarkSpecificMethodologyAsDraft)) .ReturnsAsync(true); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanMakeAmendmentOfSpecificMethodology)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanMakeAmendmentOfSpecificMethodology)) .ReturnsAsync(false); - userService.Setup(s => s.MatchesPolicy(It.IsAny(), SecurityPolicies.CanDropMethodologyLink)) + userService.Setup(s => s.MatchesPolicy(It.IsAny(), CanDropMethodologyLink)) .ReturnsAsync(true); - await using (var contentDbContext = DbUtils.InMemoryApplicationDbContext(contextId)) + await using (var contentDbContext = InMemoryApplicationDbContext(contextId)) { var service = SetupMethodologyService(contentDbContext: contentDbContext, userService: userService.Object); @@ -1315,7 +1321,7 @@ public async Task ListLatestMethodologyVersions_VerifyPermissions() var result = await service.ListLatestMethodologyVersions(publication.Id); var viewModels = result.AssertRight(); - MockUtils.VerifyAllMocks(userService); + VerifyAllMocks(userService); var viewModel = Assert.Single(viewModels); var permissions = viewModel.Permissions; @@ -1349,7 +1355,7 @@ public async Task UpdateMethodology() Id = Guid.NewGuid(), Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1367,13 +1373,13 @@ public async Task UpdateMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1392,7 +1398,7 @@ public async Task UpdateMethodology() Assert.Empty(viewModel.OtherPublications); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1429,7 +1435,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1448,13 +1454,13 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1473,7 +1479,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange() Assert.Empty(viewModel.OtherPublications); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1511,7 +1517,7 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1529,13 +1535,13 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1554,7 +1560,7 @@ public async Task UpdateMethodology_UpdatingTitleToMatchPublicationTitleUnsetsAl Assert.Empty(viewModel.OtherPublications); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1595,7 +1601,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse { Slug = "alternative-methodology-title", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1614,13 +1620,13 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -1639,7 +1645,7 @@ public async Task UpdateMethodology_UpdatingAmendmentSoSlugDoesNotChange_AndUnse Assert.Empty(viewModel.OtherPublications); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1679,7 +1685,7 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() { Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1709,21 +1715,21 @@ public async Task UpdateMethodology_SettingAlternativeTitleCausesSlugClash() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddRangeAsync(methodologyVersion, methodologyWithTargetSlug); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); var result = await service.UpdateMethodology(methodologyVersion.Id, request); - result.AssertBadRequest(ValidationErrorMessages.SlugNotUnique); + result.AssertBadRequest(SlugNotUnique); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var notUpdatedMethodology = await context .MethodologyVersions @@ -1760,7 +1766,7 @@ public async Task UpdateMethodology_StatusUpdate() Id = Guid.NewGuid(), Slug = "test-publication", OwningPublicationTitle = "Test publication", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { Owner = true, Publication = publication @@ -1778,13 +1784,13 @@ public async Task UpdateMethodology_StatusUpdate() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.MethodologyVersions.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var methodologyApprovalService = new Mock(); @@ -1799,10 +1805,10 @@ public async Task UpdateMethodology_StatusUpdate() await service.UpdateMethodology(methodologyVersion.Id, request); // Verify that the call to update the approval status happened. - MockUtils.VerifyAllMocks(methodologyApprovalService); + VerifyAllMocks(methodologyApprovalService); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var updatedMethodology = await context .MethodologyVersions @@ -1856,20 +1862,20 @@ public async Task DeleteMethodology() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that the Methodology and MethodologyVersions were created. Assert.Equal(1, context.Methodologies.Count()); Assert.Equal(4, context.MethodologyVersions.Count()); } - var methodologyImageService = new Mock(MockBehavior.Strict); + var methodologyImageService = new Mock(Strict); // Since the MethodologyVersions should be deleted in sequence, expect a call to delete images for each of the // versions in the same sequence @@ -1896,19 +1902,19 @@ public async Task DeleteMethodology() .Setup(s => s.DeleteAll(methodologyVersion1Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); var result = await service.DeleteMethodology(methodology.Id); - MockUtils.VerifyAllMocks(methodologyImageService); + VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the methodology and the versions have been successfully deleted Assert.Equal(0, context.Methodologies.Count()); @@ -1931,7 +1937,7 @@ public async Task DeleteMethodologyVersion() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Publications = CollectionUtils.ListOf(new PublicationMethodology + Publications = ListOf(new PublicationMethodology { MethodologyId = methodologyId, PublicationId = Guid.NewGuid(), @@ -1942,13 +1948,13 @@ public async Task DeleteMethodologyVersion() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.AddAsync(methodologyVersion); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that a Methodology, a MethodologyVersion and a PublicationMethodology row were // created. @@ -1960,24 +1966,24 @@ public async Task DeleteMethodologyVersion() .SingleAsync(m => m.MethodologyId == methodologyId)); } - var methodologyImageService = new Mock(MockBehavior.Strict); + var methodologyImageService = new Mock(Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodologyVersion.Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); var result = await service.DeleteMethodologyVersion(methodologyVersion.Id); - MockUtils.VerifyAllMocks(methodologyImageService); + VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the version has successfully been deleted and as it was the only version on the // methodology, the methodology is deleted too. @@ -2001,7 +2007,7 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf(new MethodologyVersion + Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2017,13 +2023,13 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddAsync(methodology); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Sanity check that there is a methodology with two versions. Assert.NotNull(await context.Methodologies.AsQueryable() @@ -2034,12 +2040,12 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() .SingleAsync(m => m.Id == methodology.Versions[1].Id)); } - var methodologyImageService = new Mock(MockBehavior.Strict); + var methodologyImageService = new Mock(Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodology.Versions[1].Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(contentDbContext: context, methodologyImageService: methodologyImageService.Object); @@ -2048,12 +2054,12 @@ public async Task DeleteMethodologyVersion_MoreThanOneVersion() // Verify that the Methodology Image Service was called to remove only the Methodology Files linked to // the version being deleted. - MockUtils.VerifyAllMocks(methodologyImageService); + VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the version has successfully been deleted and as there was another version attached // to the methodology, the methodology itself is not deleted, or the other version. @@ -2076,7 +2082,7 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf(new MethodologyVersion + Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2089,7 +2095,7 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf(new MethodologyVersion + Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2099,18 +2105,18 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.SaveChangesAsync(); } - var methodologyImageService = new Mock(MockBehavior.Strict); + var methodologyImageService = new Mock(Strict); methodologyImageService.Setup(mock => mock.DeleteAll(methodology.Versions[0].Id, false)) .ReturnsAsync(Unit.Instance); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context, methodologyImageService: methodologyImageService.Object); @@ -2119,12 +2125,12 @@ public async Task DeleteMethodologyVersion_UnrelatedMethodologiesAreUnaffected() // Verify that the Methodology Image Service was called to remove only the Methodology Files linked to // the version being deleted. - MockUtils.VerifyAllMocks(methodologyImageService); + VerifyAllMocks(methodologyImageService); result.AssertRight(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { // Assert that the methodology and its version are deleted, but the unrelated methodology is unaffected. Assert.False(context.MethodologyVersions.Any(m => m.Id == methodology.Versions[0].Id)); @@ -2150,7 +2156,7 @@ public async Task GetMethodologyStatuses() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf( + Versions = ListOf( new MethodologyVersion { PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2171,7 +2177,7 @@ public async Task GetMethodologyStatuses() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf(new MethodologyVersion + Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2187,7 +2193,7 @@ public async Task GetMethodologyStatuses() InternalReleaseNote = "Status 1 note", ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2000, 1, 1), - CreatedById = UserId, + CreatedById = User.Id, }, new() { @@ -2195,7 +2201,7 @@ public async Task GetMethodologyStatuses() InternalReleaseNote = "Status 2 note", ApprovalStatus = MethodologyApprovalStatus.Approved, Created = new DateTime(2001, 1, 1), - CreatedById = UserId, + CreatedById = User.Id, }, new() { @@ -2209,12 +2215,12 @@ public async Task GetMethodologyStatuses() var user = new User { - Id = UserId, + Id = User.Id, Email = "test@test.com", }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.MethodologyStatus.AddRangeAsync(methodologyStatuses); @@ -2222,7 +2228,7 @@ public async Task GetMethodologyStatuses() await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2253,7 +2259,7 @@ public async Task GetMethodologyStatuses() public async Task GetMethodologyStatuses_NoMethodologyVersion() { var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2274,7 +2280,7 @@ public async Task GetMethodologyStatuses_NoStatuses() Id = methodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf( + Versions = ListOf( new MethodologyVersion { PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2293,7 +2299,7 @@ public async Task GetMethodologyStatuses_NoStatuses() Id = unrelatedMethodologyId, Slug = "pupil-absence-statistics-methodology", OwningPublicationTitle = "Pupil absence statistics: methodology", - Versions = CollectionUtils.ListOf(new MethodologyVersion + Versions = ListOf(new MethodologyVersion { Id = Guid.NewGuid(), PublishingStrategy = MethodologyPublishingStrategy.Immediately, @@ -2314,14 +2320,14 @@ public async Task GetMethodologyStatuses_NoStatuses() }; var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology, unrelatedMethodology); await context.MethodologyStatus.AddRangeAsync(methodologyStatuses); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); @@ -2334,15 +2340,13 @@ public async Task GetMethodologyStatuses_NoStatuses() } } - public class ListMethodologyVersionsForApproval + public class ListUsersMethodologyVersionsForApproval { private readonly DataFixture _fixture = new(); [Fact] - public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPublication_Included() + public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverOnOwningPublication_Included() { - var user = new User(); - var publication = _fixture .DefaultPublication() .Generate(); @@ -2358,24 +2362,24 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPubli var publicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithPublication(publication) .WithRole(PublicationRole.Approver) .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(user.Id); + var result = await service.ListUsersMethodologyVersionsForApproval(); var methodologyVersionsForApproval = result.AssertRight(); var methodologyForApproval = Assert.Single(methodologyVersionsForApproval); @@ -2387,10 +2391,8 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverOnOwningPubli } [Fact] - public async Task ListMethodologyVersionsForApproval_MethodologyVersionNotInHigherReview_NotIncluded() + public async Task ListUsersMethodologyVersionsForApproval_MethodologyVersionNotInHigherReview_NotIncluded() { - var user = new User(); - var publication = _fixture .DefaultPublication() .Generate(); @@ -2411,32 +2413,30 @@ public async Task ListMethodologyVersionsForApproval_MethodologyVersionNotInHigh var publicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithPublication(publication) .WithRole(PublicationRole.Approver) .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodologies); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(user.Id); + var result = await service.ListUsersMethodologyVersionsForApproval(); Assert.Empty(result.AssertRight()); } } [Fact] - public async Task ListMethodologyVersionsForApproval_UserIsApproverButOnAdoptingPublication_NotIncluded() + public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverButOnAdoptingPublication_NotIncluded() { - var user = new User(); - var publication = _fixture .DefaultPublication() .Generate(); @@ -2453,33 +2453,31 @@ public async Task ListMethodologyVersionsForApproval_UserIsApproverButOnAdopting var publicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithPublication(publication) .WithRole(PublicationRole.Approver) .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(user.Id); + var result = await service.ListUsersMethodologyVersionsForApproval(); Assert.Empty(result.AssertRight()); } } [Fact] - public async Task ListMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPublication_NotIncluded() + public async Task ListUsersMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPublication_NotIncluded() { - var user = new User(); - var publication = _fixture .DefaultPublication() .Generate(); @@ -2496,29 +2494,29 @@ public async Task ListMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPubl // Set up the User as an Owner on the Methodology's Publication rather than an Approver. var publicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithPublication(publication) .WithRole(PublicationRole.Owner) .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(user.Id); + var result = await service.ListUsersMethodologyVersionsForApproval(); Assert.Empty(result.AssertRight()); } } [Fact] - public async Task ListMethodologyVersionsForApproval_DifferentUserIsApproverOnOwningPublication_NotIncluded() + public async Task ListUsersMethodologyVersionsForApproval_DifferentUserIsApproverOnOwningPublication_NotIncluded() { // Set up a different User as the Approver for the owning Publication. var otherUser = new User(); @@ -2544,26 +2542,24 @@ public async Task ListMethodologyVersionsForApproval_DifferentUserIsApproverOnOw .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(methodology); await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(Guid.NewGuid()); + var result = await service.ListUsersMethodologyVersionsForApproval(); Assert.Empty(result.AssertRight()); } } [Fact] - public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() + public async Task ListUsersMethodologyVersionsForApproval_MixOfMethodologies() { - var user = new User(); - var publications = _fixture .DefaultPublication() .GenerateList(2); @@ -2576,7 +2572,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .WithOwningPublication(owningPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(CollectionUtils.ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .GenerateList(2); @@ -2585,13 +2581,13 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .WithAdoptingPublication(adoptingPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(CollectionUtils.ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .Generate(2); var publicationRolesForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .ForIndex(0, s => s .SetPublication(owningPublication) .SetRole(PublicationRole.Approver)) @@ -2607,7 +2603,7 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() .GenerateList(); var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { await context.Methodologies.AddRangeAsync(ownedMethodologies); await context.Methodologies.AddRangeAsync(adoptedMethodologies); @@ -2615,11 +2611,11 @@ public async Task ListMethodologyVersionsForApproval_MixOfMethodologies() await context.SaveChangesAsync(); } - await using (var context = DbUtils.InMemoryApplicationDbContext(contentDbContextId)) + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { var service = SetupMethodologyService(context); - var result = await service.ListMethodologyVersionsForApproval(user.Id); + var result = await service.ListUsersMethodologyVersionsForApproval(); var methodologyVersionsForApproval = result.AssertRight(); // Assert that we just get the 2 MethodologyVersions where the user is the Approver of the Owning @@ -2645,12 +2641,12 @@ private static MethodologyService SetupMethodologyService( persistenceHelper ?? new PersistenceHelper(contentDbContext), contentDbContext, MapperUtils.AdminMapper(), - methodologyVersionRepository ?? Mock.Of(MockBehavior.Strict), - methodologyRepository ?? Mock.Of(MockBehavior.Strict), - methodologyImageService ?? Mock.Of(MockBehavior.Strict), - methodologyApprovalService ?? Mock.Of(MockBehavior.Strict), - methodologyCacheService ?? Mock.Of(MockBehavior.Strict), - userService ?? MockUtils.AlwaysTrueUserService(UserId).Object); + methodologyVersionRepository ?? Mock.Of(Strict), + methodologyRepository ?? Mock.Of(Strict), + methodologyImageService ?? Mock.Of(Strict), + methodologyApprovalService ?? Mock.Of(Strict), + methodologyCacheService ?? Mock.Of(Strict), + userService ?? AlwaysTrueUserService(User.Id).Object); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs index 568bcdfd7a6..9ee2fecc5bd 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs @@ -34,8 +34,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Validators.ValidationErrorMessages; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Common.Tests.Utils.MockUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyPublishingStrategy; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; using static Moq.MockBehavior; using IReleaseRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IReleaseRepository; using Release = GovUk.Education.ExploreEducationStatistics.Content.Model.Release; @@ -45,7 +43,10 @@ namespace GovUk.Education.ExploreEducationStatistics.Admin.Tests.Services { public class ReleaseServiceTests { - private static readonly Guid UserId = Guid.NewGuid(); + private static readonly User User = new() + { + Id = Guid.NewGuid() + }; [Fact] public async Task CreateReleaseNoTemplate() @@ -81,7 +82,7 @@ public async Task CreateReleaseNoTemplate() Assert.Equal("2018/19", result.YearTitle); Assert.Equal(TimeIdentifier.AcademicYear, result.TimePeriodCoverage); Assert.Equal(ReleaseType.OfficialStatistics, result.Type); - Assert.Equal(Draft, result.ApprovalStatus); + Assert.Equal(ReleaseApprovalStatus.Draft, result.ApprovalStatus); Assert.False(result.Amendment); Assert.False(result.LatestRelease); // Most recent - but not published yet. @@ -101,7 +102,7 @@ public async Task CreateReleaseNoTemplate() Assert.Equal(2018, actual.Year); Assert.Equal(TimeIdentifier.AcademicYear, actual.TimePeriodCoverage); Assert.Equal(ReleaseType.OfficialStatistics, actual.Type); - Assert.Equal(Draft, actual.ApprovalStatus); + Assert.Equal(ReleaseApprovalStatus.Draft, actual.ApprovalStatus); Assert.Equal(0, actual.Version); Assert.Null(actual.PreviousVersionId); @@ -265,7 +266,7 @@ public async Task RemoveDataFiles() { var release = new Release { - ApprovalStatus = Draft + ApprovalStatus = ReleaseApprovalStatus.Draft }; var subject = new Subject @@ -355,7 +356,7 @@ public async Task RemoveDataFiles_FileImporting() { var release = new Release { - ApprovalStatus = Draft + ApprovalStatus = ReleaseApprovalStatus.Draft }; var subject = new Subject @@ -405,7 +406,7 @@ public async Task RemoveDataFiles_ReplacementExists() { var release = new Release { - ApprovalStatus = Draft + ApprovalStatus = ReleaseApprovalStatus.Draft }; var subject = new Subject @@ -534,7 +535,7 @@ public async Task RemoveDataFiles_ReplacementFileImporting() { var release = new Release { - ApprovalStatus = Draft + ApprovalStatus = ReleaseApprovalStatus.Draft }; var subject = new Subject @@ -1133,7 +1134,7 @@ public async Task DeleteRelease() var methodologyScheduledWithRelease = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = WithRelease, + PublishingStrategy = MethodologyPublishingStrategy.WithRelease, ScheduledWithReleaseId = release.Id, Methodology = new Methodology { @@ -1145,7 +1146,7 @@ public async Task DeleteRelease() var methodologyScheduledWithAnotherRelease = new MethodologyVersion { Id = Guid.NewGuid(), - PublishingStrategy = WithRelease, + PublishingStrategy = MethodologyPublishingStrategy.WithRelease, ScheduledWithReleaseId = Guid.NewGuid(), Methodology = new Methodology { @@ -1155,7 +1156,7 @@ public async Task DeleteRelease() var userReleaseRole = new UserReleaseRole { - UserId = UserId, + UserId = User.Id, Release = release }; @@ -1632,16 +1633,15 @@ public async Task UpdateReleasePublished_ConvertsPublishedFromLocalToUniversalTi } } - public class ListReleasesForApproval + public class ListUsersReleasesForApproval { private readonly DataFixture _fixture = new(); [Fact] - public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() + public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnRelease() { var contextId = Guid.NewGuid().ToString(); - var user = new User(); var otherUser = new User(); var publications = _fixture @@ -1649,29 +1649,29 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() .WithReleases(_ => _fixture .DefaultRelease() .WithApprovalStatuses(ListOf( - Draft, - HigherLevelReview, - Approved)) + ReleaseApprovalStatus.Draft, + ReleaseApprovalStatus.HigherLevelReview, + ReleaseApprovalStatus.Approved)) .GenerateList()) .GenerateList(4); var contributorReleaseRolesForUser = _fixture .DefaultUserReleaseRole() - .WithUser(user) + .WithUser(User) .WithRole(ReleaseRole.Contributor) .WithReleases(publications[0].Releases) .GenerateList(); var approverReleaseRolesForUser = _fixture .DefaultUserReleaseRole() - .WithUser(user) + .WithUser(User) .WithRole(ReleaseRole.Approver) .WithReleases(publications[1].Releases) .GenerateList(); var prereleaseReleaseRolesForUser = _fixture .DefaultUserReleaseRole() - .WithUser(user) + .WithUser(User) .WithRole(ReleaseRole.PrereleaseViewer) .WithReleases(publications[2].Releases) .GenerateList(); @@ -1699,8 +1699,7 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() { var service = BuildReleaseService(context); - var result = await service - .ListReleasesForApproval(user.Id); + var result = await service.ListUsersReleasesForApproval(); var viewModels = result.AssertRight(); @@ -1718,11 +1717,10 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnRelease() } [Fact] - public async Task ListReleasesForApproval_UserHasApproverRoleOnPublications() + public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnPublications() { var contextId = Guid.NewGuid().ToString(); - var user = new User(); var otherUser = new User(); var publications = _fixture @@ -1730,23 +1728,23 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnPublications() .WithReleases(_ => _fixture .DefaultRelease() .WithApprovalStatuses(ListOf( - Draft, - HigherLevelReview, - Approved, - HigherLevelReview)) + ReleaseApprovalStatus.Draft, + ReleaseApprovalStatus.HigherLevelReview, + ReleaseApprovalStatus.Approved, + ReleaseApprovalStatus.HigherLevelReview)) .GenerateList()) .GenerateList(3); var ownerPublicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithRole(PublicationRole.Owner) .WithPublication(publications[0]) .Generate(); var approverPublicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithRole(PublicationRole.Approver) .WithPublication(publications[1]) .Generate(); @@ -1783,8 +1781,7 @@ await context.UserPublicationRoles.AddRangeAsync( { var service = BuildReleaseService(context); - var result = await service - .ListReleasesForApproval(user.Id); + var result = await service.ListUsersReleasesForApproval(); var viewModels = result.AssertRight(); @@ -1797,33 +1794,31 @@ await context.UserPublicationRoles.AddRangeAsync( } [Fact] - public async Task ListReleasesForApproval_MixOfApproverReleaseAndPublicationRoles() + public async Task ListUsersReleasesForApproval_MixOfApproverReleaseAndPublicationRoles() { var contextId = Guid.NewGuid().ToString(); - var user = new User(); - var publications = _fixture .DefaultPublication() .WithReleases(_ => _fixture .DefaultRelease() .WithApprovalStatuses(ListOf( - Draft, - HigherLevelReview, - Approved)) + ReleaseApprovalStatus.Draft, + ReleaseApprovalStatus.HigherLevelReview, + ReleaseApprovalStatus.Approved)) .GenerateList()) .GenerateList(3); var approverPublicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithRole(PublicationRole.Approver) .WithPublication(publications[0]) .Generate(); var approverReleaseRolesForUser = _fixture .DefaultUserReleaseRole() - .WithUser(user) + .WithUser(User) .WithRole(ReleaseRole.Approver) .WithReleases(publications[1].Releases) .GenerateList(); @@ -1843,8 +1838,7 @@ public async Task ListReleasesForApproval_MixOfApproverReleaseAndPublicationRole { var service = BuildReleaseService(context); - var result = await service - .ListReleasesForApproval(user.Id); + var result = await service.ListUsersReleasesForApproval(); var viewModels = result.AssertRight(); @@ -1858,30 +1852,28 @@ public async Task ListReleasesForApproval_MixOfApproverReleaseAndPublicationRole } [Fact] - public async Task ListReleasesForApproval_UserHasApproverRoleOnPublicationsAndApproverRoleOnRelease() + public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnPublicationsAndApproverRoleOnRelease() { var contextId = Guid.NewGuid().ToString(); - var user = new User(); - var publication = _fixture .DefaultPublication() .WithReleases(_ => _fixture .DefaultRelease() - .WithApprovalStatus(HigherLevelReview) + .WithApprovalStatus(ReleaseApprovalStatus.HigherLevelReview) .Generate(1)) .Generate(); var approverReleaseRolesForUser = _fixture .DefaultUserReleaseRole() - .WithUser(user) + .WithUser(User) .WithRole(ReleaseRole.Approver) .WithReleases(publication.Releases) .GenerateList(); var approverPublicationRoleForUser = _fixture .DefaultUserPublicationRole() - .WithUser(user) + .WithUser(User) .WithRole(PublicationRole.Approver) .WithPublication(publication) .Generate(); @@ -1898,7 +1890,7 @@ public async Task ListReleasesForApproval_UserHasApproverRoleOnPublicationsAndAp { var service = BuildReleaseService(context); - var result = await service.ListReleasesForApproval(user.Id); + var result = await service.ListUsersReleasesForApproval(); var viewModels = result.AssertRight(); @@ -1930,7 +1922,7 @@ private static ReleaseService BuildReleaseService( userService .Setup(s => s.GetUserId()) - .Returns(UserId); + .Returns(User.Id); return new ReleaseService( contentDbContext, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Areas/Identity/Data/UsersAndRolesDbContext.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Areas/Identity/Data/UsersAndRolesDbContext.cs index 4be4091d82c..5703e399fe6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Areas/Identity/Data/UsersAndRolesDbContext.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Areas/Identity/Data/UsersAndRolesDbContext.cs @@ -74,6 +74,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) string analystRoleId = Role.Analyst.GetEnumValue(); string prereleaseRoleId = Role.PrereleaseUser.GetEnumValue(); + // Note that when amending this list of Claims to add or remove Claims from a given Role, + // we also need to check to see if updates need to be addressed in ClaimsPrincipalUtils as well. modelBuilder.Entity>() .HasData( new IdentityRoleClaim diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs index a6c6db372e4..b01ee7a40a0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Methodologies/MethodologyController.cs @@ -7,7 +7,6 @@ using GovUk.Education.ExploreEducationStatistics.Admin.ViewModels.Methodology; using GovUk.Education.ExploreEducationStatistics.Common.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Model; -using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces.Security; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -20,16 +19,13 @@ public class MethodologyController : ControllerBase { private readonly IMethodologyService _methodologyService; private readonly IMethodologyAmendmentService _methodologyAmendmentService; - private readonly IUserService _userService; public MethodologyController( IMethodologyService methodologyService, - IMethodologyAmendmentService methodologyAmendmentService, - IUserService userService) + IMethodologyAmendmentService methodologyAmendmentService) { _methodologyService = methodologyService; _methodologyAmendmentService = methodologyAmendmentService; - _userService = userService; } [HttpPut("publication/{publicationId:guid}/methodology/{methodologyId:guid}")] @@ -128,7 +124,7 @@ public Task>> GetMethodologyStatus public async Task>> ListMethodologyVersionsForApproval() { return await _methodologyService - .ListMethodologyVersionsForApproval(_userService.GetUserId()) + .ListUsersMethodologyVersionsForApproval() .HandleFailuresOrOk(); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs index 4d9505e5571..e6c9712973f 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/ReleasesController.cs @@ -9,7 +9,6 @@ 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.Content.Model; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -28,7 +27,6 @@ public class ReleasesController : ControllerBase private readonly IReleasePublishingStatusService _releasePublishingStatusService; private readonly IReleaseChecklistService _releaseChecklistService; private readonly IDataImportService _dataImportService; - private readonly IUserService _userService; public ReleasesController( IReleaseService releaseService, @@ -36,8 +34,7 @@ public ReleasesController( IReleaseDataFileService releaseDataFileService, IReleasePublishingStatusService releasePublishingStatusService, IReleaseChecklistService releaseChecklistService, - IDataImportService dataImportService, - IUserService userService) + IDataImportService dataImportService) { _releaseService = releaseService; _releaseApprovalService = releaseApprovalService; @@ -45,7 +42,6 @@ public ReleasesController( _releasePublishingStatusService = releasePublishingStatusService; _releaseChecklistService = releaseChecklistService; _dataImportService = dataImportService; - _userService = userService; } [HttpPost("publications/{publicationId:guid}/releases")] @@ -211,10 +207,10 @@ public async Task>> ListDraftReleases() } [HttpGet("releases/approvals")] - public async Task>> ListReleasesForApproval() + public async Task>> ListUsersReleasesForApproval() { return await _releaseService - .ListReleasesForApproval(_userService.GetUserId()) + .ListUsersReleasesForApproval() .HandleFailuresOrOk(); } @@ -263,7 +259,7 @@ public async Task CancelFileImport(Guid releaseId, Guid fileId) { return await _dataImportService .CancelImport(releaseId, fileId) - .HandleFailuresOr(result => new AcceptedResult()); + .HandleFailuresOr(_ => new AcceptedResult()); } [HttpGet("releases/{releaseId:guid}/stage-status")] diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs index 14daf24b9dc..a3ac26900b5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Controllers/Api/Security/PermissionsController.cs @@ -48,6 +48,13 @@ public PermissionsController( public async Task> GetGlobalPermissions() { var isBauUser = await _userService.CheckIsBauUser().IsRight(); + + // Note that we are deliberately not giving BAU Users the Approver permission, as we would + // not expect a user with that role to be the target of specific Release or Publication + // roles. If they were to be given Approver permissions, we would therefore assume that they + // should have Approver access to ALL Methodologies and Releases that are awaiting approval, + // which would potentially be overwhelming. + var isApprover = !isBauUser && (await IsReleaseApprover() || await IsPublicationApprover()); return new GlobalPermissionsViewModel( CanAccessSystem: await _userService.CheckCanAccessSystem().IsRight(), @@ -56,7 +63,7 @@ public async Task> GetGlobalPermissions CanAccessPrereleasePages: await _userService.CheckCanAccessPrereleasePages().IsRight(), CanManageAllTaxonomy: await _userService.CheckCanManageAllTaxonomy().IsRight(), IsBauUser: isBauUser, - IsApprover: !isBauUser && (await IsReleaseApprover() || await IsPublicationApprover())); + IsApprover: isApprover); } [HttpGet("permissions/topic/{topicId:guid}/publication/create")] diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs index c5fdff0494f..3da354fa3b9 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/IReleaseService.cs @@ -34,7 +34,7 @@ Task> UpdateReleasePublished(Guid releaseId, Task>> ListReleasesWithStatuses( params ReleaseApprovalStatus[] releaseApprovalStatues); - Task>> ListReleasesForApproval(Guid userId); + Task>> ListUsersReleasesForApproval(); Task>> ListScheduledReleases(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs index b1ab03eb0cd..cd1b979270a 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Interfaces/Methodologies/IMethodologyService.cs @@ -41,6 +41,6 @@ Task BuildMethodologyVersionViewModel( Task>> GetMethodologyStatuses(Guid methodologyVersionId); - Task>> ListMethodologyVersionsForApproval(Guid userId); + Task>> ListUsersMethodologyVersionsForApproval(); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index 9df1042319f..de2d239504d 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -415,8 +415,10 @@ public Task>> GetMethodolo }); } - public async Task>> ListMethodologyVersionsForApproval(Guid userId) + public async Task>> ListUsersMethodologyVersionsForApproval() { + var userId = _userService.GetUserId(); + var publicationIdsForApprover = _context .UserPublicationRoles .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) @@ -424,8 +426,6 @@ public async Task>> ListM var methodologiesToApprove = await _context .MethodologyVersions - .Include(methodologyVersion => methodologyVersion.Methodology) - .ThenInclude(methodology => methodology.Publications) .Where(methodologyVersion => methodologyVersion.Status == MethodologyApprovalStatus.HigherLevelReview && methodologyVersion.Methodology.Publications.Any( diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs index d2e805fbbf8..931dc7c8345 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs @@ -443,8 +443,10 @@ public async Task>> ListReleasesWith }); } - public async Task>> ListReleasesForApproval(Guid userId) + public async Task>> ListUsersReleasesForApproval() { + var userId = _userService.GetUserId(); + var directReleasesWithApprovalRole = await _context .UserReleaseRoles .Where(role => role.UserId == userId && role.Role == ReleaseRole.Approver) @@ -453,8 +455,6 @@ public async Task>> ListReleasesForA var indirectReleasesWithApprovalRole = await _context .UserPublicationRoles - .Include(role => role.Publication) - .ThenInclude(publication => publication.Releases) .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) .Select(role => role.Publication) .SelectMany(publication => publication.Releases.Select(release => release.Id)) @@ -472,9 +472,7 @@ public async Task>> ListReleasesForA && releaseIdsForApproval.Contains(release.Id)) .ToListAsync(); - return releasesForApproval - .Select(_mapper.Map) - .ToList(); + return _mapper.Map>(releasesForApproval); } public async Task>> ListScheduledReleases() diff --git a/src/explore-education-statistics-admin/src/services/methodologyService.ts b/src/explore-education-statistics-admin/src/services/methodologyService.ts index cffa9727c33..3b38f427dd8 100644 --- a/src/explore-education-statistics-admin/src/services/methodologyService.ts +++ b/src/explore-education-statistics-admin/src/services/methodologyService.ts @@ -1,6 +1,5 @@ import client from '@admin/services/utils/service'; import { IdTitlePair } from '@admin/services/types/common'; -import { Release } from '@admin/services/releaseService'; export type MethodologyApprovalStatus = | 'Draft' From bdbbd39e2351b1efa6de6d69c1373929149c6a67 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Mon, 18 Sep 2023 11:27:55 +0100 Subject: [PATCH 13/22] EES-4448 move approvals message --- .../components/ApprovalsTab.tsx | 3 -- .../components/ApprovalsTable.tsx | 39 +++++++++++-------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx index 30a03647161..316036f30ad 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTab.tsx @@ -20,9 +20,6 @@ export default function ApprovalsTab({

Your approvals

-

- Here you can view any releases or methodologies awaiting approval. -

diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx index dd66b936d73..5db1f2426f4 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ApprovalsTable.tsx @@ -76,23 +76,28 @@ export default function ApprovalsTable({ } return ( - - - - - - - - {orderBy(Object.keys(allApprovalsByPublication)).map(publication => ( - - ))} - -
Publication / PagePage typeActions
+ <> +

Here you can view any releases or methodologies awaiting approval.

+ + + + + + + + {orderBy(Object.keys(allApprovalsByPublication)).map(publication => ( + + ))} + +
Publication / PagePage typeActions
+ ); } From d5a9e2cff18ea738002d7fffde6c7f4167db7c4f Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Mon, 18 Sep 2023 14:36:35 +0100 Subject: [PATCH 14/22] EES-4448 - adding additional logic and unit tests around allowing Approvers of Owning Publication Releases to have Higher Revier Methodologies appearing in their "Your Approvals" tab. --- .../Methodologies/MethodologyServiceTests.cs | 332 +++++++++++++++++- ...sToSpecificReleaseAuthorizationHandlers.cs | 3 +- ...thodologyAsApprovedAuthorizationHandler.cs | 3 +- ...kMethodologyAsDraftAuthorizationHandler.cs | 5 +- ...AsHigherLevelReviewAuthorizationHandler.cs | 5 +- ...SpecificMethodologyAuthorizationHandler.cs | 3 +- ...dateSpecificReleaseAuthorizationHandler.cs | 5 +- ...pecificMethodologyAuthorizationHandlers.cs | 3 +- ...pecificPublicationAuthorizationHandlers.cs | 3 +- .../Methodologies/MethodologyService.cs | 19 +- 10 files changed, 346 insertions(+), 35 deletions(-) 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 35ffbbdfb02..37eb30438e4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -2340,12 +2340,12 @@ public async Task GetMethodologyStatuses_NoStatuses() } } - public class ListUsersMethodologyVersionsForApproval + public class ListUsersMethodologyVersionsForApprovalForPublicationRoles { private readonly DataFixture _fixture = new(); [Fact] - public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverOnOwningPublication_Included() + public async Task UserIsApproverOnOwningPublication_Included() { var publication = _fixture .DefaultPublication() @@ -2391,7 +2391,7 @@ public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverOnOwning } [Fact] - public async Task ListUsersMethodologyVersionsForApproval_MethodologyVersionNotInHigherReview_NotIncluded() + public async Task MethodologyVersionNotInHigherReview_NotIncluded() { var publication = _fixture .DefaultPublication() @@ -2435,7 +2435,7 @@ public async Task ListUsersMethodologyVersionsForApproval_MethodologyVersionNotI } [Fact] - public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverButOnAdoptingPublication_NotIncluded() + public async Task UserIsApproverButOnAdoptingPublication_NotIncluded() { var publication = _fixture .DefaultPublication() @@ -2476,7 +2476,7 @@ public async Task ListUsersMethodologyVersionsForApproval_UserIsApproverButOnAdo } [Fact] - public async Task ListUsersMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwningPublication_NotIncluded() + public async Task UserIsOnlyOwnerOnOwningPublication_NotIncluded() { var publication = _fixture .DefaultPublication() @@ -2516,7 +2516,7 @@ public async Task ListUsersMethodologyVersionsForApproval_UserIsOnlyOwnerOnOwnin } [Fact] - public async Task ListUsersMethodologyVersionsForApproval_DifferentUserIsApproverOnOwningPublication_NotIncluded() + public async Task DifferentUserIsApproverOnOwningPublication_NotIncluded() { // Set up a different User as the Approver for the owning Publication. var otherUser = new User(); @@ -2556,9 +2556,9 @@ public async Task ListUsersMethodologyVersionsForApproval_DifferentUserIsApprove Assert.Empty(result.AssertRight()); } } - + [Fact] - public async Task ListUsersMethodologyVersionsForApproval_MixOfMethodologies() + public async Task MixOfMethodologies() { var publications = _fixture .DefaultPublication() @@ -2566,22 +2566,24 @@ public async Task ListUsersMethodologyVersionsForApproval_MixOfMethodologies() var owningPublication = publications[0]; var adoptingPublication = publications[1]; - + var ownedMethodologies = _fixture .DefaultMethodology() .WithOwningPublication(owningPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, + MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .GenerateList(2); - + var adoptedMethodologies = _fixture .DefaultMethodology() .WithAdoptingPublication(adoptingPublication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, + MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) .GenerateList()) .Generate(2); @@ -2627,6 +2629,312 @@ public async Task ListUsersMethodologyVersionsForApproval_MixOfMethodologies() } } + public class ListUsersMethodologyVersionsForApprovalForReleaseRoles + { + private readonly DataFixture _fixture = new(); + + [Fact] + public async Task UserIsApproverOnOwningPublicationRelease_Included() + { + var release = _fixture.DefaultRelease().Generate(); + + var publication = _fixture + .DefaultPublication() + .WithReleases(ListOf(release)) + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) + .Generate(1)) + .Generate(); + + var releaseRoleForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(User) + .WithRelease(release) + .WithRole(ReleaseRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListUsersMethodologyVersionsForApproval(); + var methodologyVersionsForApproval = result.AssertRight(); + + var methodologyForApproval = Assert.Single(methodologyVersionsForApproval); + Assert.Equal(methodology.Versions[0].Id, methodologyForApproval.Id); + + // Assert that we have a populated view model, including the owning Publication details. + Assert.Equal(publication.Title, methodologyForApproval.OwningPublication.Title); + } + } + + [Fact] + public async Task UserIsApproverOnOwningPublicationRelease_MethodologyVersionNotInHigherReview_NotIncluded() + { + var release = _fixture.DefaultRelease().Generate(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + // Generate 2 Methodologies that are not in Higher Review. + var methodologies = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .ForIndex(0, s => s.SetMethodologyVersions(_fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.Draft) + .Generate(1))) + .ForIndex(1, s => s.SetMethodologyVersions(_fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.Approved) + .Generate(1))) + .GenerateList(); + + var releaseRoleForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(User) + .WithRelease(release) + .WithRole(ReleaseRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodologies); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListUsersMethodologyVersionsForApproval(); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task UserIsReleaseApproverButOnAdoptingPublication_NotIncluded() + { + var release = _fixture.DefaultRelease().Generate(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + // Create a Methodology that has only been adopted by the User's Publication. + var methodology = _fixture + .DefaultMethodology() + .WithAdoptingPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) + .Generate(1)) + .Generate(); + + var releaseRoleForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(User) + .WithRelease(release) + .WithRole(ReleaseRole.Approver) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListUsersMethodologyVersionsForApproval(); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task UserIsOnlyContributorOnOwningPublicationRelease_NotIncluded() + { + var release = _fixture.DefaultRelease().Generate(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) + .Generate(1)) + .Generate(); + + // Set up the User as a Contributor on the Methodology's Publication's Release rather than an Approver. + var releaseRoleForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(User) + .WithRelease(release) + .WithRole(ReleaseRole.Contributor) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListUsersMethodologyVersionsForApproval(); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task DifferentUserIsApproverOnOwningPublicationRelease_NotIncluded() + { + // Set up a different User as the Approver for the owning Publication. + var otherUser = new User(); + + var release = _fixture.DefaultRelease().Generate(); + + var publication = _fixture + .DefaultPublication() + .Generate(); + + var methodology = _fixture + .DefaultMethodology() + .WithOwningPublication(publication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) + .Generate(1)) + .Generate(); + + var releaseRoleForOtherUser = _fixture + .DefaultUserReleaseRole() + .WithUser(otherUser) + .WithRelease(release) + .WithRole(ReleaseRole.Contributor) + .Generate(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(methodology); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForOtherUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + var result = await service.ListUsersMethodologyVersionsForApproval(); + Assert.Empty(result.AssertRight()); + } + } + + [Fact] + public async Task MixOfMethodologies() + { + var owningRelease = _fixture.DefaultRelease().Generate(); + var adoptingRelease = _fixture.DefaultRelease().Generate(); + + var owningPublication = _fixture + .DefaultPublication() + .WithReleases(ListOf(owningRelease)) + .Generate(); + + var adoptingPublication = _fixture + .DefaultPublication() + .WithReleases(ListOf(adoptingRelease)) + .Generate(); + + var ownedMethodologies = _fixture + .DefaultMethodology() + .WithOwningPublication(owningPublication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, + MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .GenerateList()) + .GenerateList(2); + + var adoptedMethodologies = _fixture + .DefaultMethodology() + .WithAdoptingPublication(adoptingPublication) + .WithMethodologyVersions(_ => _fixture + .DefaultMethodologyVersion() + .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, + MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) + .GenerateList()) + .Generate(2); + + var releaseRolesForUser = _fixture + .DefaultUserReleaseRole() + .WithUser(User) + .ForIndex(0, s => s + .SetRelease(owningRelease) + .SetRole(ReleaseRole.Approver)) + .ForIndex(1, s => s + .SetRelease(owningRelease) + .SetRole(ReleaseRole.Approver)) + .ForIndex(2, s => s + .SetRelease(adoptingRelease) + .SetRole(ReleaseRole.Approver)) + .ForIndex(3, s => s + .SetRelease(adoptingRelease) + .SetRole(ReleaseRole.Contributor)) + .GenerateList(); + + var contentDbContextId = Guid.NewGuid().ToString(); + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + await context.Methodologies.AddRangeAsync(ownedMethodologies); + await context.Methodologies.AddRangeAsync(adoptedMethodologies); + await context.UserReleaseRoles.AddRangeAsync(releaseRolesForUser); + await context.SaveChangesAsync(); + } + + await using (var context = InMemoryApplicationDbContext(contentDbContextId)) + { + var service = SetupMethodologyService(context); + + var result = await service.ListUsersMethodologyVersionsForApproval(); + var methodologyVersionsForApproval = result.AssertRight(); + + // Assert that we just get the 2 MethodologyVersions where the user is the Approver of the Owning + // Publication's Release and their statuses are Higher Review. + Assert.Equal(2, methodologyVersionsForApproval.Count); + Assert.Equal(ownedMethodologies[0].Versions[1].Id, methodologyVersionsForApproval[0].Id); + Assert.Equal(ownedMethodologies[1].Versions[1].Id, methodologyVersionsForApproval[1].Id); + } + } + } + private static MethodologyService SetupMethodologyService( ContentDbContext contentDbContext, IPersistenceHelper? persistenceHelper = null, diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs index 7b70a1055ba..c1c48870c69 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/AssignPrereleaseContactsToSpecificReleaseAuthorizationHandlers.cs @@ -5,7 +5,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers { @@ -40,7 +39,7 @@ protected override async Task HandleRequirementAsync( context.User.GetUserId(), release.PublicationId, release.Id, - ListOf(Owner, Approver), + ListOf(PublicationRole.Owner, PublicationRole.Approver), ReleaseEditorAndApproverRoles)) { context.Succeed(requirement); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs index a9eb3167a67..69b356e69e5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsApprovedAuthorizationHandler.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; @@ -58,7 +57,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext .HasRolesOnPublicationOrLatestRelease( context.User.GetUserId(), owningPublication.Id, - ListOf(Approver), + ListOf(PublicationRole.Approver), ListOf(ReleaseRole.Approver))) { context.Succeed(requirement); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs index 7f986cc18ff..877c99d2507 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsDraftAuthorizationHandler.cs @@ -8,7 +8,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; @@ -51,8 +50,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext } var allowedPublicationRoles = methodologyVersion.Status == Approved - ? ListOf(Approver) - : ListOf(Owner, Approver); + ? ListOf(PublicationRole.Approver) + : ListOf(PublicationRole.Owner, PublicationRole.Approver); var allowedReleaseRoles = methodologyVersion.Status == Approved ? ListOf(ReleaseRole.Approver) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs index 438780bc7ef..1907f7323e7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/MarkMethodologyAsHigherLevelReviewAuthorizationHandler.cs @@ -8,7 +8,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; using static GovUk.Education.ExploreEducationStatistics.Content.Model.MethodologyApprovalStatus; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers; @@ -50,8 +49,8 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext } var allowedPublicationRoles = methodologyVersion.Status == Approved - ? ListOf(Approver) - : ListOf(Owner, Approver); + ? ListOf(PublicationRole.Approver) + : ListOf(PublicationRole.Owner, PublicationRole.Approver); var allowedReleaseRoles = methodologyVersion.Status == Approved ? ListOf(ReleaseRole.Approver) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs index bf1dfeac3d2..430043bb6a5 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificMethodologyAuthorizationHandler.cs @@ -7,7 +7,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.SecurityClaimTypes; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers { @@ -51,7 +50,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext .HasRolesOnPublicationOrLatestRelease( context.User.GetUserId(), owningPublication.Id, - ListOf(Owner, Approver), + ListOf(PublicationRole.Owner, PublicationRole.Approver), ReleaseEditorAndApproverRoles)) { context.Succeed(requirement); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs index 270f46be1da..109bbda1bd0 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/UpdateSpecificReleaseAuthorizationHandler.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Authorization; using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers.AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using static GovUk.Education.ExploreEducationStatistics.Content.Model.ReleaseApprovalStatus; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers @@ -53,8 +52,8 @@ protected override async Task HandleRequirementAsync( } var allowedPublicationRoles = release.ApprovalStatus == Approved - ? ListOf(Approver) - : ListOf(Owner, Approver); + ? ListOf(PublicationRole.Approver) + : ListOf(PublicationRole.Owner, PublicationRole.Approver); var allowedReleaseRoles = release.ApprovalStatus == Approved ? ListOf(ReleaseRole.Approver) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs index 56970503674..7684287757c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificMethodologyAuthorizationHandlers.cs @@ -10,7 +10,6 @@ using static GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers. AuthorizationHandlerResourceRoleService; using static GovUk.Education.ExploreEducationStatistics.Common.Services.CollectionUtils; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; using IPublicationRepository = GovUk.Education.ExploreEducationStatistics.Admin.Services.Interfaces.IPublicationRepository; @@ -64,7 +63,7 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext .HasRolesOnPublicationOrLatestRelease( context.User.GetUserId(), owningPublication.Id, - ListOf(Owner, Approver), + ListOf(PublicationRole.Owner, PublicationRole.Approver), ReleaseEditorAndApproverRoles)) { context.Succeed(requirement); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs index ffbac4d8262..c235c8899f4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Security/AuthorizationHandlers/ViewSpecificPublicationAuthorizationHandlers.cs @@ -5,7 +5,6 @@ using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; using Microsoft.AspNetCore.Authorization; using Microsoft.EntityFrameworkCore; -using static GovUk.Education.ExploreEducationStatistics.Content.Model.PublicationRole; namespace GovUk.Education.ExploreEducationStatistics.Admin.Security.AuthorizationHandlers { @@ -82,7 +81,7 @@ protected override async Task HandleRequirementAsync( .HasRolesOnPublication( context.User.GetUserId(), publication.Id, - Owner, Approver)) + PublicationRole.Owner, PublicationRole.Approver)) { context.Succeed(requirement); } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs index de2d239504d..1632b391438 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/Methodologies/MethodologyService.cs @@ -419,11 +419,22 @@ public async Task>> ListU { var userId = _userService.GetUserId(); - var publicationIdsForApprover = _context + var directPublicationsWithApprovalRole = await _context .UserPublicationRoles .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) - .Select(role => role.PublicationId); - + .Select(role => role.PublicationId) + .ToListAsync(); + + var indirectPublicationsWithApprovalRole = await _context + .UserReleaseRoles + .Where(role => role.UserId == userId && role.Role == ReleaseRole.Approver) + .Select(role => role.Release.PublicationId) + .ToListAsync(); + + var publicationIdsForApproval = directPublicationsWithApprovalRole + .Concat(indirectPublicationsWithApprovalRole) + .Distinct(); + var methodologiesToApprove = await _context .MethodologyVersions .Where(methodologyVersion => @@ -431,7 +442,7 @@ public async Task>> ListU && methodologyVersion.Methodology.Publications.Any( publicationMethodology => publicationMethodology.Owner - && publicationIdsForApprover.Contains(publicationMethodology.PublicationId))) + && publicationIdsForApproval.Contains(publicationMethodology.PublicationId))) .ToListAsync(); return (await methodologiesToApprove From c057dfc746a5f51dfacf5587e3b8d7e4640e305d Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Wed, 20 Sep 2023 11:26:17 +0100 Subject: [PATCH 15/22] EES-4448 - responding to PR comments. Various renames and overloads of test utility methods to be more semantically correct and offer more flexibility. Renames of various test suite methods to be less verbose and removal of duplicate test method. Shortening of Linq query. --- .../Security/PermissionsControllerTests.cs | 123 ++++++++---------- .../Statistics/TableBuilderControllerTests.cs | 6 +- .../Methodologies/MethodologyServiceTests.cs | 78 ++++------- .../Services/ReleaseServiceTests.cs | 64 +-------- .../TestStartup.cs | 65 +++------ .../appsettings.IntegrationTests.json | 6 +- .../Services/ReleaseService.cs | 3 +- .../Startup.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 27 +++- .../Fixtures/ReleaseGeneratorExtensions.cs | 2 +- 10 files changed, 134 insertions(+), 242 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs index 31c64d786e4..4e7b7dec496 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Security/PermissionsControllerTests.cs @@ -24,28 +24,32 @@ public PermissionsControllerTests(TestApplicationFactory testApp) [Fact] public async Task GetGlobalPermissions_AuthenticatedUser() { - var user = AuthenticatedUser(); + var client = _testApp + .SetUser(AuthenticatedUser()) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); - var expectedPermissions = new GlobalPermissionsViewModel( + response.AssertOk(new GlobalPermissionsViewModel( CanAccessSystem: true, CanAccessAnalystPages: false, CanAccessAllImports: false, CanAccessPrereleasePages: false, CanManageAllTaxonomy: false, IsBauUser: false, - IsApprover: false); - - var client = SetupApp().SetUser(user).CreateClient(); - var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); + IsApprover: false)); } [Fact] public async Task GetGlobalPermissions_BauUser() { - var user = BauUser(); - - var expectedPermissions = new GlobalPermissionsViewModel( + var client = _testApp + .SetUser(BauUser()) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + + response.AssertOk(new GlobalPermissionsViewModel( CanAccessSystem: true, CanAccessAnalystPages: true, CanAccessAllImports: true, @@ -54,11 +58,7 @@ public async Task GetGlobalPermissions_BauUser() IsBauUser: true, // Expect "IsApprover" to be false even for BAU as we don't expect BAU users to be assigned // individual Approver roles on Releases or Publications. - IsApprover: false); - - var client = SetupApp().SetUser(user).CreateClient(); - var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); + IsApprover: false)); } [Fact] @@ -66,17 +66,7 @@ public async Task GetGlobalPermissions_AnalystUser_NotReleaseOrPublicationApprov { var user = AnalystUser(); - var expectedPermissions = new GlobalPermissionsViewModel( - CanAccessSystem: true, - CanAccessAnalystPages: true, - CanAccessAllImports: false, - CanAccessPrereleasePages: true, - CanManageAllTaxonomy: false, - IsBauUser: false, - // Expect this to be false if the user isn't an approver of any kind - IsApprover: false); - - var client = SetupApp() + var client = _testApp .SetUser(user) .AddContentDbTestData(context => { @@ -97,25 +87,24 @@ public async Task GetGlobalPermissions_AnalystUser_NotReleaseOrPublicationApprov .CreateClient(); var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); - } - - [Fact] - public async Task GetGlobalPermissions_AnalystUser_ReleaseApprover() - { - var user = AnalystUser(); - - var expectedPermissions = new GlobalPermissionsViewModel( + + response.AssertOk(new GlobalPermissionsViewModel( CanAccessSystem: true, CanAccessAnalystPages: true, CanAccessAllImports: false, CanAccessPrereleasePages: true, CanManageAllTaxonomy: false, IsBauUser: false, - // Expect this to be true if the user is a Release approver - IsApprover: true); + // Expect this to be false if the user isn't an approver of any kind + IsApprover: false)); + } + + [Fact] + public async Task GetGlobalPermissions_AnalystUser_ReleaseApprover() + { + var user = AnalystUser(); - var client = SetupApp() + var client = _testApp .SetUser(user) .AddContentDbTestData(context => { @@ -128,25 +117,24 @@ public async Task GetGlobalPermissions_AnalystUser_ReleaseApprover() .CreateClient(); var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); - } - - [Fact] - public async Task GetGlobalPermissions_AnalystUser_PublicationApprover() - { - var user = AnalystUser(); - - var expectedPermissions = new GlobalPermissionsViewModel( + + response.AssertOk(new GlobalPermissionsViewModel( CanAccessSystem: true, CanAccessAnalystPages: true, CanAccessAllImports: false, CanAccessPrereleasePages: true, CanManageAllTaxonomy: false, IsBauUser: false, - // Expect this to be true if the user is a Publication approver - IsApprover: true); + // Expect this to be true if the user is a Release approver + IsApprover: true)); + } - var client = SetupApp() + [Fact] + public async Task GetGlobalPermissions_AnalystUser_PublicationApprover() + { + var user = AnalystUser(); + + var client = _testApp .SetUser(user) .AddContentDbTestData(context => { @@ -159,38 +147,41 @@ public async Task GetGlobalPermissions_AnalystUser_PublicationApprover() .CreateClient(); var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); + + response.AssertOk(new GlobalPermissionsViewModel( + CanAccessSystem: true, + CanAccessAnalystPages: true, + CanAccessAllImports: false, + CanAccessPrereleasePages: true, + CanManageAllTaxonomy: false, + IsBauUser: false, + // Expect this to be true if the user is a Publication approver + IsApprover: true)); } [Fact] public async Task GetGlobalPermissions_PreReleaseUser() { - var user = PreReleaseUser(); - - var expectedPermissions = new GlobalPermissionsViewModel( + var client = _testApp + .SetUser(PreReleaseUser()) + .CreateClient(); + + var response = await client.GetAsync("/api/permissions/access"); + + response.AssertOk(new GlobalPermissionsViewModel( CanAccessSystem: true, CanAccessAnalystPages: false, CanAccessAllImports: false, CanAccessPrereleasePages: true, CanManageAllTaxonomy: false, IsBauUser: false, - IsApprover: false); - - var client = SetupApp().SetUser(user).CreateClient(); - - var response = await client.GetAsync("/api/permissions/access"); - response.AssertOk(expectedPermissions); + IsApprover: false)); } [Fact] public async Task GetGlobalPermissions_UnauthenticatedUser() { - var response = await SetupApp().CreateClient().GetAsync("/api/permissions/access"); + var response = await _testApp.CreateClient().GetAsync("/api/permissions/access"); response.AssertUnauthorized(); } - - private WebApplicationFactory SetupApp() - { - return _testApp.Initialise(); - } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs index 6f4dd76e593..f293e6e8495 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Controllers/Api/Statistics/TableBuilderControllerTests.cs @@ -280,10 +280,8 @@ private static (TableBuilderController controller, private WebApplicationFactory SetupApp( ITableBuilderService? tableBuilderService = null) { - return _testApp - .Initialise() - .ConfigureServices(services => services - .ReplaceService(tableBuilderService ?? Mock.Of(Strict))); + return _testApp.ConfigureServices(services => + services.ReplaceService(tableBuilderService ?? Mock.Of(Strict))); } } } 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 37eb30438e4..c3ae791968c 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -2729,7 +2729,7 @@ public async Task UserIsApproverOnOwningPublicationRelease_MethodologyVersionNot } [Fact] - public async Task UserIsReleaseApproverButOnAdoptingPublication_NotIncluded() + public async Task UserIsReleaseApproverOnAdoptingPublication_NotIncluded() { var release = _fixture.DefaultRelease().Generate(); @@ -2858,64 +2858,44 @@ public async Task DifferentUserIsApproverOnOwningPublicationRelease_NotIncluded( } [Fact] - public async Task MixOfMethodologies() + public async Task UserIsPublicationAndReleaseApprover_NoDuplication() { - var owningRelease = _fixture.DefaultRelease().Generate(); - var adoptingRelease = _fixture.DefaultRelease().Generate(); + var release = _fixture.DefaultRelease().Generate(); - var owningPublication = _fixture + var publication = _fixture .DefaultPublication() - .WithReleases(ListOf(owningRelease)) + .WithReleases(ListOf(release)) .Generate(); - var adoptingPublication = _fixture - .DefaultPublication() - .WithReleases(ListOf(adoptingRelease)) - .Generate(); - - var ownedMethodologies = _fixture - .DefaultMethodology() - .WithOwningPublication(owningPublication) - .WithMethodologyVersions(_ => _fixture - .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, - MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) - .GenerateList()) - .GenerateList(2); - - var adoptedMethodologies = _fixture + var methodology = _fixture .DefaultMethodology() - .WithAdoptingPublication(adoptingPublication) + .WithOwningPublication(publication) .WithMethodologyVersions(_ => _fixture .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, - MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) - .GenerateList()) - .Generate(2); - - var releaseRolesForUser = _fixture + .WithApprovalStatus(MethodologyApprovalStatus.HigherLevelReview) + .GenerateList(1)) + .Generate(); + + var publicationRoleForUser = _fixture + .DefaultUserPublicationRole() + .WithUser(User) + .WithPublication(publication) + .WithRole(PublicationRole.Approver) + .Generate(); + + var releaseRoleForUser = _fixture .DefaultUserReleaseRole() .WithUser(User) - .ForIndex(0, s => s - .SetRelease(owningRelease) - .SetRole(ReleaseRole.Approver)) - .ForIndex(1, s => s - .SetRelease(owningRelease) - .SetRole(ReleaseRole.Approver)) - .ForIndex(2, s => s - .SetRelease(adoptingRelease) - .SetRole(ReleaseRole.Approver)) - .ForIndex(3, s => s - .SetRelease(adoptingRelease) - .SetRole(ReleaseRole.Contributor)) - .GenerateList(); + .WithRelease(release) + .WithRole(ReleaseRole.Approver) + .Generate(); var contentDbContextId = Guid.NewGuid().ToString(); await using (var context = InMemoryApplicationDbContext(contentDbContextId)) { - await context.Methodologies.AddRangeAsync(ownedMethodologies); - await context.Methodologies.AddRangeAsync(adoptedMethodologies); - await context.UserReleaseRoles.AddRangeAsync(releaseRolesForUser); + await context.Methodologies.AddRangeAsync(methodology); + await context.UserPublicationRoles.AddRangeAsync(publicationRoleForUser); + await context.UserReleaseRoles.AddRangeAsync(releaseRoleForUser); await context.SaveChangesAsync(); } @@ -2926,11 +2906,9 @@ public async Task MixOfMethodologies() var result = await service.ListUsersMethodologyVersionsForApproval(); var methodologyVersionsForApproval = result.AssertRight(); - // Assert that we just get the 2 MethodologyVersions where the user is the Approver of the Owning - // Publication's Release and their statuses are Higher Review. - Assert.Equal(2, methodologyVersionsForApproval.Count); - Assert.Equal(ownedMethodologies[0].Versions[1].Id, methodologyVersionsForApproval[0].Id); - Assert.Equal(ownedMethodologies[1].Versions[1].Id, methodologyVersionsForApproval[1].Id); + // Assert that we just get back a single MethodologyVersion with no duplication, despite the user + // having links to this MethodologyVersion via Publication Roles and Release Roles. + Assert.Single(methodologyVersionsForApproval); } } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs index 9ee2fecc5bd..9ab386fc645 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/ReleaseServiceTests.cs @@ -1638,7 +1638,7 @@ public class ListUsersReleasesForApproval private readonly DataFixture _fixture = new(); [Fact] - public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnRelease() + public async Task UserHasApproverRoleOnRelease() { var contextId = Guid.NewGuid().ToString(); @@ -1717,7 +1717,7 @@ public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnRelease() } [Fact] - public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnPublications() + public async Task UserHasApproverRoleOnPublications() { var contextId = Guid.NewGuid().ToString(); @@ -1794,65 +1794,7 @@ await context.UserPublicationRoles.AddRangeAsync( } [Fact] - public async Task ListUsersReleasesForApproval_MixOfApproverReleaseAndPublicationRoles() - { - var contextId = Guid.NewGuid().ToString(); - - var publications = _fixture - .DefaultPublication() - .WithReleases(_ => _fixture - .DefaultRelease() - .WithApprovalStatuses(ListOf( - ReleaseApprovalStatus.Draft, - ReleaseApprovalStatus.HigherLevelReview, - ReleaseApprovalStatus.Approved)) - .GenerateList()) - .GenerateList(3); - - var approverPublicationRoleForUser = _fixture - .DefaultUserPublicationRole() - .WithUser(User) - .WithRole(PublicationRole.Approver) - .WithPublication(publications[0]) - .Generate(); - - var approverReleaseRolesForUser = _fixture - .DefaultUserReleaseRole() - .WithUser(User) - .WithRole(ReleaseRole.Approver) - .WithReleases(publications[1].Releases) - .GenerateList(); - - var release1WithApproverRoleForUser = publications[0].Releases[1]; - var release2WithApproverRoleForUser = publications[1].Releases[1]; - - await using (var context = InMemoryApplicationDbContext(contextId)) - { - await context.Publications.AddRangeAsync(publications); - await context.UserPublicationRoles.AddRangeAsync(approverPublicationRoleForUser); - await context.UserReleaseRoles.AddRangeAsync(approverReleaseRolesForUser); - await context.SaveChangesAsync(); - } - - await using (var context = InMemoryApplicationDbContext(contextId)) - { - var service = BuildReleaseService(context); - - var result = await service.ListUsersReleasesForApproval(); - - var viewModels = result.AssertRight(); - - // Assert that the only Releases returned for this user are the Releases where they have Approver - // role on the overarching Publication and the Releases are in Higher Review, and where the user - // has a direct Approver role on a Release in higher review. - Assert.Equal(2, viewModels.Count); - Assert.Equal(release1WithApproverRoleForUser.Id, viewModels[0].Id); - Assert.Equal(release2WithApproverRoleForUser.Id, viewModels[1].Id); - } - } - - [Fact] - public async Task ListUsersReleasesForApproval_UserHasApproverRoleOnPublicationsAndApproverRoleOnRelease() + public async Task UserIsPublicationAndReleaseApprover_NoDuplication() { var contextId = Guid.NewGuid().ToString(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs index 7e83985a407..4aa958d66c6 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/TestStartup.cs @@ -7,9 +7,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Tests.Extensions; using GovUk.Education.ExploreEducationStatistics.Common.Tests.Fixtures; using GovUk.Education.ExploreEducationStatistics.Content.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Content.Model.Tests.Utils; using GovUk.Education.ExploreEducationStatistics.Data.Model.Database; -using GovUk.Education.ExploreEducationStatistics.Data.Model.Tests.Utils; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.Configuration; @@ -47,61 +45,37 @@ public TestStartup( configureSpa: false) { } + + public override void ConfigureServices(IServiceCollection services) + { + base.ConfigureServices(services); + + services + .UseInMemoryDbContext() + .UseInMemoryDbContext() + .UseInMemoryDbContext() + .MockService() + .MockService() + .MockService() + .MockService() + .RegisterControllers(); + } } public static class TestStartupExtensions { - /// - /// Call this method when using this TestStartup to replace DbContexts with in-memory equivalents and services - /// which have external dependencies with mocks. - /// - /// - /// - public static WebApplicationFactory Initialise( - this WebApplicationFactory testApp) - { - return testApp - .ReplaceExternalDependencies() - .RegisterControllers() - .ResetDbContexts(); - } - - private static WebApplicationFactory ReplaceExternalDependencies( - this WebApplicationFactory testApp) - { - return testApp.WithWebHostBuilder(builder => builder - .ConfigureServices(services => - { - services - .ReplaceDbContext() - .ReplaceDbContext() - .ReplaceDbContext() - .ReplaceService() - .ReplaceService() - .ReplaceService() - .ReplaceService(); - })); - } - - private static WebApplicationFactory ResetDbContexts(this WebApplicationFactory testApp) - { - return testApp - .ResetContentDbContext() - .ResetStatisticsDbContext(); - } - /// /// This method adds an authenticated User in the form of a ClaimsPrincipal to the HttpContext. /// /// This User will subsequently be available for the Identity Framework as well as our own UserService, and any - /// other production code that looks up the User from the currect HttpContext. + /// other production code that looks up the User from the current HttpContext. /// /// /// /// public static WebApplicationFactory SetUser( this WebApplicationFactory testApp, - ClaimsPrincipal? user = null) + ClaimsPrincipal user) { return testApp.WithWebHostBuilder(builder => builder .ConfigureServices(services => @@ -110,10 +84,7 @@ public static WebApplicationFactory SetUser( .AddAuthentication(TestAuthHandler.AuthenticationScheme) .AddScheme(TestAuthHandler.AuthenticationScheme, _ => { }); - if (user != null) - { - services.AddScoped(_ => user); - } + services.AddScoped(_ => user); })); } } diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json index 25ab018279e..757d9b09aa7 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/appsettings.IntegrationTests.json @@ -1,8 +1,6 @@ { "OpenIdConnect": { - "ClientId": "ees-admin-client", - "ClientSecret": "eaa4cc70-e80b-4a7e-a20f-2856d97f470d", - "Authority": "http://ees.local:5030/auth/realms/ees-realm", - "RequireHttpsMetadata": false + "ClientId": "test-client-id", + "Authority": "https://example.com" } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs index 931dc7c8345..34b8551cc94 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Services/ReleaseService.cs @@ -456,8 +456,7 @@ public async Task>> ListUsersRelease var indirectReleasesWithApprovalRole = await _context .UserPublicationRoles .Where(role => role.UserId == userId && role.Role == PublicationRole.Approver) - .Select(role => role.Publication) - .SelectMany(publication => publication.Releases.Select(release => release.Id)) + .SelectMany(role => role.Publication.Releases.Select(release => release.Id)) .ToListAsync(); var releaseIdsForApproval = directReleasesWithApprovalRole diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index 0af5b55833f..3961b9aa422 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -142,7 +142,7 @@ public Startup( } // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + public virtual void ConfigureServices(IServiceCollection services) { services.AddHealthChecks(); diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs index a6aa039eafc..9a085c5bc79 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common.Tests/Extensions/ServiceCollectionExtensions.cs @@ -17,7 +17,7 @@ public static class ServiceCollectionExtensions /// /// This method replaces a DcContext that has been registered in Startup with an in-memory equivalent. /// - public static IServiceCollection ReplaceDbContext(this IServiceCollection services) + public static IServiceCollection UseInMemoryDbContext(this IServiceCollection services) where TDbContext : DbContext { // Remove the default DbContext descriptor that was provided by Startup.cs. @@ -54,19 +54,34 @@ public static IServiceCollection ReplaceService( ServiceLifetime.Scoped => services.AddScoped(_ => replacement), ServiceLifetime.Transient => services.AddTransient(_ => replacement), _ => throw new ArgumentOutOfRangeException( - $"Cannot register test service with ${nameof(ServiceLifetime)} {descriptor.Lifetime}") + $"Cannot register test service with {nameof(ServiceLifetime)}.{descriptor.Lifetime}") }; } /// - /// This method replaces a service that has been registered in Startup with a Strict Mock. The same - /// lifecycle that was registered in Startup will be used to register the new service. + /// This method replaces a service that has been registered in Startup with a Mock. The same + /// lifecycle that was registered in Startup will be used to register the Mock. /// public static IServiceCollection ReplaceService( - this IServiceCollection services) + this IServiceCollection services, + Mock replacement) + where TService : class + { + return services.ReplaceService(replacement.Object); + } + + /// + /// This method replaces a service that has been registered in Startup with a Mock. The same + /// lifecycle that was registered in Startup will be used to register the new service. + /// + /// The Mock will be setup with Strict behavior by default. + /// + public static IServiceCollection MockService( + this IServiceCollection services, + MockBehavior behavior = Strict) where TService : class { - return services.ReplaceService(Mock.Of(Strict)); + return services.ReplaceService(Mock.Of(behavior)); } /// diff --git a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs index 1fe948f5ad6..5b095bd6245 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Content.Model.Tests/Fixtures/ReleaseGeneratorExtensions.cs @@ -31,7 +31,7 @@ public static InstanceSetters SetDefaults(this InstanceSetters .SetDefault(p => p.Slug) .SetDefault(p => p.Title) .SetDefault(p => p.DataGuidance) - .Set(p => p.ReleaseName, (_, _, context) => $"{1000 + context.Index}"); + .Set(p => p.ReleaseName, (_, _, context) => $"{2000 + context.Index}"); public static InstanceSetters SetApprovalStatus( this InstanceSetters setters, From 084e8f17e234e191cf396f1db4f3f46ac108e109 Mon Sep 17 00:00:00 2001 From: Amy Benson Date: Wed, 20 Sep 2023 13:23:49 +0100 Subject: [PATCH 16/22] EES-4448 responding to front end PR comments --- .../Methodologies/MethodologyServiceTests.cs | 81 ++----------------- .../admin-dashboard/AdminDashboardPage.tsx | 21 +++-- .../components/ApprovalsTable.tsx | 18 ++--- .../components/DraftReleaseRow.tsx | 2 +- .../__tests__/ApprovalsTable.test.tsx | 16 ---- .../src/services/releaseService.ts | 2 +- 6 files changed, 26 insertions(+), 114 deletions(-) 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 c3ae791968c..3ac454ec3d2 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin.Tests/Services/Methodologies/MethodologyServiceTests.cs @@ -601,7 +601,7 @@ public async Task GetAdoptableMethodologies() { MethodologyVersion = methodologyVersion, InternalReleaseNote = "Test approval", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, }; var adoptingPublication = new Publication(); @@ -621,7 +621,7 @@ public async Task GetAdoptableMethodologies() var methodologyVersionRepository = new Mock(Strict); methodologyRepository.Setup(mock => - mock.GetUnrelatedToPublication(adoptingPublication.Id)) + mock.GetPublishedMethodologiesUnrelatedToPublication(adoptingPublication.Id)) .ReturnsAsync(ListOf(methodology)); methodologyVersionRepository.Setup(mock => mock.GetLatestPublishedVersion(methodology.Id)) @@ -670,8 +670,8 @@ public async Task GetAdoptableMethodologies_NoUnpublishedMethodologies() new() { Published = null, - PublishingStrategy = Immediately, - Status = Draft, + PublishingStrategy = MethodologyPublishingStrategy.Immediately, + Status = MethodologyApprovalStatus.Draft, AlternativeTitle = "Alternative title" }, }, @@ -818,7 +818,7 @@ public async Task GetMethodology() { MethodologyVersion = methodologyVersion, InternalReleaseNote = "Test approval", - ApprovalStatus = Approved, + ApprovalStatus = MethodologyApprovalStatus.Approved, }; var contentDbContextId = Guid.NewGuid().ToString(); @@ -2556,77 +2556,6 @@ public async Task DifferentUserIsApproverOnOwningPublication_NotIncluded() Assert.Empty(result.AssertRight()); } } - - [Fact] - public async Task MixOfMethodologies() - { - var publications = _fixture - .DefaultPublication() - .GenerateList(2); - - var owningPublication = publications[0]; - var adoptingPublication = publications[1]; - - var ownedMethodologies = _fixture - .DefaultMethodology() - .WithOwningPublication(owningPublication) - .WithMethodologyVersions(_ => _fixture - .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, - MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) - .GenerateList()) - .GenerateList(2); - - var adoptedMethodologies = _fixture - .DefaultMethodology() - .WithAdoptingPublication(adoptingPublication) - .WithMethodologyVersions(_ => _fixture - .DefaultMethodologyVersion() - .WithApprovalStatuses(ListOf(MethodologyApprovalStatus.Approved, - MethodologyApprovalStatus.HigherLevelReview, MethodologyApprovalStatus.Draft)) - .GenerateList()) - .Generate(2); - - var publicationRolesForUser = _fixture - .DefaultUserPublicationRole() - .WithUser(User) - .ForIndex(0, s => s - .SetPublication(owningPublication) - .SetRole(PublicationRole.Approver)) - .ForIndex(1, s => s - .SetPublication(owningPublication) - .SetRole(PublicationRole.Owner)) - .ForIndex(2, s => s - .SetPublication(adoptingPublication) - .SetRole(PublicationRole.Approver)) - .ForIndex(3, s => s - .SetPublication(adoptingPublication) - .SetRole(PublicationRole.Owner)) - .GenerateList(); - - var contentDbContextId = Guid.NewGuid().ToString(); - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) - { - await context.Methodologies.AddRangeAsync(ownedMethodologies); - await context.Methodologies.AddRangeAsync(adoptedMethodologies); - await context.UserPublicationRoles.AddRangeAsync(publicationRolesForUser); - await context.SaveChangesAsync(); - } - - await using (var context = InMemoryApplicationDbContext(contentDbContextId)) - { - var service = SetupMethodologyService(context); - - var result = await service.ListUsersMethodologyVersionsForApproval(); - var methodologyVersionsForApproval = result.AssertRight(); - - // Assert that we just get the 2 MethodologyVersions where the user is the Approver of the Owning - // Publication and their statuses are Higher Review. - Assert.Equal(2, methodologyVersionsForApproval.Count); - Assert.Equal(ownedMethodologies[0].Versions[1].Id, methodologyVersionsForApproval[0].Id); - Assert.Equal(ownedMethodologies[1].Versions[1].Id, methodologyVersionsForApproval[1].Id); - } - } } public class ListUsersMethodologyVersionsForApprovalForReleaseRoles diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx index ca033799ebf..9d0d34c62ee 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/AdminDashboardPage.tsx @@ -106,14 +106,7 @@ const AdminDashboardPage = () => { - { - if (section.id === 'draft-releases') { - reloadDraftReleases(); - } - }} - > + @@ -121,7 +114,9 @@ const AdminDashboardPage = () => { lazy id="draft-releases" data-testid="publication-draft-releases" - title={`Draft releases (${draftReleases.length})`} + title={`Draft releases ${ + !isLoadingDraftReleases ? `(${draftReleases.length})` : '' + }`} > { lazy id="approvals" data-testid="publication-approvals" - title={`Your approvals (${totalApprovals})`} + title={`Your approvals ${ + !isLoadingApprovals ? `(${totalApprovals})` : '' + }`} > { = useMemo(() => { return releaseApprovals.reduce>( (acc, release) => { @@ -34,7 +34,6 @@ export default function ApprovalsTable({ acc[release.publicationTitle].releases.push(release); } else { acc[release.publicationTitle] = { - ...acc[release.publicationTitle], releases: [release], }; } @@ -45,7 +44,7 @@ export default function ApprovalsTable({ }, [releaseApprovals]); const methodologiesByPublication: Dictionary<{ - methodologies: MethodologyVersion[]; + methodologies?: MethodologyVersion[]; }> = useMemo(() => { return methodologyApprovals.reduce< Dictionary<{ methodologies: MethodologyVersion[] }> @@ -56,7 +55,6 @@ export default function ApprovalsTable({ ); } else { acc[methodology.owningPublication.title] = { - ...acc[methodology.owningPublication.title], methodologies: [methodology], }; } @@ -69,7 +67,9 @@ export default function ApprovalsTable({ methodologiesByPublication, ); - if (!Object.keys(allApprovalsByPublication).length) { + const publications = Object.keys(allApprovalsByPublication); + + if (!publications.length) { return (

There are no releases or methodologies awaiting your approval.

); @@ -85,7 +85,7 @@ export default function ApprovalsTable({
Page type Actions
+ {publication}
(releaseContentRoute.path, { - publicationId: release.publicationId, + publicationId: release.publication.id, releaseId: release.id, })} > diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx index 090f31d504e..2b6a69d3989 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleaseRow.tsx @@ -1,7 +1,10 @@ import Link from '@admin/components/Link'; import DraftReleaseRowIssues from '@admin/pages/admin-dashboard/components/DraftReleaseRowIssues'; import { getReleaseApprovalStatusLabel } from '@admin/pages/release/utils/releaseSummaryUtil'; -import { Release } from '@admin/services/releaseService'; +import { + ReleaseSummaryWithPermissions, + DashboardReleaseSummary, +} from '@admin/services/releaseService'; import { ReleaseRouteParams, releaseSummaryRoute, @@ -14,7 +17,7 @@ import { generatePath } from 'react-router'; interface Props { isBauUser: boolean; - release: Release; + release: DashboardReleaseSummary & ReleaseSummaryWithPermissions; onDelete: () => void; } @@ -34,7 +37,7 @@ const DraftReleaseRow = ({ isBauUser, release, onDelete }: Props) => { (releaseSummaryRoute.path, { - publicationId: release.publicationId, + publicationId: release.publication.id, releaseId: release.id, })} > @@ -42,11 +45,11 @@ const DraftReleaseRow = ({ isBauUser, release, onDelete }: Props) => { {release.title} - {release.amendment && release.previousVersionId && ( + {release.previousVersionId && ( (releaseSummaryRoute.path, { - publicationId: release.publicationId, + publicationId: release.publication.id, releaseId: release.previousVersionId, })} > diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTab.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTab.tsx index d3a6e1ac37b..27a8366ff0d 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTab.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTab.tsx @@ -1,12 +1,12 @@ import DraftReleasesTable from '@admin/pages/admin-dashboard/components/DraftReleasesTable'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import LoadingSpinner from '@common/components/LoadingSpinner'; import React from 'react'; interface Props { isBauUser: boolean; isLoading: boolean; - releases: Release[]; + releases: DashboardReleaseSummary[]; onChangeRelease: () => void; } diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTable.tsx index 065a9aa0a6a..d478b8ab042 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/DraftReleasesTable.tsx @@ -7,7 +7,7 @@ import { } from '@admin/pages/publication/components/PublicationGuidance'; import releaseService, { DeleteReleasePlan, - Release, + DashboardReleaseSummary, } from '@admin/services/releaseService'; import ButtonText from '@common/components/ButtonText'; import InfoIcon from '@common/components/InfoIcon'; @@ -19,7 +19,7 @@ import React, { useMemo, useState } from 'react'; interface PublicationRowProps { isBauUser: boolean; publication: string; - releases: Release[]; + releases: DashboardReleaseSummary[]; onDelete: (releaseId: string) => void; } const PublicationRow = ({ @@ -49,7 +49,7 @@ const PublicationRow = ({ interface DraftReleasesTableProps { isBauUser: boolean; - releases: Release[]; + releases: DashboardReleaseSummary[]; onChangeRelease: () => void; } @@ -67,17 +67,21 @@ const DraftReleasesTable = ({ const [showDraftStatusGuidance, toggleDraftStatusGuidance] = useToggle(false); const [showIssuesGuidance, toggleIssuesGuidance] = useToggle(false); - const releasesByPublication: Dictionary = useMemo(() => { - return releases.reduce>((acc, release) => { - if (acc[release.publicationTitle]) { - acc[release.publicationTitle].push(release); - } else { - acc[release.publicationTitle] = [release]; - } + const releasesByPublication: Dictionary = + useMemo(() => { + return releases.reduce>( + (acc, release) => { + if (acc[release.publication.title]) { + acc[release.publication.title].push(release); + } else { + acc[release.publication.title] = [release]; + } - return acc; - }, {}); - }, [releases]); + return acc; + }, + {}, + ); + }, [releases]); return ( <> diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTab.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTab.tsx index 7c9d8bfcdf6..b6e0dc7aea9 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTab.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTab.tsx @@ -1,11 +1,11 @@ import ScheduledReleasesTable from '@admin/pages/admin-dashboard/components/ScheduledReleasesTable'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import LoadingSpinner from '@common/components/LoadingSpinner'; import React from 'react'; interface Props { isLoading: boolean; - releases: Release[]; + releases: DashboardReleaseSummary[]; } const ScheduledReleasesTab = ({ isLoading, releases }: Props) => { diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTable.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTable.tsx index 4c7e2f3de7e..283a2aff2b1 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTable.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/ScheduledReleasesTable.tsx @@ -3,7 +3,7 @@ import { ScheduledStagesGuidanceModal, ScheduledStatusGuidanceModal, } from '@admin/pages/publication/components/PublicationGuidance'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import ButtonText from '@common/components/ButtonText'; import InfoIcon from '@common/components/InfoIcon'; import useToggle from '@common/hooks/useToggle'; @@ -13,7 +13,7 @@ import React, { useMemo } from 'react'; interface PublicationRowProps { publication: string; - releases: Release[]; + releases: DashboardReleaseSummary[]; } const PublicationRow = ({ publication, releases }: PublicationRowProps) => { @@ -27,7 +27,7 @@ const PublicationRow = ({ publication, releases }: PublicationRowProps) => { {releases.map(release => ( ))} @@ -36,7 +36,7 @@ const PublicationRow = ({ publication, releases }: PublicationRowProps) => { }; interface ScheduledReleasesTableProps { - releases: Release[]; + releases: DashboardReleaseSummary[]; } const ScheduledReleasesTable = ({ releases }: ScheduledReleasesTableProps) => { @@ -45,17 +45,21 @@ const ScheduledReleasesTable = ({ releases }: ScheduledReleasesTableProps) => { const [showScheduledStagesGuidance, toggleScheduledStagesGuidance] = useToggle(false); - const releasesByPublication: Dictionary = useMemo(() => { - return releases.reduce>((acc, release) => { - if (acc[release.publicationTitle]) { - acc[release.publicationTitle].push(release); - } else { - acc[release.publicationTitle] = [release]; - } + const releasesByPublication: Dictionary = + useMemo(() => { + return releases.reduce>( + (acc, release) => { + if (acc[release.publication.title]) { + acc[release.publication.title].push(release); + } else { + acc[release.publication.title] = [release]; + } - return acc; - }, {}); - }, [releases]); + return acc; + }, + {}, + ); + }, [releases]); return ( <> {releases.length === 0 ? ( diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx index 984e05fa923..9c659fe2aad 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ApprovalsTable.test.tsx @@ -1,6 +1,6 @@ import ApprovalsTable from '@admin/pages/admin-dashboard/components/ApprovalsTable'; import { MethodologyVersion } from '@admin/services/methodologyService'; -import { Release } from '@admin/services/releaseService'; +import { DashboardReleaseSummary } from '@admin/services/releaseService'; import { waitFor, within } from '@testing-library/dom'; import { render, screen } from '@testing-library/react'; import React from 'react'; @@ -9,30 +9,32 @@ import { MemoryRouter } from 'react-router'; describe('ApprovalsTable', () => { const testMethodologies: MethodologyVersion[] = [ { - id: 'c8c911e3-39c1-452b-801f-25bb79d1deb7', - methodologyId: 'b8bd000c-f9d8-4319-a2b3-6bc18675e5ac', + id: 'methodology-version-1', + methodologyId: 'methodology-1', owningPublication: { - id: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', - title: 'Permanent and fixed-period exclusions in England', + id: 'publication-1', + title: 'Publication 1 title', }, otherPublications: [], published: '2018-08-25T00:00:00', publishingStrategy: 'Immediately', - slug: 'permanent-and-fixed-period-exclusions-in-england', + slug: 'methodology-1-slug', status: 'Approved', - title: 'Pupil exclusion statistics: methodology', + title: 'Methodology 1', amendment: false, }, ]; - const testReleases: Release[] = [ + const testReleases: DashboardReleaseSummary[] = [ { id: 'test-id', title: 'Academic year 2016/17', slug: '2024-25', - publicationId: 'bf2b4284-6b84-46b0-aaaa-a2e0a23be2a9', - publicationTitle: 'Permanent and fixed-period exclusions in England', - publicationSlug: 'pub-slug', + publication: { + id: 'publication-1', + title: 'Publication 1 title', + slug: 'publication-1-slug', + }, year: 2024, yearTitle: '2024/25', nextReleaseDate: { @@ -41,16 +43,13 @@ describe('ApprovalsTable', () => { day: '', }, live: false, + latestRelease: false, timePeriodCoverage: { value: 'AY', label: 'Academic year', }, - preReleaseAccessList: '

Test public access list

', - preReleaseUsersOrInvitesAdded: false, - latestRelease: false, type: 'NationalStatistics', approvalStatus: 'Approved', - notifySubscribers: false, amendment: false, permissions: { canAddPrereleaseUsers: true, @@ -59,17 +58,16 @@ describe('ApprovalsTable', () => { canDeleteRelease: false, canMakeAmendmentOfRelease: false, }, - updatePublishedDate: false, }, { id: '86d868cf-ff4b-4325-ef26-08d93c9b5089', title: 'Academic year 2024/25', slug: '2024-25', - publicationId: '959bd40c-4685-46ff-396d-08d93c9b5159', - publicationTitle: - 'UI tests - Publication and Release UI Permissions Publication Owner', - publicationSlug: - 'ui-tests-publication-and-release-ui-permissions-publication-owner', + publication: { + id: 'publication-2', + title: 'Publication 2 title', + slug: 'publication-2-slug', + }, year: 2024, yearTitle: '2024/25', nextReleaseDate: { @@ -79,16 +77,14 @@ describe('ApprovalsTable', () => { }, publishScheduled: '2048-11-16', live: false, + latestRelease: false, timePeriodCoverage: { value: 'AY', label: 'Academic year', }, - preReleaseAccessList: '

Test public access list

', - preReleaseUsersOrInvitesAdded: false, - latestRelease: false, type: 'NationalStatistics', approvalStatus: 'Approved', - notifySubscribers: false, + previousVersionId: 'old-release-2-id', amendment: false, permissions: { canAddPrereleaseUsers: true, @@ -97,7 +93,6 @@ describe('ApprovalsTable', () => { canDeleteRelease: false, canMakeAmendmentOfRelease: false, }, - updatePublishedDate: false, }, ]; @@ -119,7 +114,7 @@ describe('ApprovalsTable', () => { expect(rows).toHaveLength(6); expect(within(rows[1]).getByRole('columnheader')).toHaveTextContent( - 'Permanent and fixed-period exclusions in England', + 'Publication 1 title', ); const row3cells = within(rows[2]).getAllByRole('cell'); @@ -130,16 +125,14 @@ describe('ApprovalsTable', () => { ).toBeInTheDocument(); const row4cells = within(rows[3]).getAllByRole('cell'); - expect(row4cells[0]).toHaveTextContent( - 'Pupil exclusion statistics: methodology', - ); + expect(row4cells[0]).toHaveTextContent('Methodology 1'); expect(row4cells[1]).toHaveTextContent('Methodology'); expect( within(row4cells[2]).getByRole('link', { name: /Review this page/ }), ).toBeInTheDocument(); expect(within(rows[4]).getByRole('columnheader')).toHaveTextContent( - 'UI tests - Publication and Release UI Permissions Publication Owner', + 'Publication 2 title', ); const row6cells = within(rows[5]).getAllByRole('cell'); diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/DraftReleasesTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/DraftReleasesTable.test.tsx index 99b03c09657..337a018d541 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/DraftReleasesTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/DraftReleasesTable.test.tsx @@ -1,6 +1,6 @@ import DraftReleasesTable from '@admin/pages/admin-dashboard/components/DraftReleasesTable'; import _releaseService, { - ReleaseWithPermissions, + DashboardReleaseSummary, } from '@admin/services/releaseService'; import { waitFor, within } from '@testing-library/dom'; import { render, screen } from '@testing-library/react'; @@ -13,71 +13,136 @@ jest.mock('@admin/services/releaseService'); const releaseService = _releaseService as jest.Mocked; describe('DraftReleasesTable', () => { - const testReleases: ReleaseWithPermissions[] = [ + const testReleases: DashboardReleaseSummary[] = [ { id: 'release-1', latestRelease: true, slug: 'release-1-slug', title: 'Release 1', - publicationId: 'publication-1', - publicationTitle: 'Publication 1', + year: 2020, + yearTitle: '2020/21', + publication: { + id: 'publication-1', + title: 'Publication 1', + slug: 'publication-1-slug', + }, + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + approvalStatus: 'Draft', + amendment: false, permissions: { + canViewRelease: true, canUpdateRelease: true, canDeleteRelease: false, canMakeAmendmentOfRelease: false, canAddPrereleaseUsers: false, }, - approvalStatus: 'Draft', - } as ReleaseWithPermissions, + }, { id: 'release-2', latestRelease: true, slug: 'release-2-slug', title: 'Release 2', - publicationId: 'publication-2', - publicationTitle: 'Publication 2', + year: 2021, + yearTitle: '2021/22', + publication: { + id: 'publication-2', + title: 'Publication 2', + slug: 'publication-2-slug', + }, + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + approvalStatus: 'Draft', + amendment: true, + previousVersionId: 'previous-version-id', permissions: { + canViewRelease: true, canDeleteRelease: true, canUpdateRelease: true, canMakeAmendmentOfRelease: false, canAddPrereleaseUsers: false, }, - approvalStatus: 'Draft', - amendment: true, - previousVersionId: 'previous-version-id', - } as ReleaseWithPermissions, + }, { id: 'release-3', latestRelease: false, slug: 'release-3-slug', title: 'Release 3', - publicationId: 'publication-1', - publicationTitle: 'Publication 1', + year: 2022, + yearTitle: '2022/23', + publication: { + id: 'publication-1', + title: 'Publication 1', + slug: 'publication-1-slug', + }, + nextReleaseDate: { + year: '2200', + month: '1', + day: '', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + approvalStatus: 'HigherLevelReview', + amendment: false, permissions: { + canViewRelease: true, canUpdateRelease: true, canDeleteRelease: true, canMakeAmendmentOfRelease: false, canAddPrereleaseUsers: false, }, - approvalStatus: 'HigherLevelReview', - } as ReleaseWithPermissions, + }, { id: 'release-4', latestRelease: true, slug: 'release-4-slug', title: 'Release 4', - publicationId: 'publication-3', - publicationTitle: 'Publication 3', + year: 2023, + yearTitle: '2023/24', + publication: { + id: 'publication-3', + title: 'Publication 3', + slug: 'publication-3-slug', + }, + live: false, + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + approvalStatus: 'HigherLevelReview', + amendment: true, + previousVersionId: 'previous-version-id', permissions: { + canViewRelease: true, canDeleteRelease: true, canUpdateRelease: true, canMakeAmendmentOfRelease: false, canAddPrereleaseUsers: false, }, - approvalStatus: 'HigherLevelReview', - amendment: true, - previousVersionId: 'previous-version-id', - } as ReleaseWithPermissions, + }, ]; beforeEach(() => { diff --git a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ScheduledReleasesTable.test.tsx b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ScheduledReleasesTable.test.tsx index 03c124420d9..3e03956b95a 100644 --- a/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ScheduledReleasesTable.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/admin-dashboard/components/__tests__/ScheduledReleasesTable.test.tsx @@ -1,6 +1,6 @@ import ScheduledReleasesTable from '@admin/pages/admin-dashboard/components/ScheduledReleasesTable'; import _releaseService, { - ReleaseWithPermissions, + DashboardReleaseSummary, } from '@admin/services/releaseService'; import { waitFor, within } from '@testing-library/dom'; import { render, screen } from '@testing-library/react'; @@ -11,71 +11,123 @@ jest.mock('@admin/services/releaseService'); const releaseService = _releaseService as jest.Mocked; describe('ScheduledReleasesTable', () => { - const testReleases: ReleaseWithPermissions[] = [ + const testReleases: DashboardReleaseSummary[] = [ { id: 'release-1', latestRelease: true, publishScheduled: '2021-06-30T00:00:00', slug: 'release-1-slug', title: 'Release 1', - publicationId: 'publication-1', - publicationTitle: 'Publication 1', + amendment: false, + live: false, + year: 2021, + yearTitle: '2021', + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + publication: { + id: 'publication-1', + slug: 'publication-1-slug', + title: 'Publication 1', + }, permissions: { + canViewRelease: true, canUpdateRelease: true, canAddPrereleaseUsers: false, canDeleteRelease: false, canMakeAmendmentOfRelease: false, }, approvalStatus: 'Approved', - } as ReleaseWithPermissions, + }, { id: 'release-2', latestRelease: true, publishScheduled: '2021-05-30T00:00:00', slug: 'release-2-slug', title: 'Release 2', - publicationId: 'publication-2', - publicationTitle: 'Publication 2', + amendment: false, + live: false, + year: 2021, + yearTitle: '2021', + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + publication: { + id: 'publication-2', + slug: 'publication-2-slug', + title: 'Publication 2', + }, permissions: { + canViewRelease: true, canUpdateRelease: true, canAddPrereleaseUsers: false, canDeleteRelease: false, canMakeAmendmentOfRelease: false, }, approvalStatus: 'Approved', - } as ReleaseWithPermissions, + }, { id: 'release-3', latestRelease: false, publishScheduled: '2021-01-01T00:00:00', slug: 'release-3-slug', title: 'Release 3', - publicationId: 'publication-1', - publicationTitle: 'Publication 1', + amendment: false, + live: false, + year: 2021, + yearTitle: '2021', + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + publication: { + id: 'publication-1', + slug: 'publication-1-slug', + title: 'Publication 1', + }, permissions: { + canViewRelease: true, canUpdateRelease: true, canDeleteRelease: true, canAddPrereleaseUsers: false, canMakeAmendmentOfRelease: false, }, approvalStatus: 'Approved', - } as ReleaseWithPermissions, + }, { id: 'release-4', latestRelease: true, publishScheduled: '2021-05-30T00:00:00', slug: 'release-4-slug', title: 'Release 4', - publicationId: 'publication-3', - publicationTitle: 'Publication 3', + amendment: false, + live: false, + year: 2021, + yearTitle: '2021', + timePeriodCoverage: { + value: 'AY', + label: 'Academic year', + }, + type: 'NationalStatistics', + publication: { + id: 'publication-3', + slug: 'publication-3-slug', + title: 'Publication 3', + }, permissions: { + canViewRelease: true, canUpdateRelease: true, canAddPrereleaseUsers: false, canDeleteRelease: false, canMakeAmendmentOfRelease: false, }, approvalStatus: 'Approved', - } as ReleaseWithPermissions, + }, ]; beforeEach(() => { diff --git a/src/explore-education-statistics-admin/src/pages/publication/__data__/testReleases.ts b/src/explore-education-statistics-admin/src/pages/publication/__data__/testReleases.ts index 048043d6eed..dd9cd232d17 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/__data__/testReleases.ts +++ b/src/explore-education-statistics-admin/src/pages/publication/__data__/testReleases.ts @@ -17,6 +17,7 @@ export const testReleaseSummaries: ReleaseSummary[] = [ yearTitle: '2023/24', live: false, amendment: false, + latestRelease: false, }, { id: 'release-2', @@ -32,6 +33,7 @@ export const testReleaseSummaries: ReleaseSummary[] = [ yearTitle: '2022/23', live: false, amendment: false, + latestRelease: false, }, { id: 'release-3', @@ -48,6 +50,7 @@ export const testReleaseSummaries: ReleaseSummary[] = [ yearTitle: '2021/22', live: true, amendment: false, + latestRelease: false, }, ]; diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationDraftReleases.test.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationDraftReleases.test.tsx index 0a1c65f32f9..70cd2428c9f 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationDraftReleases.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationDraftReleases.test.tsx @@ -40,6 +40,7 @@ describe('PublicationDraftReleases', () => { type: 'AdHocStatistics', year: 2022, yearTitle: '2022/23', + latestRelease: false, }; const testRelease2: ReleaseSummaryWithPermissions = { diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationPublishedReleases.test.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationPublishedReleases.test.tsx index 54222e3068c..bd9fd743ca4 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationPublishedReleases.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationPublishedReleases.test.tsx @@ -46,6 +46,7 @@ describe('PublicationPublishedReleases', () => { type: 'AdHocStatistics', year: 2021, yearTitle: '2021/22', + latestRelease: false, }; const testRelease2: ReleaseSummaryWithPermissions = { diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationReleaseAccess.test.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationReleaseAccess.test.tsx index c8438096b82..a08a99e5e2a 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationReleaseAccess.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationReleaseAccess.test.tsx @@ -37,6 +37,7 @@ describe('PublicationReleaseAccess', () => { yearTitle: '2000/01', live: false, amendment: false, + latestRelease: false, }; const testContributors: UserReleaseRole[] = [ diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationScheduledReleases.test.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationScheduledReleases.test.tsx index 76b109ebce9..6ffe9d1d4d4 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationScheduledReleases.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationScheduledReleases.test.tsx @@ -40,6 +40,7 @@ describe('PublicationScheduledReleases', () => { value: 'AY', }, type: 'AdHocStatistics', + latestRelease: false, }; const testRelease2: ReleaseSummaryWithPermissions = { diff --git a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationUnpublishedReleases.test.tsx b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationUnpublishedReleases.test.tsx index 8205bb89420..3c75c1d751d 100644 --- a/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationUnpublishedReleases.test.tsx +++ b/src/explore-education-statistics-admin/src/pages/publication/components/__tests__/PublicationUnpublishedReleases.test.tsx @@ -46,6 +46,7 @@ describe('PublicationUnpublishedReleases', () => { type: 'NationalStatistics', year: 2022, yearTitle: '2022/23', + latestRelease: false, }; const testRelease2: ReleaseSummaryWithPermissions = { @@ -63,6 +64,7 @@ describe('PublicationUnpublishedReleases', () => { type: 'NationalStatistics', year: 2024, yearTitle: '2024/25', + latestRelease: false, }; const testRelease3: ReleaseSummaryWithPermissions = { @@ -80,6 +82,7 @@ describe('PublicationUnpublishedReleases', () => { type: 'NationalStatistics', year: 2023, yearTitle: '2023/24', + latestRelease: false, }; const testReleasesPage1: PaginatedList = { diff --git a/src/explore-education-statistics-admin/src/services/releaseService.ts b/src/explore-education-statistics-admin/src/services/releaseService.ts index 870ce56a07d..d52b309e55f 100644 --- a/src/explore-education-statistics-admin/src/services/releaseService.ts +++ b/src/explore-education-statistics-admin/src/services/releaseService.ts @@ -1,6 +1,9 @@ import { IdTitlePair, ValueLabelPair } from '@admin/services/types/common'; import client from '@admin/services/utils/service'; -import { ReleaseApprovalStatus } from '@common/services/publicationService'; +import { + PublicationSummary, + ReleaseApprovalStatus, +} from '@common/services/publicationService'; import { ReleaseType } from '@common/services/types/releaseType'; import { PartialDate } from '@common/utils/date/partialDate'; @@ -39,10 +42,6 @@ export interface Release { permissions?: ReleasePermissions; } -export interface ReleaseWithPermissions extends Release { - permissions: ReleasePermissions; -} - export interface ReleaseSummary { id: string; title: string; @@ -57,8 +56,14 @@ export interface ReleaseSummary { nextReleaseDate?: PartialDate; type: ReleaseType; amendment: boolean; + latestRelease: boolean; previousVersionId?: string; permissions?: ReleasePermissions; + publication?: PublicationSummary; +} + +export interface DashboardReleaseSummary extends ReleaseSummaryWithPermissions { + publication: PublicationSummary; } export interface ReleaseSummaryWithPermissions extends ReleaseSummary { @@ -228,15 +233,15 @@ const releaseService = { return client.delete(`/release/${releaseId}`); }, - listDraftReleases(): Promise { + listDraftReleases(): Promise { return client.get('/releases/draft'); }, - listScheduledReleases(): Promise { + listScheduledReleases(): Promise { return client.get('/releases/scheduled'); }, - listReleasesForApproval(): Promise { + listReleasesForApproval(): Promise { return client.get('/releases/approvals'); }, From 820574e0bebfd05ef886b40c4e64374081267b3a Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 22 Sep 2023 09:46:13 +0100 Subject: [PATCH 21/22] EES-4448 - reinstated startup completion logging for Admin that was omitted from Startup.cs in error --- .../Startup.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs index bee289733b1..837ae8d9543 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Admin/Startup.cs @@ -54,6 +54,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Authorization; @@ -763,6 +764,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } }); } + + app.ServerFeatures.Get() + ?.Addresses + .ForEach(address => Console.WriteLine($"Server listening on address: {address}")); } private void UpdateDatabase(IApplicationBuilder app, IWebHostEnvironment env) From 03cfaa79068edf060639333918826b448d762f91 Mon Sep 17 00:00:00 2001 From: Duncan Watson Date: Fri, 22 Sep 2023 11:21:48 +0100 Subject: [PATCH 22/22] EES-4448 - fixed connection string lookup issue with TableStorageService instances. Updated admin-common.robot pre-release access steps to be more resilient --- .../Services/CoreTableStorageService.cs | 5 +++-- .../Services/PublisherTableStorageService.cs | 5 +++-- .../Services/TableStorageService.cs | 5 ++++- tests/robot-tests/tests/libs/admin-common.robot | 7 ++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs index a7c76b028d6..771bd5afea1 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/CoreTableStorageService.cs @@ -1,11 +1,12 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; +using Microsoft.Extensions.Configuration; namespace GovUk.Education.ExploreEducationStatistics.Common.Services; public class CoreTableStorageService : TableStorageService, ICoreTableStorageService { - public CoreTableStorageService() - : base("CoreStorage") + public CoreTableStorageService(IConfiguration configuration) + : base(configuration, "PublisherStorage") { } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs index dba307d422d..9f77d8be1e4 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/PublisherTableStorageService.cs @@ -1,11 +1,12 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; +using Microsoft.Extensions.Configuration; namespace GovUk.Education.ExploreEducationStatistics.Common.Services; public class PublisherTableStorageService : TableStorageService, IPublisherTableStorageService { - public PublisherTableStorageService() - : base("PublisherStorage") + public PublisherTableStorageService(IConfiguration configuration) + : base(configuration, "PublisherStorage") { } } \ No newline at end of file diff --git a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs index cc1aa6dc247..d8ec0293d25 100644 --- a/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs +++ b/src/GovUk.Education.ExploreEducationStatistics.Common/Services/TableStorageService.cs @@ -4,6 +4,7 @@ using GovUk.Education.ExploreEducationStatistics.Common.Services.Interfaces; using GovUk.Education.ExploreEducationStatistics.Common.Utils; using Microsoft.Azure.Cosmos.Table; +using Microsoft.Extensions.Configuration; namespace GovUk.Education.ExploreEducationStatistics.Common.Services { @@ -13,8 +14,10 @@ public abstract class TableStorageService : ITableStorageService private readonly StorageInstanceCreationUtil _storageInstanceCreationUtil = new(); protected TableStorageService( - string connectionString) + IConfiguration configuration, + string connectionStringKey) { + var connectionString = configuration.GetValue(connectionStringKey); var account = CloudStorageAccount.Parse(connectionString); _client = account.CreateCloudTableClient(); } diff --git a/tests/robot-tests/tests/libs/admin-common.robot b/tests/robot-tests/tests/libs/admin-common.robot index 540bbe87e18..e4209cf10a2 100644 --- a/tests/robot-tests/tests/libs/admin-common.robot +++ b/tests/robot-tests/tests/libs/admin-common.robot @@ -415,8 +415,8 @@ user creates public prerelease access list user presses keys BACKSPACE user enters text into element id:publicPreReleaseAccessForm-preReleaseAccessList ${content} user clicks button Save access list - user waits until element contains id:publicPreReleaseAccessForm-preReleaseAccessList ${content} - ... %{WAIT_SMALL} + user waits until element contains css:[data-testid="publicPreReleaseAccessListPreview"] + ... ${content} user updates public prerelease access list [Arguments] ${content} @@ -426,7 +426,8 @@ user updates public prerelease access list user presses keys BACKSPACE user enters text into element id:publicPreReleaseAccessForm-preReleaseAccessList ${content} user clicks button Save access list - user waits until element contains id:publicPreReleaseAccessForm-preReleaseAccessList ${content} + user waits until element contains css:[data-testid="publicPreReleaseAccessListPreview"] + ... ${content} user clicks footnote subject radio [Arguments] ${subject_label} ${radio_label}