Skip to content

Commit

Permalink
Merge pull request #5215 from hallieswan/SWC-6560
Browse files Browse the repository at this point in the history
SWC-6560: discussions e2e test
  • Loading branch information
hallieswan authored Oct 26, 2023
2 parents 4523088 + 09ec62d commit 641b6ff
Show file tree
Hide file tree
Showing 9 changed files with 688 additions and 46 deletions.
468 changes: 468 additions & 0 deletions e2e/discussions.spec.ts

Large diffs are not rendered by default.

42 changes: 10 additions & 32 deletions e2e/files.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import path from 'path'
import { testAuth } from './fixtures/authenticatedUserPages'
import {
createFile,
createProject,
deleteFileHandleWithRetry,
deleteProject,
entityUrlPathname,
generateEntityName,
getEntityFileHandleId,
getEntityIdFromPathname,
} from './helpers/entities'
import {
setupProject,
teardownProjectsAndFileHandles,
} from './helpers/setupTeardown'
import {
dismissAlert,
getAccessTokenFromCookie,
Expand Down Expand Up @@ -167,40 +168,17 @@ let fileHandleIds: string[] = []

test.describe('Files', () => {
testAuth.beforeAll(async ({ browser, storageStatePaths }) => {
const userContext = await browser.newContext({
storageState: storageStatePaths['swc-e2e-user'],
})
const userPage = await userContext.newPage()
const userAccessToken = await getAccessTokenFromCookie(userPage)

// create project
const userProjectName = generateEntityName('project')
const userProjectId = await createProject(
userProjectName,
userAccessToken,
userPage,
)
userProject = { name: userProjectName, id: userProjectId }

await userContext.close()
userProject = await setupProject(browser, 'swc-e2e-user', storageStatePaths)
})

testAuth.afterAll(async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
const accessToken = getAdminPAT()

// delete project
if (userProject.id) {
await deleteProject(userProject.id, accessToken, page)
}

// delete fileHandles
for await (const fileHandleId of fileHandleIds) {
await deleteFileHandleWithRetry(accessToken, fileHandleId, page)
await teardownProjectsAndFileHandles(
browser,
[userProject],
fileHandleIds,
)
}

await context.close()
})

testAuth(
Expand Down
6 changes: 3 additions & 3 deletions e2e/fixtures/authenticatedUserPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
storageStateDir,
userConfigs,
userPrefix,
UserPrefixes,
UserPrefix,
userValidatedPrefix,
} from '../helpers/userConfig'
import { waitForInitialPageLoad } from '../helpers/utils'
Expand All @@ -23,7 +23,7 @@ type AuthenticatedUserPageTestFixtures = {
validatedUserPage: Page
}

type StorageStatePaths = { [key in UserPrefixes]?: string }
export type StorageStatePaths = { [key in UserPrefix]?: string }
type AuthenticatedUserPageWorkerFixtures = {
createUsers: void
storageStatePaths: StorageStatePaths
Expand All @@ -40,7 +40,7 @@ const createPage = async (browser: Browser) => {

// Generate test fixture for a specified user
// ...based on: https://github.com/microsoft/playwright/issues/14570
function createUserPageFixture<Page>(userPrefix: UserPrefixes) {
function createUserPageFixture<Page>(userPrefix: UserPrefix) {
return async (
{ browser, storageStatePaths },
use: (r: Page) => Promise<void>,
Expand Down
28 changes: 28 additions & 0 deletions e2e/helpers/ACCESS_TYPE.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* https://rest-docs.synapse.org/rest/org/sagebionetworks/repo/model/ACCESS_TYPE.html
*/
export enum ACCESS_TYPE {
CREATE = 'CREATE',
READ = 'READ',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
CHANGE_PERMISSIONS = 'CHANGE_PERMISSIONS',
DOWNLOAD = 'DOWNLOAD',
/**
* @deprecated
*/
UPLOAD = 'UPLOAD',
PARTICIPATE = 'PARTICIPATE',
SUBMIT = 'SUBMIT',
READ_PRIVATE_SUBMISSION = 'READ_PRIVATE_SUBMISSION',
UPDATE_SUBMISSION = 'UPDATE_SUBMISSION',
DELETE_SUBMISSION = 'DELETE_SUBMISSION',
TEAM_MEMBERSHIP_UPDATE = 'TEAM_MEMBERSHIP_UPDATE',
SEND_MESSAGE = 'SEND_MESSAGE',
CHANGE_SETTINGS = 'CHANGE_SETTINGS',
MODERATE = 'MODERATE',
/** Allows to review a group of submissions (e.g. on ARs) for a given object */
REVIEW_SUBMISSIONS = 'REVIEW_SUBMISSIONS',
}

export default ACCESS_TYPE
55 changes: 55 additions & 0 deletions e2e/helpers/acl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Page } from '@playwright/test'
import ACCESS_TYPE from './ACCESS_TYPE'
import { AccessControlList, ResourceAccess } from './types'
import { waitForSrcEndpointConfig } from './utils'

// https://rest-docs.synapse.org/rest/GET/entity/id/acl.html
async function getEntityACL(page: Page, entityId: string, accessToken: string) {
await waitForSrcEndpointConfig(page)
const acl = await page.evaluate(
async ({ entityId, accessToken }) => {
// @ts-expect-error: Cannot find name 'SRC'
return await SRC.SynapseClient.getEntityACL(entityId, accessToken)
},
{ entityId, accessToken },
)
return acl as AccessControlList
}

/**
* Update an Entity's ACL
* Note: The caller must be granted ACCESS_TYPE.CHANGE_PERMISSIONS on the Entity to call this method.
* https://rest-docs.synapse.org/rest/PUT/entity/id/acl.html
*/
async function updateEntityACL(
page: Page,
acl: AccessControlList,
accessToken: string,
) {
await waitForSrcEndpointConfig(page)
const updatedACL = await page.evaluate(
async ({ acl, accessToken }) => {
// @ts-expect-error: Cannot find name 'SRC'
return await SRC.SynapseClient.updateEntityACL(acl, accessToken)
},
{ acl, accessToken },
)
return updatedACL as AccessControlList
}

// Note: The caller must be granted ACCESS_TYPE.CHANGE_PERMISSIONS on the Entity to call this method
export async function addUserToEntityACL(
page: Page,
entityId: string,
userId: number,
accessToken: string,
accessType: ACCESS_TYPE[] = [ACCESS_TYPE.DOWNLOAD, ACCESS_TYPE.READ],
) {
const acl = await getEntityACL(page, entityId, accessToken)
const newAccess: ResourceAccess = {
principalId: userId,
accessType: accessType,
}
acl.resourceAccess.push(newAccess)
return await updateEntityACL(page, acl, accessToken)
}
83 changes: 83 additions & 0 deletions e2e/helpers/setupTeardown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Browser } from '@playwright/test'
import { StorageStatePaths } from '../fixtures/authenticatedUserPages'
import ACCESS_TYPE from './ACCESS_TYPE'
import { addUserToEntityACL } from './acl'
import {
createProject,
deleteFileHandleWithRetry,
deleteProject,
generateEntityName,
} from './entities'
import {
getAccessTokenFromCookie,
getAdminPAT,
getUserIdFromLocalStorage,
} from './testUser'
import { Project } from './types'
import { UserPrefix } from './userConfig'

export const setupProject = async (
browser: Browser,
projectCreator: UserPrefix,
storageStatePaths: StorageStatePaths,
) => {
const context = await browser.newContext({
storageState: storageStatePaths[projectCreator],
})
const page = await context.newPage()
const accessToken = await getAccessTokenFromCookie(page)

const projectName = generateEntityName('project')
const projectId = await createProject(projectName, accessToken, page)
await context.close()

return { name: projectName, id: projectId } as Project
}

export const setupProjectWithPermissions = async (
browser: Browser,
projectCreator: UserPrefix,
projectAccessor: UserPrefix,
storageStatePaths: StorageStatePaths,
accessorPermissions: ACCESS_TYPE[] = [ACCESS_TYPE.DOWNLOAD, ACCESS_TYPE.READ],
) => {
const project = await setupProject(browser, projectCreator, storageStatePaths)

const context = await browser.newContext({
storageState: storageStatePaths[projectAccessor],
})
const page = await context.newPage()
const userId = await getUserIdFromLocalStorage(page)
await addUserToEntityACL(
page,
project.id,
Number(userId),
getAdminPAT(),
accessorPermissions,
)
await context.close()

return project
}

export const teardownProjectsAndFileHandles = async (
browser: Browser,
projects: Project[],
fileHandleIds: string[],
) => {
const context = await browser.newContext()
const page = await context.newPage()
const accessToken = getAdminPAT()

// delete projects
for (const project of projects) {
await deleteProject(project.id, accessToken, page)
}

// delete fileHandles
for (const fileHandleId of fileHandleIds) {
await deleteFileHandleWithRetry(accessToken, fileHandleId, page)
}

await context.close()
}
18 changes: 18 additions & 0 deletions e2e/helpers/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import ACCESS_TYPE from './ACCESS_TYPE'

/* USERS */
export type LoginResponse = {
accessToken: string // A token that authorizes subsequent requests
Expand Down Expand Up @@ -84,3 +86,19 @@ export type FileUploadComplete = {
}

export type FileType = 'text/txt' | 'text/csv' | 'application/json'

/* ACL */
export type ResourceAccess = {
principalId: number
accessType: ACCESS_TYPE[]
}

export type AccessControlList = {
id: string
createdBy?: string
creationDate?: string
modifiedBy?: string
modifiedOn?: string
etag?: string
resourceAccess: ResourceAccess[]
}
4 changes: 2 additions & 2 deletions e2e/helpers/userConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const storageStateDir = 'playwright/.auth/'
export const userPrefix = 'swc-e2e-user'
export const userValidatedPrefix = 'swc-e2e-user-validated'
const userPrefixes = [userPrefix, userValidatedPrefix] as const
export type UserPrefixes = (typeof userPrefixes)[number]
export type UserPrefix = (typeof userPrefixes)[number]

const generateUserName = (prefix: string) => {
// uncomment to use static username for troubleshooting:
Expand All @@ -27,7 +27,7 @@ const generateUserEmail = (prefix: string) => {
}

type UserConfig = {
[key in UserPrefixes]: TestUser
[key in UserPrefix]: TestUser
}

export const userConfigs: UserConfig = {
Expand Down
30 changes: 21 additions & 9 deletions e2e_workflow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@

### Local

1. Create a .env file with the following environment variables: `ADMIN_PAT`.
2. Build SWC: `mvn clean install`
3. Run Tests\*: `yarn e2e`

\*Note: if running tests repeatedly without changing SWC, it will be faster to run SWC in a separate terminal (`mvn gwt:run`) and then run tests (`yarn e2e`), since Playwright will use the existing server and SWC won't need to recompile before tests are run.

The test suite will create a new test user with randomly generated username / password on the dev stack for tests that require an authenticated test user. The user will be deleted after the tests finish running.

When writing a new test, it can be useful to create a static user with known username and password, so that issues can be debugged by logging into the user's account. Additionally, users can only be deleted if all associated objects have also been deleted. If a test creates an entity in the user account but fails to clean up the entity, the test user won't be deleted and will clutter the dev stack. After a failed test run, persistent entities and test user accounts can be cleaned up more easily if user credentials are known.
1. Install Docker, if not already installed.
2. Create a new admin user on the backend dev stack named: `swc-e2e-admin-{your-name}`, then create a new PAT with View, Download, and Modify permissions.
3. Create a `.env` file with the following environment variables: `ADMIN_PAT`.
4. Build SWC: `yarn build`
5. Serve SWC via Tomcat Docker container: `yarn docker:start`
6. Run Tests: `yarn e2e`. Tests can be run multiple times against the same Docker container.
7. When finished testing, stop and remove Docker container: `yarn docker:stop`

Notes:

- Test user creation:
- The test suite will create a new test user with randomly generated username / password on the backend dev stack for tests that require an authenticated test user. The user will be deleted after the tests finish running.
- However, test users can only be deleted if all associated objects have also been deleted. If a test creates an entity in the user account but fails to clean up the entity, the test user won't be deleted and will clutter the dev stack. After a failed test run, persistent entities and test user accounts can be cleaned up more easily if user credentials are known.
- Writing new tests:
- Create static test users with known username and password, so that issues can be debugged by logging into the user accounts. See comments in `e2e/helpers/userConfig.ts`.
- Start by running the new test against one browser with one worker, trace on, and no retries: `yarn e2e --project=firefox --workers=1 --retries=0 --trace=on e2e/{new_test}.spec.ts`.
- Before pushing changes to CI, run tests with the same configuration as CI by adding `CI=true` to the `.env` file.
- Running tests without installing Docker:
- Ensure that your maven settings file (usually located at `~/.m2/settings.xml`) points at the backend development stack. See endpoint parameters [in this guide](https://sagebionetworks.jira.com/wiki/spaces/SWC/pages/15597754/Developer+Bootstrap).
- Run `mvn clean install` followed by `mvn gwt:run` instead of steps 2 and 3.
- If you would like to run tests repeatedly without changing SWC, it will be faster to run SWC in a separate terminal (`mvn gwt:run`) and then run tests (`yarn e2e`), since Playwright will use the existing server and SWC won't need to recompile before tests are run.

### CI

Expand Down

0 comments on commit 641b6ff

Please sign in to comment.