diff --git a/e2e/tests/api-driven/src/hasuraTriggers/hasuraTriggers.feature b/e2e/tests/api-driven/src/hasuraTriggers/hasuraTriggers.feature new file mode 100644 index 0000000000..ab3c9b8e0f --- /dev/null +++ b/e2e/tests/api-driven/src/hasuraTriggers/hasuraTriggers.feature @@ -0,0 +1,14 @@ +Feature: Database triggers + + @regression @add-user-trigger + Scenario: Adding a user to Planx - with Templates team + Given the Templates team exists + When a new user is added + Then they are granted access to the Templates team + And have the teamEditor role + + @regression @add-user-trigger + Scenario: Adding a user to Planx - without Templates team + Given the Templates team does not exist + When a new user is added + Then they are not granted access to the Templates team \ No newline at end of file diff --git a/e2e/tests/api-driven/src/hasuraTriggers/helpers.ts b/e2e/tests/api-driven/src/hasuraTriggers/helpers.ts new file mode 100644 index 0000000000..1d63422601 --- /dev/null +++ b/e2e/tests/api-driven/src/hasuraTriggers/helpers.ts @@ -0,0 +1,6 @@ +import { $admin } from "../client"; + +export const cleanup = async () => { + await $admin.user._destroyAll(); + await $admin.team._destroyAll(); +}; diff --git a/e2e/tests/api-driven/src/hasuraTriggers/steps.ts b/e2e/tests/api-driven/src/hasuraTriggers/steps.ts new file mode 100644 index 0000000000..e76d02c591 --- /dev/null +++ b/e2e/tests/api-driven/src/hasuraTriggers/steps.ts @@ -0,0 +1,62 @@ +import { After, Given, Then, When, World } from "@cucumber/cucumber"; +import { cleanup } from "./helpers"; +import { User } from "@opensystemslab/planx-core/types"; +import { $admin } from "../client"; +import assert from "assert"; +import { createTeam, createUser } from "../globalHelpers"; + +export class CustomWorld extends World { + user!: User; + templatesTeamId!: number; +} + +After("@add-user-trigger", async function () { + await cleanup(); +}); + +Given("the Templates team exists", async function (this) { + const templatesTeamId = await createTeam({ slug: "templates" }); + + assert.ok(templatesTeamId, "Templates team is not defined"); + + this.templatesTeamId = templatesTeamId; +}); + +Given("the Templates team does not exist", async function (this) { + const templatesTeam = await $admin.team.getBySlug("templates"); + + assert.equal( + templatesTeam, + undefined, + "Templates team exists but should not be defined", + ); +}); + +When("a new user is added", async function (this) { + const userId = await createUser(); + const user = await $admin.user.getById(userId); + + assert.ok(user, "User is not defined"); + + this.user = user; +}); + +Then( + "they are granted access to the Templates team", + async function (this) { + assert.strictEqual(this.user.teams.length, 1); + assert.strictEqual(this.user.teams[0].team.slug, "templates"); + assert.strictEqual(this.user.teams[0].team.id, this.templatesTeamId); + }, +); + +Then("have the teamEditor role", async function (this) { + assert.strictEqual(this.user.teams[0].role, "teamEditor"); +}); + +Then( + "they are not granted access to the Templates team", + async function (this) { + assert.strictEqual(this.user.teams.length, 0); + }, +); diff --git a/editor.planx.uk/src/@planx/components/NextSteps/Public.tsx b/editor.planx.uk/src/@planx/components/NextSteps/Public.tsx index 194c2b34f6..9b534dd75d 100644 --- a/editor.planx.uk/src/@planx/components/NextSteps/Public.tsx +++ b/editor.planx.uk/src/@planx/components/NextSteps/Public.tsx @@ -1,4 +1,5 @@ import { PublicProps } from "@planx/components/ui"; + import React from "react"; import NextStepsList from "ui/NextStepsList"; @@ -18,7 +19,10 @@ const NextStepsComponent: React.FC = (props) => { policyRef={props.policyRef} howMeasured={props.howMeasured} /> - + ); }; diff --git a/editor.planx.uk/src/components/Header.tsx b/editor.planx.uk/src/components/Header.tsx index ee2c12563f..d8ea18c2c9 100644 --- a/editor.planx.uk/src/components/Header.tsx +++ b/editor.planx.uk/src/components/Header.tsx @@ -482,10 +482,7 @@ const EditorToolbar: React.FC<{ {user.isPlatformAdmin ? `All teams` - : user.teams - .map((team) => team.team.name) - .concat(["Templates"]) - .join(", ")} + : user.teams.map((team) => team.team.name).join(", ")} )} diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/user.test.ts b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/user.test.ts new file mode 100644 index 0000000000..5c0f9e24c7 --- /dev/null +++ b/editor.planx.uk/src/pages/FlowEditor/lib/__tests__/user.test.ts @@ -0,0 +1,101 @@ +import { User } from "@opensystemslab/planx-core/types"; + +import { FullStore, vanillaStore } from "../store"; + +const { getState, setState } = vanillaStore; +const { canUserEditTeam } = getState(); + +const redUser: User = { + id: 1, + isPlatformAdmin: false, + firstName: "Red", + lastName: "Reddison", + email: "red@red-team.com", + teams: [ + { + role: "teamEditor", + team: { + name: "Red Team", + slug: "red-team", + id: 1, + }, + }, + { + role: "teamViewer", + team: { + name: "Blue Team", + slug: "blue-team", + id: 1, + }, + }, + ], +}; + +const blueUser: User = { + id: 2, + isPlatformAdmin: false, + firstName: "Blue", + lastName: "Bluey", + email: "blue@blue-team.com", + teams: [ + { + role: "teamEditor", + team: { + name: "Blue Team", + slug: "blue-team", + id: 1, + }, + }, + ], +}; + +const readOnlyUser: User = { + id: 3, + isPlatformAdmin: false, + firstName: "Read", + lastName: "Only", + email: "readonly@no-team.com", + teams: [], +}; + +const adminUser: User = { + id: 4, + isPlatformAdmin: true, + firstName: "Platform", + lastName: "Admin", + email: "admin@opensystemslab.io", + teams: [], +}; + +let initialState: FullStore; + +beforeEach(() => { + initialState = getState(); +}); + +afterEach(() => setState(initialState)); + +describe("canUserEditTeam helper function", () => { + it("returns true when a user has teamEditor permission for a team", () => { + setState({ user: redUser }); + expect(canUserEditTeam("red-team")).toBe(true); + expect(canUserEditTeam("blue-team")).toBe(false); + }); + + it("returns false when a user does not have permission for a team", () => { + setState({ user: blueUser }); + expect(canUserEditTeam("red-team")).toBe(false); + }); + + it("returns false when a user does not have any permissions", () => { + setState({ user: readOnlyUser }); + expect(canUserEditTeam("red-team")).toBe(false); + expect(canUserEditTeam("blue-team")).toBe(false); + }); + + it("returns true when a user is has the platformAdmin role", () => { + setState({ user: adminUser }); + expect(canUserEditTeam("red-team")).toBe(true); + expect(canUserEditTeam("blue-team")).toBe(true); + }); +}); diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx index a493c74a4b..3aef3a108f 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx +++ b/editor.planx.uk/src/pages/FlowEditor/lib/analyticsProvider.tsx @@ -10,16 +10,19 @@ export type AnalyticsType = "init" | "resume"; type AnalyticsLogDirection = AnalyticsType | "forwards" | "backwards"; export type HelpClickMetadata = Record; +export type SelectedUrlsMetadata = Record<'selectedUrls', string[]>; let lastAnalyticsLogId: number | undefined = undefined; const analyticsContext = createContext<{ createAnalytics: (type: AnalyticsType) => Promise; trackHelpClick: (metadata?: HelpClickMetadata) => Promise; + trackNextStepsLinkClick: (metadata?: SelectedUrlsMetadata) => Promise; node: Store.node | null; }>({ createAnalytics: () => Promise.resolve(), trackHelpClick: () => Promise.resolve(), + trackNextStepsLinkClick: () => Promise.resolve(), node: null, }); const { Provider } = analyticsContext; @@ -100,6 +103,7 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ value={{ createAnalytics, trackHelpClick, + trackNextStepsLinkClick, node, }} > @@ -171,6 +175,30 @@ export const AnalyticsProvider: React.FC<{ children: React.ReactNode }> = ({ } } + async function trackNextStepsLinkClick(metadata?: SelectedUrlsMetadata) { + if (shouldTrackAnalytics && lastAnalyticsLogId) { + await publicClient.mutate({ + mutation: gql` + mutation UpdateHasClickNextStepsLink( + $id: bigint! + $metadata: jsonb = {} + ) { + update_analytics_logs_by_pk( + pk_columns: { id: $id } + _append: { metadata: $metadata } + ) { + id + } + } + `, + variables: { + id: lastAnalyticsLogId, + metadata, + }, + }); + } + } + async function createAnalytics(type: AnalyticsType) { if (shouldTrackAnalytics) { const response = await publicClient.mutate({ diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts index 0f8cec17fd..861ad8c764 100644 --- a/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts +++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/user.ts @@ -1,4 +1,4 @@ -import { User } from "@opensystemslab/planx-core/types"; +import { User, UserTeams } from "@opensystemslab/planx-core/types"; import { _client } from "client"; import jwtDecode from "jwt-decode"; import { Team } from "types"; @@ -22,16 +22,13 @@ export const userStore: StateCreator = ( getUser: () => get().user, canUserEditTeam(teamSlug) { - const user = this.getUser(); + const user = get().getUser(); if (!user) return false; - return ( - user.isPlatformAdmin || - teamSlug === "templates" || - user.teams.filter( - (team) => team.role === "teamEditor" && team.team.slug === teamSlug, - ).length > 0 - ); + const hasTeamEditorRole = (team: UserTeams) => + team.role === "teamEditor" && team.team.slug === teamSlug; + + return user.isPlatformAdmin || user.teams.some(hasTeamEditorRole); }, async initUserStore(jwt: string) { diff --git a/editor.planx.uk/src/pages/Preview/StatusPage.tsx b/editor.planx.uk/src/pages/Preview/StatusPage.tsx index b01ebe2e3c..898fb0a494 100644 --- a/editor.planx.uk/src/pages/Preview/StatusPage.tsx +++ b/editor.planx.uk/src/pages/Preview/StatusPage.tsx @@ -96,7 +96,7 @@ const StatusPage: React.FC = ({ onClick={removeSessionIdSearchParam} sx={contentFlowSpacing} > - Start new application + Start new application )} diff --git a/editor.planx.uk/src/ui/NextStepsList.tsx b/editor.planx.uk/src/ui/NextStepsList.tsx index ef13c27ffe..e7feb561f1 100644 --- a/editor.planx.uk/src/ui/NextStepsList.tsx +++ b/editor.planx.uk/src/ui/NextStepsList.tsx @@ -5,8 +5,9 @@ import Link from "@mui/material/Link"; import { styled, Theme } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; import type { Step as StyledListItem } from "@planx/components/NextSteps/model"; +import { useAnalyticsTracking } from "pages/FlowEditor/lib/analyticsProvider"; import { handleSubmit } from "pages/Preview/Node"; -import React from "react"; +import React, { useState } from "react"; interface NextStepsListProps { steps: StyledListItem[]; @@ -15,6 +16,7 @@ interface NextStepsListProps { interface ListItemProps extends StyledListItem { handleSubmit?: handleSubmit; + handleSelectingUrl?: (url: string) => void; } const Root = styled("ul")(({ theme }) => ({ @@ -72,11 +74,18 @@ const ArrowButton = styled("span")(({ theme }) => ({ flexShrink: "0", })); -const LinkStep = (props: ListItemProps) => ( - - - -); +function LinkStep(props: ListItemProps) { + return ( + props.handleSelectingUrl && props.url && props.handleSelectingUrl(props.url)} + > + + + ); +} const ContinueStep = (props: ListItemProps) => ( props.handleSubmit && props.handleSubmit()}> @@ -109,12 +118,20 @@ const Step = ({ title, description, url }: ListItemProps) => ( ); function NextStepsList(props: NextStepsListProps) { + const [selectedUrls, setSelectedUrls] = useState([]); + const { trackNextStepsLinkClick } = useAnalyticsTracking() + + const handleSelectingUrl = (newUrl: string) => { + setSelectedUrls(prevSelectedUrls => [...prevSelectedUrls, newUrl]); + trackNextStepsLinkClick({'selectedUrls': [...selectedUrls, newUrl]}) + } + return ( {props.steps?.map((step, i) => ( {step.url ? ( - + ) : ( )} diff --git a/hasura.planx.uk/metadata/tables.yaml b/hasura.planx.uk/metadata/tables.yaml index ec848ee84c..5b5f621381 100644 --- a/hasura.planx.uk/metadata/tables.yaml +++ b/hasura.planx.uk/metadata/tables.yaml @@ -62,6 +62,52 @@ - table: schema: public name: bops_applications + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - req_headers + - request + - response + - response_headers + - bops_id + - destination_url + - session_id + - created_at + - sanitised_at + select_permissions: + - role: api + permission: + columns: + - id + - req_headers + - request + - response + - response_headers + - bops_id + - destination_url + - session_id + - created_at + - sanitised_at + filter: {} + update_permissions: + - role: api + permission: + columns: + - id + - req_headers + - request + - response + - response_headers + - bops_id + - destination_url + - session_id + - created_at + - sanitised_at + filter: {} + check: {} event_triggers: - name: setup_bops_applications_notifications definition: @@ -90,6 +136,46 @@ - table: schema: public name: email_applications + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - request + - response + - recipient + - team_slug + - created_at + - sanitised_at + - session_id + select_permissions: + - role: api + permission: + columns: + - id + - request + - response + - recipient + - team_slug + - created_at + - sanitised_at + - session_id + filter: {} + update_permissions: + - role: api + permission: + columns: + - id + - request + - response + - recipient + - team_slug + - created_at + - sanitised_at + - session_id + filter: {} + check: {} event_triggers: - name: setup_email_applications_notifications definition: @@ -119,6 +205,12 @@ using: foreign_key_constraint_on: flow_id select_permissions: + - role: api + permission: + columns: + - document_template + - flow_id + filter: {} - role: public permission: columns: @@ -177,6 +269,20 @@ name: compile_flow_portals comment: Flow data with portals merged in insert_permissions: + - role: api + permission: + check: {} + columns: + - creator_id + - team_id + - settings + - slug + - created_at + - updated_at + - copied_from + - id + - version + - data - role: platformAdmin permission: check: {} @@ -213,6 +319,22 @@ - version - data select_permissions: + - role: api + permission: + columns: + - creator_id + - team_id + - settings + - slug + - created_at + - updated_at + - copied_from + - id + - version + - data + computed_fields: + - data_merged + filter: {} - role: platformAdmin permission: columns: @@ -259,6 +381,21 @@ - data_merged filter: {} update_permissions: + - role: api + permission: + columns: + - creator_id + - team_id + - settings + - slug + - created_at + - updated_at + - copied_from + - id + - version + - data + filter: {} + check: {} - role: platformAdmin permission: columns: @@ -376,6 +513,21 @@ - flow_id - id select_permissions: + - role: api + permission: + columns: + - data + - email + - created_at + - deleted_at + - locked_at + - submitted_at + - updated_at + - flow_id + - id + - has_user_saved + - sanitised_at + filter: {} - role: public permission: columns: @@ -397,6 +549,22 @@ - deleted_at: _is_null: true update_permissions: + - role: api + permission: + columns: + - data + - email + - created_at + - deleted_at + - locked_at + - submitted_at + - updated_at + - flow_id + - id + - has_user_saved + - sanitised_at + filter: {} + check: {} - role: public permission: columns: @@ -610,7 +778,36 @@ - name: session using: foreign_key_constraint_on: session_id + insert_permissions: + - role: api + permission: + check: {} + columns: + - session_preview_data + - applicant_name + - payee_email + - payee_name + - created_at + - paid_at + - id + - session_id + - govpay_payment_id + - payment_amount select_permissions: + - role: api + permission: + columns: + - session_preview_data + - applicant_name + - payee_email + - payee_name + - created_at + - paid_at + - id + - session_id + - govpay_payment_id + - payment_amount + filter: {} - role: public permission: columns: @@ -624,6 +821,27 @@ filter: id: _eq: x-hasura-payment-request-id + update_permissions: + - role: api + permission: + columns: + - session_preview_data + - applicant_name + - payee_email + - payee_name + - created_at + - paid_at + - id + - session_id + - govpay_payment_id + - payment_amount + filter: {} + check: {} + delete_permissions: + - role: api + permission: + backend_only: false + filter: {} event_triggers: - name: setup_payment_expiry_events definition: @@ -751,6 +969,18 @@ insertion_order: null column_mapping: session_id: id + insert_permissions: + - role: api + permission: + check: {} + columns: + - payment_id + - status + - team_slug + - created_at + - flow_id + - session_id + - amount - table: schema: public name: payment_status_enum @@ -758,6 +988,16 @@ - table: schema: public name: planning_constraints_requests + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - response + - destination_url + - session_id + - created_at - table: schema: public name: project_types @@ -779,6 +1019,16 @@ using: foreign_key_constraint_on: publisher_id insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - publisher_id + - summary + - created_at + - flow_id + - data - role: platformAdmin permission: check: {} @@ -808,6 +1058,16 @@ - flow_id - data select_permissions: + - role: api + permission: + columns: + - id + - publisher_id + - summary + - created_at + - flow_id + - data + filter: {} - role: platformAdmin permission: columns: @@ -841,6 +1101,21 @@ - table: schema: public name: reconciliation_requests + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - response + - message + - session_id + - created_at + delete_permissions: + - role: api + permission: + backend_only: false + filter: {} - table: schema: public name: sessions @@ -924,6 +1199,14 @@ - role - id select_permissions: + - role: api + permission: + columns: + - team_id + - user_id + - role + - id + filter: {} - role: platformAdmin permission: columns: @@ -999,6 +1282,23 @@ - theme - updated_at select_permissions: + - role: api + permission: + columns: + - id + - notify_personalisation + - settings + - theme + - domain + - name + - slug + - created_at + - updated_at + - boundary + - submission_email + computed_fields: + - boundary_bbox + filter: {} - role: platformAdmin permission: columns: @@ -1061,6 +1361,49 @@ - table: schema: public name: uniform_applications + insert_permissions: + - role: api + permission: + check: {} + columns: + - id + - response + - idox_submission_id + - created_at + - destination + - xml + - payload + - sanitised_at + - submission_reference + select_permissions: + - role: api + permission: + columns: + - id + - response + - idox_submission_id + - created_at + - destination + - xml + - payload + - sanitised_at + - submission_reference + filter: {} + update_permissions: + - role: api + permission: + columns: + - id + - response + - idox_submission_id + - created_at + - destination + - xml + - payload + - sanitised_at + - submission_reference + filter: {} + check: {} event_triggers: - name: setup_uniform_applications_notifications definition: @@ -1124,6 +1467,17 @@ - is_platform_admin - email select_permissions: + - role: api + permission: + columns: + - id + - first_name + - last_name + - created_at + - updated_at + - is_platform_admin + - email + filter: {} - role: platformAdmin permission: columns: diff --git a/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/down.sql b/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/down.sql new file mode 100644 index 0000000000..2daff6714a --- /dev/null +++ b/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/down.sql @@ -0,0 +1,2 @@ +DROP FUNCTION IF EXISTS grant_new_user_template_team_access; +DROP TRIGGER grant_new_user_template_team_access on users; \ No newline at end of file diff --git a/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/up.sql b/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/up.sql new file mode 100644 index 0000000000..a0ae84a00e --- /dev/null +++ b/hasura.planx.uk/migrations/1696884217237_grant_new_user_template_team_access/up.sql @@ -0,0 +1,28 @@ +CREATE OR REPLACE FUNCTION grant_new_user_template_team_access() RETURNS trigger AS $$ +DECLARE + templates_team_id INT; +BEGIN + SELECT id INTO templates_team_id FROM teams WHERE slug = 'templates'; + IF templates_team_id IS NOT NULL THEN + INSERT INTO team_members (user_id, team_id, role) VALUES (NEW.id, templates_team_id, 'teamEditor'); + END IF; + + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER grant_new_user_template_team_access AFTER INSERT ON users + FOR EACH ROW EXECUTE PROCEDURE grant_new_user_template_team_access(); + +COMMENT ON TRIGGER grant_new_user_template_team_access ON users +IS 'Automatically grant all new users teamEditor access to the shared Templates team'; + +-- Insert a record to team_members for all existing users +INSERT INTO + team_members (user_id, team_id, role) +SELECT + id, + 29, + 'teamEditor' +FROM + users; \ No newline at end of file diff --git a/hasura.planx.uk/tests/analytics.test.js b/hasura.planx.uk/tests/analytics.test.js index 53bfb9c10b..5147eb2a17 100644 --- a/hasura.planx.uk/tests/analytics.test.js +++ b/hasura.planx.uk/tests/analytics.test.js @@ -69,4 +69,26 @@ describe("analytics and analytics_logs", () => { expect(i).toHaveNoMutationsFor("analytics_logs"); }); }); + + describe("api", () => { + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query analytics", () => { + expect(i.queries).not.toContain("analytics"); + }); + + test("cannot query analytics_logs", () => { + expect(i.queries).not.toContain("analytics_logs"); + }); + + test("cannot create, update, or delete analytics", () => { + expect(i).toHaveNoMutationsFor("analytics"); + }); + + test("cannot create, update, or delete analytics_logs", () => { + expect(i).toHaveNoMutationsFor("analytics_logs"); + }); + }); }); diff --git a/hasura.planx.uk/tests/blpu_codes.test.js b/hasura.planx.uk/tests/blpu_codes.test.js index ade04eb536..7614433e41 100644 --- a/hasura.planx.uk/tests/blpu_codes.test.js +++ b/hasura.planx.uk/tests/blpu_codes.test.js @@ -47,7 +47,22 @@ describe("blpu_codes", () => { describe("teamEditor", () => { let i; beforeAll(async () => { - i = await introspectAs("platformAdmin"); + i = await introspectAs("teamEditor"); + }); + + test("cannot query blpu_codes", () => { + expect(i.queries).not.toContain("blpu_codes"); + }); + + test("cannot create, update, or delete blpu_codes", () => { + expect(i).toHaveNoMutationsFor("blpu_codes"); + }); + }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); }); test("cannot query blpu_codes", () => { diff --git a/hasura.planx.uk/tests/bops_applications.test.js b/hasura.planx.uk/tests/bops_applications.test.js index dc6b2b909f..91001b1262 100644 --- a/hasura.planx.uk/tests/bops_applications.test.js +++ b/hasura.planx.uk/tests/bops_applications.test.js @@ -59,4 +59,21 @@ describe("bops_applications", () => { expect(i).toHaveNoMutationsFor("bops_applications"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query and mutate bops applications", () => { + expect(i.queries).toContain("bops_applications"); + expect(i.mutations).toContain("insert_bops_applications"); + expect(i.mutations).toContain("update_bops_applications_by_pk"); + }); + + test("cannot delete bops applications", () => { + expect(i.mutations).not.toContain("delete_bops_applications"); + }); + }); }); diff --git a/hasura.planx.uk/tests/email_applications.test.js b/hasura.planx.uk/tests/email_applications.test.js index b4a2cd711a..11db01eed5 100644 --- a/hasura.planx.uk/tests/email_applications.test.js +++ b/hasura.planx.uk/tests/email_applications.test.js @@ -60,4 +60,22 @@ describe("email_applications", () => { expect(i).toHaveNoMutationsFor("email_applications"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("has full access to query and mutate email applications", () => { + expect(i.queries).toContain("email_applications"); + expect(i.mutations).toContain("insert_email_applications"); + expect(i.mutations).toContain("insert_email_applications_one"); + expect(i.mutations).toContain("update_email_applications_by_pk"); + }); + + test("cannot delete email applications", () => { + expect(i.mutations).not.toContain("delete_email_applications"); + }); + }); }); diff --git a/hasura.planx.uk/tests/flow_document_templates.test.js b/hasura.planx.uk/tests/flow_document_templates.test.js new file mode 100644 index 0000000000..75057b2f86 --- /dev/null +++ b/hasura.planx.uk/tests/flow_document_templates.test.js @@ -0,0 +1,81 @@ +const { introspectAs } = require("./utils"); + +describe("flow_document_templates", () => { + describe("public", () => { + let i; + beforeAll(async () => { + i = await introspectAs("public"); + }); + + // TODO: Check this - seems unnecessary / incorrect? + test.skip("cannot query flow_document_templates", () => { + expect(i.queries).not.toContain("flow_document_templates"); + }); + + test("cannot create, update, or delete flow_document_templates", () => { + expect(i).toHaveNoMutationsFor("flow_document_templates"); + }); + }); + + describe("admin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("admin"); + }); + + test("can query flow_document_templates", () => { + expect(i.queries).toContain("flow_document_templates"); + }); + + test("can create, update, or delete flow_document_templates", () => { + expect(i.mutations).toContain("insert_flow_document_templates"); + expect(i.mutations).toContain("insert_flow_document_templates_one"); + expect(i.mutations).toContain("update_flow_document_templates_by_pk"); + expect(i.mutations).toContain("delete_flow_document_templates_by_pk"); + }); + }); + + describe("platformAdmin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("platformAdmin"); + }); + + test("cannot query flow_document_templates", () => { + expect(i.queries).not.toContain("flow_document_templates"); + }); + + test("cannot create, update, or delete flow_document_templates", () => { + expect(i).toHaveNoMutationsFor("flow_document_templates"); + }); + }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query flow_document_templates", () => { + expect(i.queries).not.toContain("flow_document_templates"); + }); + + test("cannot create, update, or delete flow_document_templates", () => { + expect(i).toHaveNoMutationsFor("flow_document_templates"); + }); + }); + + describe("api", () => { + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query flow_document_templates", () => { + expect(i.queries).toContain("flow_document_templates"); + }); + + test("cannot create, update, or delete flow_document_templates", () => { + expect(i).toHaveNoMutationsFor("flow_document_templates"); + }); + }); +}); diff --git a/hasura.planx.uk/tests/flows.test.js b/hasura.planx.uk/tests/flows.test.js index fbfd945f38..2338e7ea27 100644 --- a/hasura.planx.uk/tests/flows.test.js +++ b/hasura.planx.uk/tests/flows.test.js @@ -140,4 +140,46 @@ describe("flows and operations", () => { expect(i.mutations).not.toContain("update_published_flows"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query flows", () => { + expect(i.queries).toContain("flows"); + }); + + test("can create and update flows", () => { + expect(i.mutations).toContain("update_flows_by_pk"); + expect(i.mutations).toContain("update_flows"); + }); + + test("cannot delete flows", () => { + expect(i.mutations).not.toContain("delete_flows_by_pk"); + expect(i.mutations).not.toContain("delete_flows"); + }); + + test("cannot query or mutate operations", () => { + expect(i.queries).not.toContain("operations"); + expect(i).toHaveNoMutationsFor("operations"); + }); + + test("can query published flows", () => { + expect(i.queries).toContain("published_flows"); + }); + + test("can create published_flows", () => { + expect(i.mutations).toContain("insert_published_flows_one"); + expect(i.mutations).toContain("insert_published_flows"); + }); + + test("cannot update or delete published_flows", () => { + expect(i.mutations).not.toContain("delete_published_flows_by_pk"); + expect(i.mutations).not.toContain("delete_published_flows"); + expect(i.mutations).not.toContain("update_published_flows_by_pk"); + expect(i.mutations).not.toContain("update_published_flows"); + }); + }); }); diff --git a/hasura.planx.uk/tests/global_settings.test.js b/hasura.planx.uk/tests/global_settings.test.js index 98d12e8bbc..c7b3d9be95 100644 --- a/hasura.planx.uk/tests/global_settings.test.js +++ b/hasura.planx.uk/tests/global_settings.test.js @@ -63,4 +63,19 @@ describe("global_settings", () => { expect(i).toHaveNoMutationsFor("global_settings"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query global_settings view", () => { + expect(i.queries).not.toContain("global_settings"); + }); + + test("cannot create, update, or delete global_settings", () => { + expect(i).toHaveNoMutationsFor("global_settings"); + }); + }); }); diff --git a/hasura.planx.uk/tests/lowcal_sessions.test.js b/hasura.planx.uk/tests/lowcal_sessions.test.js index 7de02f9e9d..aa0149ff88 100644 --- a/hasura.planx.uk/tests/lowcal_sessions.test.js +++ b/hasura.planx.uk/tests/lowcal_sessions.test.js @@ -458,4 +458,26 @@ describe("lowcal_sessions", () => { expect(i).toHaveNoMutationsFor("lowcal_sessions"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot insert lowcal_sessions", () => { + expect(i.mutations).not.toContain("insert_lowcal_sessions"); + expect(i.mutations).not.toContain("insert_lowcal_sessions_one"); + }); + + test("can query and update local_sessions", () => { + expect(i.queries).toContain("lowcal_sessions"); + expect(i.mutations).toContain("update_lowcal_sessions_by_pk"); + expect(i.mutations).toContain("update_lowcal_sessions"); + }); + + test("cannot delete lowcal_sessions", () => { + expect(i.mutations).not.toContain("delete_lowcal_sessions"); + }); + }); }); diff --git a/hasura.planx.uk/tests/payment_requests.test.js b/hasura.planx.uk/tests/payment_requests.test.js index 45ecb2c4e5..5580187ae3 100644 --- a/hasura.planx.uk/tests/payment_requests.test.js +++ b/hasura.planx.uk/tests/payment_requests.test.js @@ -127,6 +127,21 @@ describe("payment_requests", () => { expect(i).toHaveNoMutationsFor("payment_requests"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("has full access to query and mutate payment_requests", async () => { + expect(i.queries).toContain("payment_requests"); + expect(i.mutations).toContain("insert_payment_requests"); + expect(i.mutations).toContain("update_payment_requests"); + expect(i.mutations).toContain("update_payment_requests_by_pk"); + expect(i.mutations).toContain("delete_payment_requests"); + }); + }); }); const insertSessions = async (sessionIds) => { diff --git a/hasura.planx.uk/tests/payment_status.test.js b/hasura.planx.uk/tests/payment_status.test.js index 35ea07522b..db1ff42490 100644 --- a/hasura.planx.uk/tests/payment_status.test.js +++ b/hasura.planx.uk/tests/payment_status.test.js @@ -66,4 +66,24 @@ describe("payment_status", () => { expect(i).toHaveNoMutationsFor("payment_status"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query payment_status", () => { + expect(i.queries).not.toContain("payment_status"); + }) + + test("can insert payment_status", () => { + expect(i.mutations).toContain("insert_payment_status"); + }); + + test("cannot delete or update payment_status", () => { + expect(i.mutations).not.toContain("update_payment_status"); + expect(i.mutations).not.toContain("delete_payment_status"); + }) + }); }); diff --git a/hasura.planx.uk/tests/planning_constraints_requests.test.js b/hasura.planx.uk/tests/planning_constraints_requests.test.js index 2d178eb68c..b1c5648db5 100644 --- a/hasura.planx.uk/tests/planning_constraints_requests.test.js +++ b/hasura.planx.uk/tests/planning_constraints_requests.test.js @@ -59,4 +59,24 @@ describe("planning_constraints_requests", () => { expect(i).toHaveNoMutationsFor("planning_constraints_requests"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query planning_constraints_requests", () => { + expect(i.queries).not.toContain("planning_constraints_requests"); + }) + + test("can insert planning_constraints_requests", () => { + expect(i.mutations).toContain("insert_planning_constraints_requests"); + }); + + test("cannot update or delete planning_constriants_requests", () => { + expect(i.mutations).not.toContain("update_planning_constraints_requests_by_pk"); + expect(i.mutations).not.toContain("delete_planning_constraints_requests"); + }) + }); }); diff --git a/hasura.planx.uk/tests/reconciliation_requests.test.js b/hasura.planx.uk/tests/reconciliation_requests.test.js new file mode 100644 index 0000000000..482f98723d --- /dev/null +++ b/hasura.planx.uk/tests/reconciliation_requests.test.js @@ -0,0 +1,88 @@ +const { introspectAs } = require("./utils"); + +describe("reconciliation_requests", () => { + describe("public", () => { + let i; + beforeAll(async () => { + i = await introspectAs("public"); + }); + + test("cannot query reconciliation_requests", () => { + expect(i.queries).not.toContain("reconciliation_requests"); + }); + + test("cannot create, update, or delete reconciliation_requests", () => { + expect(i).toHaveNoMutationsFor("reconciliation_requests"); + }); + }); + + describe("admin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("admin"); + }); + + test("can query reconciliation_requests", () => { + expect(i.queries).toContain("reconciliation_requests"); + }); + + test("can create, update, or delete reconciliation_requests", () => { + expect(i.mutations).toContain("insert_reconciliation_requests"); + expect(i.mutations).toContain("insert_reconciliation_requests_one"); + expect(i.mutations).toContain("update_reconciliation_requests_by_pk"); + expect(i.mutations).toContain("delete_reconciliation_requests_by_pk"); + }); + }); + + describe("platformAdmin", () => { + let i; + beforeAll(async () => { + i = await introspectAs("platformAdmin"); + }); + + test("cannot query reconciliation_requests", () => { + expect(i.queries).not.toContain("reconciliation_requests"); + }); + + test("cannot create, update, or delete reconciliation_requests", () => { + expect(i).toHaveNoMutationsFor("reconciliation_requests"); + }); + }); + + describe("teamEditor", () => { + let i; + beforeAll(async () => { + i = await introspectAs("teamEditor"); + }); + + test("cannot query reconciliation_requests", () => { + expect(i.queries).not.toContain("reconciliation_requests"); + }); + + test("cannot create, update, or delete reconciliation_requests", () => { + expect(i).toHaveNoMutationsFor("reconciliation_requests"); + }); + }); + + describe("api", () => { + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query reconciliation_requests", () => { + expect(i.queries).not.toContain("reconciliation_requests"); + }); + + test("cannot update reconciliation_requests", () => { + expect(i.mutations).not.toContain("update_reconciliation_requests"); + }); + + test("can delete reconciliation_requests", () => { + expect(i.mutations).toContain("delete_reconciliation_requests"); + }); + + test("can insert reconciliation requests", () => { + expect(i.mutations).toContain("insert_reconciliation_requests"); + }); + }); +}); diff --git a/hasura.planx.uk/tests/sessions.test.js b/hasura.planx.uk/tests/sessions.test.js index 12e708f93f..83f91797f1 100644 --- a/hasura.planx.uk/tests/sessions.test.js +++ b/hasura.planx.uk/tests/sessions.test.js @@ -625,5 +625,22 @@ describe("sessions", () => { expect(i).toHaveNoMutationsFor("sessions"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("cannot query sessions", () => { + expect(i.queries).not.toContain("sessions"); + }); + + test("cannot create, update, or delete sessions", () => { + expect(i.mutations).not.toContain("insert_sessions"); + expect(i.mutations).not.toContain("update_sessions"); + expect(i.mutations).not.toContain("delete_sessions"); + }); + }); }); diff --git a/hasura.planx.uk/tests/team_members.test.js b/hasura.planx.uk/tests/team_members.test.js index d88dd5c41a..6ed7853b1e 100644 --- a/hasura.planx.uk/tests/team_members.test.js +++ b/hasura.planx.uk/tests/team_members.test.js @@ -59,4 +59,19 @@ describe("team_members", () => { expect(i).toHaveNoMutationsFor("team_members"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query teams members", () => { + expect(i.queries).toContain("team_members"); + }); + + test("cannot create, update, or delete team_members", () => { + expect(i).toHaveNoMutationsFor("team_members"); + }); + }); }); diff --git a/hasura.planx.uk/tests/teams.test.js b/hasura.planx.uk/tests/teams.test.js index 6aca15d251..85865cb2dc 100644 --- a/hasura.planx.uk/tests/teams.test.js +++ b/hasura.planx.uk/tests/teams.test.js @@ -71,4 +71,19 @@ describe("teams", () => { expect(i.mutations).not.toContain("insert_teams"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query teams", () => { + expect(i.queries).toContain("teams"); + }); + + test("cannot create, update, or delete teams", () => { + expect(i).toHaveNoMutationsFor("teams"); + }); + }); }); diff --git a/hasura.planx.uk/tests/uniform_applications.test.js b/hasura.planx.uk/tests/uniform_applications.test.js index efa53fa142..92f851a377 100644 --- a/hasura.planx.uk/tests/uniform_applications.test.js +++ b/hasura.planx.uk/tests/uniform_applications.test.js @@ -59,4 +59,21 @@ describe("uniform_applications", () => { expect(i).toHaveNoMutationsFor("uniform_applications"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("has full access to query and mutate uniform applications", () => { + expect(i.queries).toContain("uniform_applications"); + expect(i.mutations).toContain("insert_uniform_applications"); + expect(i.mutations).toContain("update_uniform_applications_by_pk"); + }); + + test("cannot delete uniform applications", () => { + expect(i.mutations).not.toContain("delete_uniform_applications"); + }) + }); }); diff --git a/hasura.planx.uk/tests/users.test.js b/hasura.planx.uk/tests/users.test.js index 91149556e6..80466806d2 100644 --- a/hasura.planx.uk/tests/users.test.js +++ b/hasura.planx.uk/tests/users.test.js @@ -66,4 +66,19 @@ describe("users", () => { expect(i).toHaveNoMutationsFor("users"); }); }); + + describe("api", () => { + let i; + beforeAll(async () => { + i = await introspectAs("api"); + }); + + test("can query users", async () => { + expect(i.queries).toContain("users"); + }); + + test("cannot create, update, or delete users", async () => { + expect(i).toHaveNoMutationsFor("users"); + }); + }); }); diff --git a/hasura.planx.uk/tests/utils.js b/hasura.planx.uk/tests/utils.js index c4d724d987..f0e5a4cdf0 100644 --- a/hasura.planx.uk/tests/utils.js +++ b/hasura.planx.uk/tests/utils.js @@ -83,6 +83,7 @@ const introspectAs = async (role, userId = undefined) => { public: gqlPublic, platformAdmin: gqlWithRole("platformAdmin", userId), teamEditor: gqlWithRole("teamEditor", userId), + api: gqlWithRole("api"), }[role] const INTROSPECTION_QUERY = ` query IntrospectionQuery {