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 (
+
+
+
+ Publication / Page |
+ Page type |
+ Actions |
+
+ {orderBy(Object.keys(allApprovalsByPublication)).map(publication => (
+
+ ))}
+
+
+ );
+}
+
+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