From 24242e29436c69f593d744b7c5e9b61de77d3a0b Mon Sep 17 00:00:00 2001 From: Rory Doak Date: Wed, 6 Nov 2024 09:25:28 +0000 Subject: [PATCH] refine feature file add Given logic and alter jwt build for demoUser lint fix refine Given statements add When steps add Then steps lint:fix tidy verification and helper --- .../src/demo-workspace/demoUser.feature | 72 ++-- .../api-driven/src/demo-workspace/helper.ts | 336 ++++++++++++++++++ .../demo-workspace/steps/background_steps.ts | 147 +++++++- .../demo-workspace/steps/navigation_steps.ts | 59 +-- .../steps/verification_steps.ts | 128 +++++-- e2e/tests/api-driven/src/jwt.ts | 10 +- e2e/tests/ui-driven/src/helpers/context.ts | 2 +- .../ui-driven/src/helpers/globalHelpers.ts | 6 +- 8 files changed, 643 insertions(+), 117 deletions(-) diff --git a/e2e/tests/api-driven/src/demo-workspace/demoUser.feature b/e2e/tests/api-driven/src/demo-workspace/demoUser.feature index 679fd1a845..3ca059e639 100644 --- a/e2e/tests/api-driven/src/demo-workspace/demoUser.feature +++ b/e2e/tests/api-driven/src/demo-workspace/demoUser.feature @@ -1,13 +1,8 @@ Feature: Demo user access Background: - Given I am a user with a demoUser role - And I have two users in the database: - | id | first_name | last_name | email | - | 1 | Demo | User | demo.user@email.com | - | 2 | Nota | Demo | nota.demo@email.com | - And I have the following teams in the database: + Given I have the following teams in the database: | id | name | slug | | 1 | Open Systems Lab | open-systems-lab | | 29 | Templates | templates | @@ -15,39 +10,48 @@ | 32 | Demo | demo | | 45 | Other Team | other-team | + And I have two users in the database: + | id | first_name | last_name | email | + | 1 | Demo | User | demo.user@email.com | + | 2 | Nota | Demo | nota.demo@email.com | + And I have the following flows in the database: - | id | creator_id | name | team_id | - | 1 | 1 | Test Flow 1 | 32 | - | 2 | 1 | Test Flow 2 | 32 | - | 3 | 2 | Other Flow | 45 | + | creator_id | name | slug | team_id | + | 1 | Demo Flow | demo-flow | 32 | + | 2 | Test OSL | test-osl | 1 | + | 2 | Test Templates | test-templates | 29 | + | 2 | Test ODP | test-odp | 30 | + | 2 | Other Flow | other-flow | 45 | + And I am a demoUser + + @demo-user-permissions + Scenario: I can only view my own flows + When I am in the "" team + Then I should only see my own flows + But I should not see flows that I have not created + Examples: + | TEAM | + | demo | -@demo-user-permissions -Scenario: I can only view my own flows - When I am in the Demo team - Then I should only see flows with ids "1, 2" - And I should not see flow with id "3" @demo-user-permissions Scenario Outline: I can only view specific teams - When I am on the Teams page - Then I can only see team with id: "" + When I query the teams table + Then I can access the teams with slug: "" + But I should not access the Other Team Examples: - | ID | - | 1 | - | 29 | - | 30 | - | 32 | - - @demo-user-permissions - Scenario: Creating a new flow in the Demo team - When I am in the Demo team - Then I should be able to create a flow + | SLUG | + | open-systems-lab | + | templates | + | open-digital-planning | + | demo | @demo-user-permissions - Scenario Outline: Creating a new flow in other teams - When I am in the "" team - Then I should not be able to create a flow + Scenario Outline: Creating a new flow + When I insert a flow into the team: "" + Then I should not succeed + But I should succeed in the Demo team Examples: | TEAM | @@ -57,8 +61,7 @@ Scenario: I can only view my own flows @demo-user-permissions Scenario Outline: Actioning my own flows - When I am in the Demo team - And I am on my own flow + When I am on my own flow Then I should be able to "" the flow Examples: @@ -78,11 +81,6 @@ Scenario: I can only view my own flows | open-systems-lab | | open-digital-planning | - @demo-user-permissions - Scenario: Accessing flow settings - When I am on my own flow - Then I should have access to flow settings - @demo-user-permissions Scenario Outline: Editing team settings When I am in the "" team diff --git a/e2e/tests/api-driven/src/demo-workspace/helper.ts b/e2e/tests/api-driven/src/demo-workspace/helper.ts index e69de29bb2..3d59accf80 100644 --- a/e2e/tests/api-driven/src/demo-workspace/helper.ts +++ b/e2e/tests/api-driven/src/demo-workspace/helper.ts @@ -0,0 +1,336 @@ +import gql from "graphql-tag"; +import { $admin } from "../client"; +import { Team } from "@opensystemslab/planx-core/types"; +import { UPDATE_FLOW_QUERY } from "../permissions/queries/flows"; +import { UUID } from "crypto"; + +export type User = { + id: number; + first_name: string; + last_name: string; + email: string; +}; + +export const cleanup = async () => { + await $admin.flow._destroyAll(); + await $admin.team._destroyAll(); + await $admin.user._destroyAll(); +}; + +export const userExistsCheck = async (userId: number) => { + const userOne = await $admin.user.getById(userId); + return Boolean(userOne); +}; + +export const addDemoUserToTeam = async (userId: number, teamId: number) => { + try { + const memberAdded = await upsertMember({ + userId, + teamId, + role: "demoUser", + }); + return true; + } catch (error) { + console.error(error); + return false; + } +}; + +export const checkTeamsExist = async (teamArray) => { + const existenceArray = await Promise.all( + teamArray.map(async (team) => { + const teamObj = await $admin.team.getBySlug(team.slug); + return teamObj; + }), + ); + return existenceArray; +}; + +export const createTeams = async (array) => { + const teamIdArray = await Promise.all( + array.map(async (team) => { + const id = await createTeam(team); + return id; + }), + ); + return teamIdArray; +}; + +export const createFlow = async (client, args) => { + try { + const { flow } = await client.request( + gql` + mutation InsertFlow($teamId: Int!, $slug: String!, $name: String!) { + flow: insert_flows_one( + object: { team_id: $teamId, slug: $slug, name: $name } + ) { + id + slug + creator_id + } + } + `, + { + teamId: args.teamId, + slug: args.slug, + name: args.name, + }, + ); + + return flow; + } catch (error) { + return false; + } +}; + +export async function createTeam( + newTeam: Team & { id: number }, +): Promise { + try { + const response: { insert_teams_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateTeam($name: String!, $id: Int, $slug: String!) { + insert_teams_one( + object: { + id: $id + name: $name + slug: $slug + # Create empty records for associated tables - these can get populated later + team_settings: { data: {} } + integrations: { data: {} } + } + ) { + id + } + } + `, + { + ...newTeam, + }, + ); + return response.insert_teams_one.id; + } catch (error) { + return 0; + } +} + +export async function upsertMember(args): Promise { + const response: { insert_team_members_one: { id: number } | null } = + await $admin.client.request( + gql` + mutation UpsertTeamMember( + $role: user_roles_enum = demoUser + $team_id: Int + $user_id: Int + ) { + insert_team_members_one( + object: { team_id: $team_id, user_id: $user_id, role: $role } + ) { + id + } + } + `, + { + team_id: args.teamId, + user_id: args.userId, + role: args.role, + }, + ); + return Boolean(response.insert_team_members_one?.id); +} + +export async function createUser(args): Promise { + const response: { insert_users_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateUser( + $id: Int + $first_name: String! + $last_name: String! + $email: String! + $is_platform_admin: Boolean + ) { + insert_users_one( + object: { + id: $id + first_name: $first_name + last_name: $last_name + email: $email + is_platform_admin: $is_platform_admin + } + ) { + id + } + } + `, + { + id: args.id, + first_name: args.first_name, + last_name: args.last_name, + email: args.email, + is_platform_admin: args.isPlatformAdmin, + }, + ); + return response.insert_users_one.id; +} +export async function createDemoUser(args): Promise { + const response: { insert_users_one: { id: number } } = + await $admin.client.request( + gql` + mutation CreateUser( + $id: Int + $first_name: String! + $last_name: String! + $email: String! + $is_platform_admin: Boolean = false + $role: user_roles_enum! + ) { + insert_users_one( + object: { + id: $id + first_name: $first_name + last_name: $last_name + email: $email + is_platform_admin: $is_platform_admin + teams: { data: { role: $role, team_id: 32 } } + } + ) { + id + } + } + `, + { + id: args.id, + first_name: args.first_name, + last_name: args.last_name, + email: args.email, + is_platform_admin: args.isPlatformAdmin, + role: "demoUser", + }, + ); + return response.insert_users_one.id; +} + +export async function getTeams(client) { + const { teams } = await client.request(gql` + query getTeams { + teams { + id + slug + team_settings { + homepage + } + } + } + `); + + return teams; +} + +export async function getTeamAndFlowsBySlug(client, slug) { + const { + teams: [team], + } = await client.request( + gql` + query getTeamAndFlowsBySlug($slug: String) { + teams(where: { slug: { _eq: $slug } }) { + id + slug + flows { + creator_id + id + } + } + } + `, + { + slug, + }, + ); + + return team; +} + +export async function getFlowBySlug(client, slug) { + const { + flows: [flow], + } = await client.request( + gql` + query getFlowBySlug($slug: String) { + flows(where: { slug: { _eq: $slug } }) { + slug + id + } + } + `, + { + slug: slug, + }, + ); + + return flow; +} + +export async function updateFlow(client, flowId: UUID) { + const { update_flows_by_pk: response } = await client.request( + gql` + mutation updateFlow($flowId: uuid!) { + result: update_flows_by_pk( + pk_columns: { id: $flowId } + _set: { slug: "new-slug", name: "new Name" } + ) { + id + } + } + `, + + { flowId: flowId }, + ); + return response; +} + +export async function deleteFlow(client, flowId: UUID) { + const { delete_flows_by_pk: response } = await client.request( + gql` + mutation MyMutation($flowId: uuid!) { + delete_flows_by_pk(id: $flowId) { + id + } + } + `, + + { flowId: flowId }, + ); + return response; +} + +export async function updateTeamSettings(client, teamId: number) { + try { + const { update_team_settings: response } = await client.request( + gql` + mutation updateTeamSettings($teamId: Int!) { + update_team_settings( + where: { team_id: { _eq: $teamId } } + _set: { + homepage: "newpage.com" + submission_email: "new.email@email.com" + } + ) { + returning { + team_id + submission_email + homepage + } + } + } + `, + { + teamId: teamId, + }, + ); + return response; + } catch (error) { + return false; + } +} diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts index c199e8eb2a..42b6503980 100644 --- a/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts +++ b/e2e/tests/api-driven/src/demo-workspace/steps/background_steps.ts @@ -1,22 +1,135 @@ -import { Given } from "@cucumber/cucumber"; +import { After, Before, DataTable, Given, World } from "@cucumber/cucumber"; +import { + checkTeamsExist, + cleanup, + createDemoUser, + createFlow, + createTeams, + createUser, + User, +} from "../helper"; +import { $admin, getClient } from "../../client"; +import { strict as assert } from "node:assert"; +import { CoreDomainClient } from "@opensystemslab/planx-core"; +import { Team } from "@opensystemslab/planx-core/types"; +import { UUID } from "node:crypto"; -Given('I am a user with a demoUser role', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +export class CustomWorld extends World { + demoClient!: CoreDomainClient["client"]; + adminClient!: CoreDomainClient["client"]; + demoUser!: User; + otherTeam!: Record | undefined; + insertFlowTeamId!: number; + demoFlowSlug!: string; + adminFlowSlug!: string; + demoTeamsArray!: Team[]; + currentTeamId!: number; + teamFlows!: { creator_id: number; id: UUID }[]; +} - Given('I have two users in the database:', async function (dataTable) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +Before("@demo-user-permissions", async function () { + await cleanup(); +}); - Given('I have the following teams in the database:', async function (dataTable) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +After("@demo-user-permissions", async function () { + await cleanup(); +}); +Given( + "I have two users in the database:", + async function (dataTable: DataTable) { + // I need to then query the database to get the users + // then assert.ok they exist + const featureData = dataTable.hashes(); - Given('I have the following flows in the database:', async function (dataTable) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); + const userOneId = await createDemoUser({ + ...featureData[0], + isPlatformAdmin: false, + }); + const userTwoId = await createUser({ + ...featureData[1], + isPlatformAdmin: true, + }); + + const userOne = await $admin.user.getById(userOneId); + const userTwo = await $admin.user.getById(userTwoId); + + if (userOne) { + const { client: demoClient } = await getClient(userOne?.email); + this.demoClient = demoClient; + this.demoUser = userOne; + } + if (userTwo) { + const { client: adminClient } = await getClient(userTwo?.email); + this.adminClient = adminClient; + } + + assert.ok(userOne && userTwo, "Users have not been added correctly"); + }, +); + +Given( + "I have the following teams in the database:", + async function (this, dataTable: DataTable) { + const teamsArray = dataTable.hashes(); + + await createTeams(teamsArray); + + const getTeamsArray = await checkTeamsExist(teamsArray); + this.otherTeam = teamsArray.find((team) => team.slug === "other-team"); + + assert.ok(Boolean(getTeamsArray), "Teams have not been added correctly"); + }, +); + +Given( + "I have the following flows in the database:", + async function (this, dataTable) { + const flowsArray = dataTable.hashes(); + + const demoFlow = flowsArray.find((flow) => flow.creator_id === "1"); + const adminFlow = flowsArray.find((flow) => flow.creator_id === "2"); + this.demoFlowSlug = demoFlow.slug; + this.adminFlowSlug = adminFlow.slug; + + const newFlowArray = await Promise.all( + flowsArray.map(async (flow) => { + if (flow.creator_id === "1") { + const flowId = await createFlow(this.demoClient, { + teamId: Number(flow.team_id), + name: flow.name, + slug: flow.slug, + }); + return flowId; + } + + if (flow.creator_id === "2") { + const flowId = await createFlow(this.adminClient, { + teamId: Number(flow.team_id), + name: flow.name, + slug: flow.slug, + }); + return flowId; + } + return "no creator id"; + }), + ); + + assert.ok( + Boolean(newFlowArray.length === 5), + "Flows have not been added correctly", + ); + assert.ok( + Boolean(!newFlowArray.includes(false)), + `NewFlowArray is returning: ${newFlowArray}`, + ); + }, +); + +Given("I am a demoUser", async function () { + const userOne = await $admin.user.getById(1); + assert.ok( + Boolean(userOne?.teams[0].role === "demoUser"), + "User 1 is a demo user", + ); +}); diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts index 6c967d933e..295cd9c74e 100644 --- a/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts +++ b/e2e/tests/api-driven/src/demo-workspace/steps/navigation_steps.ts @@ -1,26 +1,45 @@ import { When } from "@cucumber/cucumber"; +import { $admin } from "../../client"; +import { strict as assert } from "node:assert"; +import { getFlowBySlug, getTeamAndFlowsBySlug, getTeams } from "../helper"; +import { CustomWorld } from "./background_steps"; -When('I am on the Teams page', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +When("I am in the {string} team", async function (this, string) { + // Write code here that turns the phrase above into concrete actions + const team = await getTeamAndFlowsBySlug(this.demoClient, string); + this.currentTeamId = team.id; + this.teamFlows = team.flows; + assert.equal(string, team.slug, "Error retrieving the correct team"); +}); -When('I am in the Demo team', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +When("I query the teams table", async function (this) { + const teamsArray = await getTeams(this.demoClient); + this.demoTeamsArray = teamsArray; + assert.ok(teamsArray, "Teams not fetched correctly"); +}); - When('I am in the {string} team', async function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +When( + "I insert a flow into the team: {string}", + async function (this, string) { + const team = await $admin.team.getBySlug(string); + this.insertFlowTeamId = team.id; + }, +); - When('I am on my own flow', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +When("I am on my own flow", async function (this) { + const flow = await getFlowBySlug(this.demoClient, this.demoFlowSlug); - When('I want to edit a flow that I did not create', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); + assert.equal(flow.slug, this.demoFlowSlug, "Incorrect flow has been fetched"); +}); + +When( + "I want to edit a flow that I did not create", + async function (this) { + const flow = await getFlowBySlug(this.adminClient, this.adminFlowSlug); + assert.equal( + flow.slug, + this.adminFlowSlug, + "Incorrect flow has been fetched", + ); + }, +); diff --git a/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts b/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts index 0e9134c203..8206e2740f 100644 --- a/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts +++ b/e2e/tests/api-driven/src/demo-workspace/steps/verification_steps.ts @@ -1,47 +1,99 @@ import { Then } from "@cucumber/cucumber"; +import { CustomWorld } from "./background_steps"; +import { strict as assert } from "node:assert"; +import { + createFlow, + deleteFlow, + getFlowBySlug, + updateFlow, + updateTeamSettings, +} from "../helper"; -Then('I should be able to create a flow', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); +Then("I should only see my own flows", async function (this) { + assert.equal( + this.demoUser.id, + this.teamFlows[0].creator_id, + "Creator ID is incorrect", + ); +}); + +Then( + "I should not see flows that I have not created", + async function (this) { + const doesContainOtherFlows = this.teamFlows.find( + (flow) => flow.creator_id !== this.demoUser.id, + ); + assert.ok(!doesContainOtherFlows, "Other flows are in the array"); + }, +); -Then('I should not be able to create a flow', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; +Then( + "I can access the teams with slug: {string}", + async function (this, string) { + const canAccessTeam = this.demoTeamsArray.find( + (team) => team.slug === string, + ); + assert.ok(canAccessTeam, "Team is not in the array"); + }, +); + +Then("I should not access the Other Team", async function (this) { + const cannotAccessOtherTeam = this.demoTeamsArray.find( + (team) => team.slug !== this.otherTeam?.slug, + ); + assert.ok(cannotAccessOtherTeam, "Team is not in the array"); }); -Then('I should have access to flow settings', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; +Then("I should not succeed", async function (this) { + const hasSucceeded = await createFlow(this.demoClient, { + name: "Bad flow", + slug: "bad-flow", + teamId: this.insertFlowTeamId, }); + assert.ok(!hasSucceeded, "Flow was able to be created on this team"); +}); - - Then('I should not have access to team settings', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'; +Then("I should succeed in the Demo team", async function () { + const hasSucceeded = await createFlow(this.demoClient, { + name: "Good flow", + slug: "good-flow", + teamId: 32, }); + assert.ok(hasSucceeded, "Flow not added correctly"); +}); + +Then( + "I should be able to {string} the flow", + async function (this, string) { + const demoFlow = await getFlowBySlug(this.demoClient, this.demoFlowSlug); + const hasSucceeded = + (await string) === "update" + ? updateFlow(this.demoClient, demoFlow.id) + : deleteFlow(this.demoClient, demoFlow.id); + + assert.ok(hasSucceeded, `Cannot ${string} the flow `); + }, +); + +Then( + "I should not have access to modify the flow", + async function (this) { + const canUpdate = await updateFlow(this.demoClient, this.teamFlows[0].id); + const canDelete = await deleteFlow(this.demoClient, this.teamFlows[0].id); + assert.ok( + !canUpdate && !canDelete, + "Flow can be modified by the demo user", + ); + }, +); - Then('I should not have access to modify the flow', async function () { - // Write code here that turns the phrase above into concrete actions - return 'pending'}); - - Then('I should be able to {string} the flow', async function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); - - Then('I can only see team with id: {string}', async function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); - - Then('I should only see flows with ids {string}', async function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); - - - Then('I should not see flow with id {string}', async function (string) { - // Write code here that turns the phrase above into concrete actions - return 'pending'; - }); \ No newline at end of file +Then( + "I should not have access to team settings", + async function (this) { + const canUpdateSettings = await updateTeamSettings( + this.demoClient, + this.currentTeamId, + ); + assert.ok(!canUpdateSettings, "Demo User can update the team settings"); + }, +); diff --git a/e2e/tests/api-driven/src/jwt.ts b/e2e/tests/api-driven/src/jwt.ts index 27abe0e0f2..02c1e49f72 100644 --- a/e2e/tests/api-driven/src/jwt.ts +++ b/e2e/tests/api-driven/src/jwt.ts @@ -31,8 +31,7 @@ const getAllowedRolesForUser = (user: User): Role[] => { const teamRoles = user.teams.map((teamRole) => teamRole.role); const allowedRoles: Role[] = [ "public", // Allow public access - "teamEditor", // Least privileged role for authenticated users - required for Editor access - ...teamRoles, // User specific roles + ...teamRoles, // User specif ic roles ]; if (user.isPlatformAdmin) allowedRoles.push("platformAdmin"); @@ -47,5 +46,10 @@ const getAllowedRolesForUser = (user: User): Role[] => { * This is the role of least privilege for the user */ const getDefaultRoleForUser = (user: User): Role => { - return user.isPlatformAdmin ? "platformAdmin" : "teamEditor"; + const teamRoles = user.teams.map((teamRole) => teamRole.role); + const isDemoUser = teamRoles.includes("demoUser"); + if (user.isPlatformAdmin) return "platformAdmin"; + if (isDemoUser) return "demoUser"; + + return "teamEditor"; }; diff --git a/e2e/tests/ui-driven/src/helpers/context.ts b/e2e/tests/ui-driven/src/helpers/context.ts index a68edf78f5..008547e52d 100644 --- a/e2e/tests/ui-driven/src/helpers/context.ts +++ b/e2e/tests/ui-driven/src/helpers/context.ts @@ -127,7 +127,7 @@ export function generateAuthenticationDemoToken(userId: string) { { sub: `${userId}`, "https://hasura.io/jwt/claims": { - "x-hasura-allowed-roles": ["public","demoUser"], + "x-hasura-allowed-roles": ["public", "demoUser"], "x-hasura-default-role": "demoUser", "x-hasura-user-id": `${userId}`, }, diff --git a/e2e/tests/ui-driven/src/helpers/globalHelpers.ts b/e2e/tests/ui-driven/src/helpers/globalHelpers.ts index ff112000dd..c8f70dc10f 100644 --- a/e2e/tests/ui-driven/src/helpers/globalHelpers.ts +++ b/e2e/tests/ui-driven/src/helpers/globalHelpers.ts @@ -2,7 +2,11 @@ import { FlowGraph } from "@opensystemslab/planx-core/types"; import type { Browser, Page, Request } from "@playwright/test"; import { gql } from "graphql-request"; import type { Context } from "./context"; -import { generateAuthenticationDemoToken, generateAuthenticationToken, getGraphQLClient } from "./context"; +import { + generateAuthenticationDemoToken, + generateAuthenticationToken, + getGraphQLClient, +} from "./context"; // Test card numbers to be used in gov.uk sandbox environment // reference: https://docs.payments.service.gov.uk/testing_govuk_pay/#if-you-39-re-using-a-test-39-sandbox-39-account