Skip to content

Commit

Permalink
fix(ui): broken playwright authentication for cleanup (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewrisse authored Sep 16, 2024
1 parent 8fbe561 commit 6d17187
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 57 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/e2e-playwright.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,15 @@ jobs:
rm packages/ui/zarf-package-leapfrogai-ui-amd64-e2e-test.tar.zst
# Run the playwright UI tests using the deployed Supabase endpoint and upload report as an artifact
# Note - workflow doesn't need teardown and causes import issues
- name: UI/API/Supabase E2E Playwright Tests
run: |
cp src/leapfrogai_ui/.env.example src/leapfrogai_ui/.env
mkdir -p playwright/auth
touch playwright/auth.user.json
rm src/leapfrogai_ui/tests/global.teardown.ts
mkdir -p src/leapfrogai_ui/playwright/.auth
SERVICE_ROLE_KEY=$(uds zarf tools kubectl get secret -n leapfrogai supabase-bootstrap-jwt -o jsonpath={.data.service-key} | base64 -d)
echo "::add-mask::$SERVICE_ROLE_KEY"
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY TEST_ENV=CI USERNAME=doug PASSWORD=$FAKE_E2E_USER_PASSWORD PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY TEST_ENV=CI USERNAME=doug PASSWORD=$FAKE_E2E_USER_PASSWORD PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY DEFAULT_MODEL=llama-cpp-python npm --prefix src/leapfrogai_ui run test:integration:ci
# Upload the Playwright report as an artifact
- name: Archive Playwright Report
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-registry1-weekly.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
mkdir -p playwright/auth
touch playwright/auth.user.json
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY TEST_ENV=CI USERNAME=doug PASSWORD=$FAKE_E2E_USER_PASSWORD PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY npm --prefix src/leapfrogai_ui run test:integration:ci
SERVICE_ROLE_KEY=$SERVICE_ROLE_KEY TEST_ENV=CI USERNAME=doug PASSWORD=$FAKE_E2E_USER_PASSWORD PUBLIC_SUPABASE_ANON_KEY=$ANON_KEY DEFAULT_MODEL=llama-cpp-python npm --prefix src/leapfrogai_ui run test:integration:ci
- name: Archive Playwright Report
uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
Expand Down
6 changes: 4 additions & 2 deletions src/leapfrogai_ui/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ SUPABASE_AUTH_KEYCLOAK_SECRET=<secret>
#ORIGIN=http://localhost:3000 # set if running in Docker locally (variable is also used in deployment)

#If specified, app will use OpenAI instead of LeapfrogAI
OPENAI_API_KEY=
#OPENAI_API_KEY=

# PLAYWRIGHT
USERNAME=[email protected]
PASSWORD=<password>
# MFA secret is only needed when running playwright tests locally. In the workflow, the keycloak user
# is created without MFA requirements
MFA_SECRET=<secret>
# Service Role key comes from Supabase and allows Playwright to bypass row level security for test setup/cleanup. This is only needed for tests.
SERVICE_ROLE_KEY=<key>
SERVICE_ROLE_KEY=<key>
2 changes: 1 addition & 1 deletion src/leapfrogai_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"format": "prettier --write . && eslint . --fix",
"test:integration": "playwright test",
"test:integration:ui": "playwright test --ui",
"test:integration:ci": "playwright test tests/global.setup.ts tests/api.test.ts tests/api-keys.test.ts tests/header.test.ts",
"test:integration:ci": "playwright test tests/global.setup.ts tests/api.test.ts tests/api-keys.test.ts",
"test:unit": "vitest run",
"test:unit:watch": "vitest",
"supabase:start": "supabase start",
Expand Down
17 changes: 16 additions & 1 deletion src/leapfrogai_ui/tests/chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ test('it saves in progress responses when interrupted by a page reload', async (
page,
openAIClient
}) => {
if (process.env.DEFAULT_MODEL === 'llama-cpp-python') {
test.skip();
}
const uniqueLongMessagePrompt = `${LONG_RESPONSE_PROMPT} ${new Date().toISOString()}`;
await loadChatPage(page);
const messages = page.getByTestId('message');
Expand All @@ -43,6 +46,9 @@ test('it saves in progress responses when interrupted by changing threads', asyn
page,
openAIClient
}) => {
if (process.env.DEFAULT_MODEL === 'llama-cpp-python') {
test.skip();
}
const uniqueLongMessagePrompt = `${LONG_RESPONSE_PROMPT} ${new Date().toISOString()}`;
await loadChatPage(page);
const messages = page.getByTestId('message');
Expand All @@ -64,6 +70,9 @@ function countWords(str: string) {
}

test('it cancels responses', async ({ page, openAIClient }) => {
if (process.env.DEFAULT_MODEL === 'llama-cpp-python') {
test.skip();
}
await loadChatPage(page);
const messages = page.getByTestId('message');
await sendMessage(page, LONG_RESPONSE_PROMPT);
Expand All @@ -83,6 +92,9 @@ test('it cancels responses when clicking enter instead of pause button and does
page,
openAIClient
}) => {
if (process.env.DEFAULT_MODEL === 'llama-cpp-python') {
test.skip();
}
await loadChatPage(page);
const messages = page.getByTestId('message');
await sendMessage(page, LONG_RESPONSE_PROMPT); // response must take a long time for this test to work
Expand Down Expand Up @@ -166,7 +178,10 @@ test('it formats code in a code block and can copy the code', async ({ page }) =

// The skeleton only shows for assistant messages
// TODO -this test can be flaky if the backend is really fast and the loading-msg skeleton barely has time to be shown
test('it shows a loading skeleton when a response is pending', async ({ page, openAIClient }) => {
test.skip('it shows a loading skeleton when a response is pending', async ({
page,
openAIClient
}) => {
const assistant = await createAssistantWithApi({ openAIClient });

await loadChatPage(page);
Expand Down
2 changes: 2 additions & 0 deletions src/leapfrogai_ui/tests/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const supabaseUsername = '[email protected]';
export const supabasePassword = 'fakepass';
62 changes: 36 additions & 26 deletions src/leapfrogai_ui/tests/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,51 @@
import { test as base } from '@playwright/test';
import OpenAI from 'openai';
import fs from 'node:fs';

type MyFixtures = {
openAIClient: OpenAI;
};

export async function getAccessToken() {
const supabaseUrl = process.env.PUBLIC_SUPABASE_URL;
const serviceRoleKey = process.env.SERVICE_ROLE_KEY;

const response = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
method: 'POST',
// @ts-expect-error: apikey is a required header for this request
headers: {
'Content-Type': 'application/json',
apikey: serviceRoleKey,
Authorization: `Bearer ${serviceRoleKey}`
},
body: JSON.stringify({
email: process.env.USERNAME,
password: process.env.PASSWORD
})
});

const data = await response.json();
type Cookie = {
name: string;
value: string;
domain: string;
path: string;
expires: number;
httpOnly: boolean;
secure: boolean;
sameSite: string;
};

if (response.ok) {
return data.access_token;
} else {
console.error('Error fetching access token:', data);
throw new Error(data.error_description || 'Failed to fetch access token');
// Gets an access token from cookie
export const getAccessToken = async () => {
try {
const authData = JSON.parse(
fs.readFileSync(`${process.cwd()}/playwright/.auth/user.json`, 'utf-8')
);
const cookie = authData.cookies.find(
(cookie: Cookie) =>
cookie.name === 'sb-supabase-kong-auth-token' ||
cookie.name === 'sb-supabase-kong-auth-token.0' ||
cookie.name === 'sb-supabase-kong-auth-token.1'
);
const cookieStripped = cookie.value.replace('base64-', '');
// Decode the base64 string
const convertedCookie = Buffer.from(cookieStripped, 'base64').toString('utf-8');
const accessTokenMatch = convertedCookie.match(/"access_token":"(.*?)"/);
if (!accessTokenMatch) {
console.log('Access token not found in cookie');
return '';
}
return accessTokenMatch[1];
} catch (e) {
console.error('Error getting access token', e);
return '';
}
}
};

export const getOpenAIClient = async () => {
const token = await getAccessToken();

return new OpenAI({
apiKey: process.env.OPENAI_API_KEY || token,
baseURL: process.env.OPENAI_API_KEY
Expand Down
19 changes: 7 additions & 12 deletions src/leapfrogai_ui/tests/global.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,21 @@ import { expect, test as setup } from './fixtures';
import * as OTPAuth from 'otpauth';
import { delay } from 'msw';
import type { Page } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';
import { supabasePassword, supabaseUsername } from './constants';

const doSupabaseLogin = async (page: Page) => {
// The fake keycloak user does not have a password stored in supabase, so login with that user will not work
// we create a separate user when keycloak is disabled (or use existing one)
await page.goto('/'); // go to the home page
await delay(2000); // allow page to fully hydrate
// when running in Github CI, create a new account because we don't have seed migrations
const emailField = page.getByTestId('email-input');
const passwordField = page.getByTestId('password-input');

await emailField.click();
await emailField.fill(process.env.USERNAME!);
await emailField.fill(supabaseUsername);
await passwordField.click();
await passwordField.fill(process.env.PASSWORD!);

const emailText = await emailField.innerText();
const passwordText = await passwordField.innerText();
if (emailText !== process.env.USERNAME!) await emailField.fill(process.env.USERNAME!);
if (passwordText !== process.env.PASSWORD!) await passwordField.fill(process.env.PASSWORD!);
await passwordField.fill(supabasePassword);

await page.getByTestId('submit-btn').click();

Expand All @@ -30,7 +26,6 @@ const doSupabaseLogin = async (page: Page) => {
await page.getByTestId('toggle-submit-btn').click();
await page.getByTestId('submit-btn').click();
}
// }
};

const doKeycloakLogin = async (page: Page) => {
Expand Down Expand Up @@ -107,7 +102,7 @@ setup('authenticate', async ({ page }) => {
// will invalidate the session and cause other tests to fail
await logout(page);

if (process.env.PUBLIC_DISABLE_KEYCLOAK === 'false') await delay(31000); // prevent logging back in too quickly and getting denied
if (process.env.PUBLIC_DISABLE_KEYCLOAK !== 'true') await delay(31000); // prevent logging back in too quickly and getting denied
// Log back in to begin rest of tests
await login(page);

Expand All @@ -119,5 +114,5 @@ setup('authenticate', async ({ page }) => {

// End of authentication steps.

await page.context().storageState({ path: authFile });
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
17 changes: 8 additions & 9 deletions src/leapfrogai_ui/tests/global.teardown.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { test as teardown } from './fixtures';
import { test } from '@playwright/test';
import { cleanup } from './helpers/cleanup';
import { getOpenAIClient } from './fixtures';

// teardown not necessary in CI testing envs
if (process.env.TEST_ENV !== 'CI') {
teardown('teardown', async ({ openAIClient }) => {
console.log('cleaning up...');
await cleanup(openAIClient);
console.log('clean up complete');
});
}
test('teardown', async () => {
const openAIClient = await getOpenAIClient();
console.log('cleaning up...');
await cleanup(openAIClient);
console.log('clean up complete');
});
2 changes: 1 addition & 1 deletion src/leapfrogai_ui/tests/helpers/apiHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const deleteAllTestAPIKeys = async () => {
fetch(`${process.env.LEAPFROGAI_API_BASE_URL}/leapfrogai/v1/auth/api-keys/${key.id}`, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.SERVICE_ROLE_KEY}`,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
})
Expand Down
24 changes: 24 additions & 0 deletions src/leapfrogai_ui/tests/helpers/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { deleteAllAssistants, deleteAssistantAvatars } from './assistantHelpers'
import { deleteAllTestThreadsWithApi } from './threadHelpers';
import type OpenAI from 'openai';
import { deleteAllTestAPIKeys } from './apiHelpers';
import { createClient } from '@supabase/supabase-js';
import { supabaseUsername } from '../constants';

export const cleanup = async (openAIClient: OpenAI) => {
deleteAllGeneratedFixtureFiles();
Expand All @@ -11,4 +13,26 @@ export const cleanup = async (openAIClient: OpenAI) => {
await deleteAllTestThreadsWithApi(openAIClient);
await deleteAssistantAvatars();
await deleteAllTestAPIKeys();
if (process.env.PUBLIC_DISABLE_KEYCLOAK === 'true') {
const supabase = createClient(process.env.PUBLIC_SUPABASE_URL!, process.env.SERVICE_ROLE_KEY!, {
auth: {
autoRefreshToken: false,
persistSession: false
}
});
const {
data: { users }
} = await supabase.auth.admin.listUsers();
let userId = '';
for (const user of users) {
if (user.email === supabaseUsername) {
userId = user.id;
}
}
if (userId) {
await supabase.from('profiles').delete().eq('id', userId);
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) console.error('Error deleting test user', error);
}
}
};
8 changes: 7 additions & 1 deletion src/leapfrogai_ui/tests/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(
process.env.PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_KEY!
process.env.SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
);

export const SHORT_RESPONSE_PROMPT = 'respond with no more than one sentence';
Expand Down

0 comments on commit 6d17187

Please sign in to comment.