Skip to content

Commit

Permalink
Merge pull request #5178 from hallieswan/SWC-6531
Browse files Browse the repository at this point in the history
SWC-6531: refactor SWC e2e helpers to use exposed SRC methods and remove duplicate code
  • Loading branch information
hallieswan authored Sep 7, 2023
2 parents 66f4df0 + 1fb1716 commit b318091
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 346 deletions.
3 changes: 1 addition & 2 deletions e2e/auth.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
// - locally: set in .env file, read by dotenv

import { Page, expect, test as setup } from '@playwright/test'
import { getEndpoint } from './helpers/http'
import { setLocalStorage } from './helpers/localStorage'
import {
cleanupTestUser,
Expand Down Expand Up @@ -39,7 +38,7 @@ for (const {
setup.slow()

await setup.step('create test user', async () => {
userId = await createTestUser(getEndpoint(), user, getAdminPAT())
userId = await createTestUser(user, getAdminPAT(), userPage)
expect(userId).not.toBeUndefined()

await setLocalStorage(userPage, localStorageKey, user.username)
Expand Down
214 changes: 90 additions & 124 deletions e2e/helpers/http.ts
Original file line number Diff line number Diff line change
@@ -1,141 +1,107 @@
import { createHmac } from 'crypto'
import { fetchWithExponentialTimeout } from './srcFetch'
import { Page, expect } from '@playwright/test'
import { navigateToHomepageIfPageHasNotBeenLoaded } from './localStorage'

export const BASE64_ENCODING = 'base64'
export const DEFAULT_GETDELETE_HEADERS = {
Accept: '*/*',
'User-Agent': 'SynapseWebClient',
}
export const DEFAULT_POST_HEADERS = {
...DEFAULT_GETDELETE_HEADERS,
Accept: 'application/json; charset=UTF-8',
'Content-Type': 'application/json; charset=UTF-8',
}

function generateDigitalSignature(
data: string,
base64EncodedSecretKey: string,
) {
const hash = createHmac(
'sha1',
Buffer.from(base64EncodedSecretKey, BASE64_ENCODING),
)
.update(data)
.digest(BASE64_ENCODING)
return hash
}

function getDigitalSignature(
uri: string,
username: string,
base64EncodedSecretKey: string,
) {
const timestamp = new Date().toISOString()
const signature = generateDigitalSignature(
`${username}${uri}${timestamp}`,
base64EncodedSecretKey,
)
return {
userId: username,
signatureTimestamp: timestamp,
signature: signature,
}
}

export function getUserIdFromJwt(token: string) {
const payload = JSON.parse(
Buffer.from(token.split('.')[1], BASE64_ENCODING).toString(),
)
return payload.sub
export enum BackendDestinationEnum {
REPO_ENDPOINT,
PORTAL_ENDPOINT,
}

function updateHeaders(
headers: { [key: string]: string },
uri: string,
accessToken?: string,
userName?: string,
apiKey?: string,
) {
return {
...headers,
...(accessToken && {
'Access-Control-Request-Headers': 'authorization',
Authorization: `Bearer ${accessToken}`,
}),
...(apiKey && userName && getDigitalSignature(uri, userName, apiKey)),
}
export async function waitForSrcEndpointConfig(page: Page) {
// window only available after page has initially loaded
await navigateToHomepageIfPageHasNotBeenLoaded(page)
// ensure that endpoint config is set,
// ...so API calls point to the correct stack
await expect(async () => {
const response = await page.evaluate('window.SRC.OVERRIDE_ENDPOINT_CONFIG')
expect(response).not.toBeUndefined()
}).toPass()
}

export async function doPost<T>(
endpoint: string,
uri: string,
requestContent: string,
accessToken?: string,
userName?: string,
apiKey?: string,
page: Page,
url: string,
requestJsonObject: unknown,
accessToken: string | undefined,
endpoint: BackendDestinationEnum,
additionalOptions: RequestInit = {},
) {
const url: RequestInfo = `${endpoint}${uri}`
const options: RequestInit = {
body: requestContent,
headers: updateHeaders(
DEFAULT_POST_HEADERS,
uri,
await waitForSrcEndpointConfig(page)
const response = await page.evaluate(
async ({
url,
requestJsonObject,
accessToken,
userName,
apiKey,
),
method: 'POST',
mode: 'cors',
}
return await fetchWithExponentialTimeout(url, options)
endpoint,
additionalOptions,
}) => {
// @ts-expect-error: Cannot find name 'SRC'
const srcEndpoint = await SRC.SynapseEnums.BackendDestinationEnum[
endpoint
]
// @ts-expect-error: Cannot find name 'SRC'
return await SRC.HttpClient.doPost(
url,
requestJsonObject,
accessToken,
srcEndpoint,
additionalOptions,
)
},
{ url, requestJsonObject, accessToken, endpoint, additionalOptions },
)
return response
}

export async function doGet<T>(
endpoint: string,
uri: string,
accessToken?: string,
userName?: string,
apiKey?: string,
page: Page,
url: string,
accessToken: string | undefined,
endpoint: BackendDestinationEnum,
additionalOptions: RequestInit = {},
) {
const url: RequestInfo = `${endpoint}${uri}`
const options: RequestInit = {
body: null,
headers: updateHeaders(
DEFAULT_GETDELETE_HEADERS,
uri,
accessToken,
userName,
apiKey,
),
method: 'GET',
mode: 'cors',
}
return await fetchWithExponentialTimeout(url, options)
await waitForSrcEndpointConfig(page)
const response = await page.evaluate(
async ({ url, accessToken, endpoint, additionalOptions }) => {
// @ts-expect-error: Cannot find name 'SRC'
const srcEndpoint = await SRC.SynapseEnums.BackendDestinationEnum[
endpoint
]
// @ts-expect-error: Cannot find name 'SRC'
return await SRC.HttpClient.doGet(
url,
accessToken,
srcEndpoint,
additionalOptions,
)
},
{ url, accessToken, endpoint, additionalOptions },
)
return response
}

export async function doDelete<T>(
endpoint: string,
uri: string,
accessToken?: string,
userName?: string,
apiKey?: string,
page: Page,
url: string,
accessToken: string | undefined,
endpoint: BackendDestinationEnum,
additionalOptions: RequestInit = {},
) {
const url: RequestInfo = `${endpoint}${uri}`
const options: RequestInit = {
body: null,
headers: updateHeaders(
DEFAULT_GETDELETE_HEADERS,
uri,
accessToken,
userName,
apiKey,
),
method: 'DELETE',
mode: 'cors',
}
return await fetchWithExponentialTimeout(url, options)
}

export function getEndpoint() {
return 'https://repo-dev.dev.sagebase.org'
await waitForSrcEndpointConfig(page)
const response = await page.evaluate(
async ({ url, accessToken, endpoint, additionalOptions }) => {
// @ts-expect-error: Cannot find name 'SRC'
const srcEndpoint = await SRC.SynapseEnums.BackendDestinationEnum[
endpoint
]
// @ts-expect-error: Cannot find name 'SRC'
return await SRC.HttpClient.doDelete(
url,
accessToken,
srcEndpoint,
additionalOptions,
)
},
{ url, accessToken, endpoint, additionalOptions },
)
return response
}
55 changes: 41 additions & 14 deletions e2e/helpers/messages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { doDelete, doGet, getEndpoint } from './http'
import { Page } from '@playwright/test'
import { BackendDestinationEnum, doDelete, doGet } from './http'
import {
FileHandle,
MessageBundle,
Expand All @@ -7,49 +8,70 @@ import {
} from './types'

// Retrieves the current authenticated user's outbox.
export async function getUserOutbox(accessToken: string) {
export async function getUserOutbox(accessToken: string, page: Page) {
return (await doGet(
getEndpoint(),
page,
'/repo/v1/message/outbox',
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
)) as PaginatedResults<MessageToUser>
}

// Retrieves the current authenticated user's inbox.
// It may take several seconds for a message to appear in the inbox after creation.
async function getUserInbox(accessToken: string) {
async function getUserInbox(accessToken: string, page: Page) {
return (await doGet(
getEndpoint(),
page,
'/repo/v1/message/inbox',
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
)) as PaginatedResults<MessageBundle>
}

// Deletes a message. Only accessible to administrators.
async function deleteUserMessage(accessToken: string, messageId: string) {
async function deleteUserMessage(
accessToken: string,
messageId: string,
page: Page,
) {
await doDelete(
getEndpoint(),
page,
`/repo/v1/admin/message/${messageId}`,
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
)
return messageId
}

// Get a FileHandle using its ID.
// Note: Only the user that created the FileHandle can access it directly.
async function getFileHandle(accessToken: string, handleId: string) {
async function getFileHandle(
accessToken: string,
handleId: string,
page: Page,
) {
return (await doGet(
getEndpoint(),
page,
`/file/v1/fileHandle/${handleId}`,
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
)) as FileHandle
}

// Delete a FileHandle using its ID.
// Note: Only the user that created the FileHandle can delete it.
// Also, a FileHandle cannot be deleted if it is associated with a FileEntity or WikiPage
async function deleteFileHandle(accessToken: string, handleId: string) {
await doDelete(getEndpoint(), `/file/v1/fileHandle/${handleId}`, accessToken)
async function deleteFileHandle(
accessToken: string,
handleId: string,
page: Page,
) {
await doDelete(
page,
`/file/v1/fileHandle/${handleId}`,
accessToken,
BackendDestinationEnum.REPO_ENDPOINT,
)
return handleId
}

Expand All @@ -65,8 +87,9 @@ export async function deleteUserOutboxMessageAndAssociatedFile(
subject: string,
userAccessToken: string,
adminAccessToken: string,
page: Page,
) {
const messages = (await getUserOutbox(userAccessToken)).results.filter(
const messages = (await getUserOutbox(userAccessToken, page)).results.filter(
message =>
message.subject === subject &&
arraysAreEqual(recipients.sort(), message.recipients.sort()),
Expand All @@ -79,8 +102,8 @@ export async function deleteUserOutboxMessageAndAssociatedFile(
}

const message = messages[0]
await deleteUserMessage(adminAccessToken, message.id)
await deleteFileHandle(userAccessToken, message.fileHandleId)
await deleteUserMessage(adminAccessToken, message.id, page)
await deleteFileHandle(userAccessToken, message.fileHandleId, page)
}

export async function deleteTeamInvitationMessage(
Expand All @@ -89,24 +112,28 @@ export async function deleteTeamInvitationMessage(
teamName: string,
inviterAccessToken: string,
adminAccessToken: string,
page: Page,
) {
await deleteUserOutboxMessageAndAssociatedFile(
recipients,
`${inviterUserName} has invited you to join the ${teamName} team`,
inviterAccessToken,
adminAccessToken,
page,
)
}

export async function deleteTeamInviteAcceptanceMessage(
recipients: string[],
accepterAccessToken: string,
adminAccessToken: string,
page: Page,
) {
await deleteUserOutboxMessageAndAssociatedFile(
recipients,
'New Member Has Joined the Team',
accepterAccessToken,
adminAccessToken,
page,
)
}
Loading

0 comments on commit b318091

Please sign in to comment.