add first multi-role e2e tests (#2417)
# What this PR does Lays ground work for #1586. Adds three new fixtures, `adminRolePage`, `editorRolePage`, and `viewerRolePage`. These fixtures can be easily accessed in a `test` context and allow the test to be run as a user authenticated with one of these Grafana basic roles. The bulk of the changes in the PR are to the "global setup" step. There is a bit of logic + communication with the Grafana instance's API, in order to create all the necessary authentication credentials. Lastly, adds the first basic role authorization test, asserting that Admin/Editors can view the list of OnCall users, whereas Viewers cannot. ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [ ] Documentation added (or `pr:no public docs` PR label added if not required) - [ ] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not required)
This commit is contained in:
parent
44d0252ef1
commit
f5495ed702
19 changed files with 351 additions and 113 deletions
8
.github/workflows/linting-and-tests.yml
vendored
8
.github/workflows/linting-and-tests.yml
vendored
|
|
@ -468,8 +468,12 @@ jobs:
|
|||
# hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped
|
||||
# to the OnCall API
|
||||
ONCALL_API_URL: http://172.17.0.1:30001
|
||||
GRAFANA_USERNAME: oncall
|
||||
GRAFANA_PASSWORD: oncall
|
||||
GRAFANA_ADMIN_USERNAME: oncall
|
||||
GRAFANA_ADMIN_PASSWORD: oncall
|
||||
GRAFANA_EDITOR_USERNAME: editor
|
||||
GRAFANA_EDITOR_PASSWORD: editor
|
||||
GRAFANA_VIEWER_USERNAME: viewer
|
||||
GRAFANA_VIEWER_PASSWORD: viewer
|
||||
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
|
||||
working-directory: ./grafana-plugin
|
||||
run: yarn test:integration
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ To run these tests locally simply do the following:
|
|||
|
||||
```bash
|
||||
npx playwright install # install playwright dependencies
|
||||
cp ./grafana-plugin/.env.example ./grafana-plugin/.env
|
||||
cp ./grafana-plugin/integration-tests/.env.example ./grafana-plugin/integration-tests/.env
|
||||
# you may need to tweak the values in ./grafana-plugin/.env according to your local setup
|
||||
cd grafana-plugin
|
||||
yarn test:integration
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# copy this file to ./.env and fill out the values according to your local setup
|
||||
|
||||
# for integration test purposes
|
||||
BASE_URL=http://localhost:3000
|
||||
ONCALL_API_URL=http://host.docker.internal:8080/
|
||||
GRAFANA_USERNAME=oncall
|
||||
GRAFANA_PASSWORD=oncall
|
||||
IS_OPEN_SOURCE=True
|
||||
1
grafana-plugin/integration-tests/.auth/.gitignore
vendored
Normal file
1
grafana-plugin/integration-tests/.auth/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.json
|
||||
9
grafana-plugin/integration-tests/.env.example
Normal file
9
grafana-plugin/integration-tests/.env.example
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
BASE_URL=http://localhost:3000
|
||||
ONCALL_API_URL=http://host.docker.internal:8080/
|
||||
GRAFANA_VIEWER_USERNAME=viewer
|
||||
GRAFANA_VIEWER_PASSWORD=viewer
|
||||
GRAFANA_EDITOR_USERNAME=editor
|
||||
GRAFANA_EDITOR_PASSWORD=editor
|
||||
GRAFANA_ADMIN_USERNAME=oncall
|
||||
GRAFANA_ADMIN_PASSWORD=oncall
|
||||
IS_OPEN_SOURCE=True
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
import { test } from '@playwright/test';
|
||||
import { test } from '../fixtures';
|
||||
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
|
||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('we can create an oncall schedule + receive an alert', async ({ page }) => {
|
||||
test('we can create an oncall schedule + receive an alert', async ({ adminRolePage }) => {
|
||||
// this test does a lot of stuff, lets give it adequate time to do its thing
|
||||
test.slow();
|
||||
|
||||
const { page, userName } = adminRolePage;
|
||||
const escalationChainName = generateRandomValue();
|
||||
const integrationName = generateRandomValue();
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
|
||||
await createOnCallSchedule(page, onCallScheduleName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
await createEscalationChain(
|
||||
page,
|
||||
escalationChainName,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { GRAFANA_USERNAME } from '../utils/constants';
|
||||
import { test, expect } from '../fixtures';
|
||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||
|
|
@ -7,14 +6,15 @@ import { waitForSms } from '../utils/phone';
|
|||
import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings';
|
||||
|
||||
// TODO: enable once we've signed up for a MailSlurp account to receieve SMSes
|
||||
test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => {
|
||||
test.skip('we can verify our phone number + receive an SMS alert', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
const escalationChainName = generateRandomValue();
|
||||
const integrationName = generateRandomValue();
|
||||
|
||||
await verifyUserPhoneNumber(page);
|
||||
await configureUserNotificationSettings(page, 'SMS');
|
||||
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, GRAFANA_USERNAME);
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, userName);
|
||||
await createIntegrationAndSendDemoAlert(page, integrationName, escalationChainName);
|
||||
|
||||
// wait for the SMS alert notification to arrive
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect, Page } from '@playwright/test';
|
||||
import { test, expect, Page } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createEscalationChain } from '../utils/escalationChain';
|
||||
|
||||
|
|
@ -16,7 +16,9 @@ const assertEscalationChainSearchWorks = async (
|
|||
};
|
||||
|
||||
// TODO: add tests for the new filtering. Commented out as this search doesn't exist anymore
|
||||
test.skip('searching allows case-insensitive partial matches', async ({ page }) => {
|
||||
test.skip('searching allows case-insensitive partial matches', async ({ adminRolePage }) => {
|
||||
const { page } = adminRolePage;
|
||||
|
||||
const escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`;
|
||||
const [firstHalf, secondHalf] = escalationChainName.split(' ');
|
||||
|
||||
|
|
|
|||
53
grafana-plugin/integration-tests/fixtures.ts
Normal file
53
grafana-plugin/integration-tests/fixtures.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { test as base, Page } from '@playwright/test';
|
||||
|
||||
import { GRAFANA_ADMIN_USERNAME, GRAFANA_EDITOR_USERNAME, GRAFANA_VIEWER_USERNAME } from './utils/constants';
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
|
||||
export class BaseRolePage {
|
||||
page: Page;
|
||||
userName: string;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
}
|
||||
|
||||
class ViewerRolePage extends BaseRolePage {
|
||||
userName = GRAFANA_VIEWER_USERNAME;
|
||||
}
|
||||
|
||||
class EditorRolePage extends BaseRolePage {
|
||||
userName = GRAFANA_EDITOR_USERNAME;
|
||||
}
|
||||
|
||||
class AdminRolePage extends BaseRolePage {
|
||||
userName = GRAFANA_ADMIN_USERNAME;
|
||||
}
|
||||
|
||||
type Fixtures = {
|
||||
viewerRolePage: ViewerRolePage;
|
||||
editorRolePage: EditorRolePage;
|
||||
adminRolePage: AdminRolePage;
|
||||
};
|
||||
|
||||
export * from '@playwright/test';
|
||||
export const test = base.extend<Fixtures>({
|
||||
viewerRolePage: async ({ browser }, use) => {
|
||||
const context = await browser.newContext({ storageState: VIEWER_USER_STORAGE_STATE });
|
||||
const page = new ViewerRolePage(await context.newPage());
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
editorRolePage: async ({ browser }, use) => {
|
||||
const context = await browser.newContext({ storageState: EDITOR_USER_STORAGE_STATE });
|
||||
const page = new EditorRolePage(await context.newPage());
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
adminRolePage: async ({ browser }, use) => {
|
||||
const context = await browser.newContext({ storageState: ADMIN_USER_STORAGE_STATE });
|
||||
const page = new AdminRolePage(await context.newPage());
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
});
|
||||
|
|
@ -1,37 +1,57 @@
|
|||
import { test as setup, chromium, FullConfig, expect, Page, BrowserContext, APIResponse } from '@playwright/test';
|
||||
import { test as setup, chromium, expect, Page, BrowserContext, FullConfig, APIRequestContext } from '@playwright/test';
|
||||
|
||||
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants';
|
||||
import GrafanaAPIClient from './utils/clients/grafana';
|
||||
import {
|
||||
GRAFANA_ADMIN_PASSWORD,
|
||||
GRAFANA_ADMIN_USERNAME,
|
||||
GRAFANA_EDITOR_PASSWORD,
|
||||
GRAFANA_EDITOR_USERNAME,
|
||||
GRAFANA_VIEWER_PASSWORD,
|
||||
GRAFANA_VIEWER_USERNAME,
|
||||
IS_CLOUD,
|
||||
IS_OPEN_SOURCE,
|
||||
ONCALL_API_URL,
|
||||
} from './utils/constants';
|
||||
import { clickButton, getInputByName } from './utils/forms';
|
||||
import { goToGrafanaPage } from './utils/navigation';
|
||||
import { STORAGE_STATE } from '../playwright.config';
|
||||
import { VIEWER_USER_STORAGE_STATE, EDITOR_USER_STORAGE_STATE, ADMIN_USER_STORAGE_STATE } from '../playwright.config';
|
||||
import { OrgRole } from '@grafana/data';
|
||||
|
||||
const IS_CLOUD = !IS_OPEN_SOURCE;
|
||||
const GLOBAL_SETUP_RETRIES = 3;
|
||||
const grafanaApiClient = new GrafanaAPIClient(GRAFANA_ADMIN_USERNAME, GRAFANA_ADMIN_PASSWORD);
|
||||
|
||||
const makeGrafanaLoginRequest = async (browserContext: BrowserContext): Promise<APIResponse> =>
|
||||
browserContext.request.post(`${BASE_URL}/login`, {
|
||||
data: {
|
||||
user: GRAFANA_USERNAME,
|
||||
password: GRAFANA_PASSWORD,
|
||||
},
|
||||
});
|
||||
type UserCreationSettings = {
|
||||
adminAuthedRequest: APIRequestContext;
|
||||
role: OrgRole;
|
||||
};
|
||||
|
||||
const pollGrafanaInstanceUntilItIsHealthy = async (browserContext: BrowserContext): Promise<boolean> => {
|
||||
console.log('Polling the grafana instance to make sure it is healthy');
|
||||
|
||||
const res = await makeGrafanaLoginRequest(browserContext);
|
||||
|
||||
if (!res.ok()) {
|
||||
console.log(`Grafana instance is unavailable. Got HTTP ${res.status()}. Will wait 5 seconds and then try again`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
return pollGrafanaInstanceUntilItIsHealthy(browserContext);
|
||||
const generateLoginStorageStateAndOptionallCreateUser = async (
|
||||
config: FullConfig,
|
||||
userName: string,
|
||||
password: string,
|
||||
storageStateFileLocation: string,
|
||||
userCreationSettings?: UserCreationSettings,
|
||||
closeContext = false
|
||||
): Promise<BrowserContext> => {
|
||||
if (userCreationSettings !== undefined && IS_OPEN_SOURCE) {
|
||||
const { adminAuthedRequest, role } = userCreationSettings;
|
||||
await grafanaApiClient.idempotentlyCreateUserWithRole(adminAuthedRequest, userName, password, role);
|
||||
}
|
||||
console.log('Grafana instance is available');
|
||||
return true;
|
||||
|
||||
const { headless } = config.projects[0]!.use;
|
||||
const browser = await chromium.launch({ headless, slowMo: headless ? 0 : 100 });
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
await grafanaApiClient.login(browserContext.request, userName, password);
|
||||
await browserContext.storageState({ path: storageStateFileLocation });
|
||||
|
||||
if (closeContext) {
|
||||
await browserContext.close();
|
||||
}
|
||||
return browserContext;
|
||||
};
|
||||
|
||||
/**
|
||||
* go to config page and wait for plugin icon to be available on left-hand navigation
|
||||
go to config page and wait for plugin icon to be available on left-hand navigation
|
||||
*/
|
||||
const configureOnCallPlugin = async (page: Page): Promise<void> => {
|
||||
/**
|
||||
|
|
@ -66,55 +86,7 @@ const configureOnCallPlugin = async (page: Page): Promise<void> => {
|
|||
* Borrowed from our friends on the Incident team
|
||||
* https://github.com/grafana/incident/blob/main/plugin/e2e/global-setup.ts
|
||||
*/
|
||||
const globalSetup = async (config: FullConfig): Promise<void> => {
|
||||
const { headless } = config.projects[0]!.use;
|
||||
const browser = await chromium.launch({ headless, slowMo: headless ? 0 : 100 });
|
||||
const browserContext = await browser.newContext();
|
||||
|
||||
if (IS_CLOUD) {
|
||||
/**
|
||||
* check that the grafana instance is available. If HTTP 503 is returned it means the
|
||||
* instance is currently unavailable. Poll until it is available
|
||||
*/
|
||||
await pollGrafanaInstanceUntilItIsHealthy(browserContext);
|
||||
}
|
||||
|
||||
const res = await makeGrafanaLoginRequest(browserContext);
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
await browserContext.storageState({ path: STORAGE_STATE });
|
||||
|
||||
// make sure the plugin has been configured
|
||||
const page = await browserContext.newPage();
|
||||
|
||||
if (IS_OPEN_SOURCE) {
|
||||
// plugin configuration can safely be skipped for cloud environments
|
||||
await configureOnCallPlugin(page);
|
||||
}
|
||||
|
||||
await browserContext.close();
|
||||
};
|
||||
|
||||
/**
|
||||
* Let's retry global setup, in the event that it fails due to an oncall-engine/oncall-celery backend error.
|
||||
* Sometimes the sync endpoint will randomly return HTTP 500.
|
||||
* See here for an example CI job which failed global setup
|
||||
* https://github.com/grafana/oncall/actions/runs/5062712137/jobs/9088529416#step:19:2536
|
||||
*
|
||||
* References on retrying playwright global setup
|
||||
* https://github.com/microsoft/playwright/discussions/11371
|
||||
*/
|
||||
const globalSetupWithRetries = async (config: FullConfig): Promise<void> => {
|
||||
for (let i = 0; i < GLOBAL_SETUP_RETRIES - 1; i++) {
|
||||
try {
|
||||
return await globalSetup(config);
|
||||
} catch (e) {}
|
||||
}
|
||||
// One last time, throwing an error if it fails.
|
||||
await globalSetup(config);
|
||||
};
|
||||
|
||||
setup('Configure Grafana OnCall plugin', async ({}, { config }) => {
|
||||
setup('Configure Grafana OnCall plugin', async ({ request }, { config }) => {
|
||||
/**
|
||||
* Unconditionally marks the setup as "slow", giving it triple the default timeout.
|
||||
* This is mostly useful for the rare case for Cloud Grafana instances where the instance may be down/unavailable
|
||||
|
|
@ -122,5 +94,47 @@ setup('Configure Grafana OnCall plugin', async ({}, { config }) => {
|
|||
*/
|
||||
setup.slow();
|
||||
|
||||
await globalSetupWithRetries(config);
|
||||
if (IS_CLOUD) {
|
||||
await grafanaApiClient.pollInstanceUntilItIsHealthy(request);
|
||||
}
|
||||
|
||||
const adminBrowserContext = await generateLoginStorageStateAndOptionallCreateUser(
|
||||
config,
|
||||
GRAFANA_ADMIN_USERNAME,
|
||||
GRAFANA_ADMIN_PASSWORD,
|
||||
ADMIN_USER_STORAGE_STATE
|
||||
);
|
||||
const adminPage = await adminBrowserContext.newPage();
|
||||
const { request: adminAuthedRequest } = adminBrowserContext;
|
||||
|
||||
await generateLoginStorageStateAndOptionallCreateUser(
|
||||
config,
|
||||
GRAFANA_EDITOR_USERNAME,
|
||||
GRAFANA_EDITOR_PASSWORD,
|
||||
EDITOR_USER_STORAGE_STATE,
|
||||
{
|
||||
adminAuthedRequest,
|
||||
role: OrgRole.Editor,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
await generateLoginStorageStateAndOptionallCreateUser(
|
||||
config,
|
||||
GRAFANA_VIEWER_USERNAME,
|
||||
GRAFANA_VIEWER_PASSWORD,
|
||||
VIEWER_USER_STORAGE_STATE,
|
||||
{
|
||||
adminAuthedRequest,
|
||||
role: OrgRole.Viewer,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
if (IS_OPEN_SOURCE) {
|
||||
// plugin configuration can safely be skipped for cloud environments
|
||||
await configureOnCallPlugin(adminPage);
|
||||
}
|
||||
|
||||
await adminBrowserContext.close();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from '../fixtures';
|
||||
import { openCreateIntegrationModal } from '../utils/integrations';
|
||||
|
||||
test('integrations have unique names', async ({ page }) => {
|
||||
test('integrations have unique names', async ({ adminRolePage }) => {
|
||||
const { page } = adminRolePage;
|
||||
await openCreateIntegrationModal(page);
|
||||
|
||||
const integrationNames = await page.getByTestId('integration-display-name').allInnerTexts();
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from '../fixtures';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
test('default dates in override creation modal are correct', async ({ page }) => {
|
||||
test('default dates in override creation modal are correct', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallSchedule(page, onCallScheduleName);
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
await clickButton({ page, buttonText: 'Add override' });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from '../fixtures';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
import { createOnCallSchedule } from '../utils/schedule';
|
||||
|
||||
test('check schedule quality for simple 1-user schedule', async ({ page }) => {
|
||||
test('check schedule quality for simple 1-user schedule', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
const onCallScheduleName = generateRandomValue();
|
||||
await createOnCallSchedule(page, onCallScheduleName);
|
||||
|
||||
await createOnCallSchedule(page, onCallScheduleName, userName);
|
||||
|
||||
/**
|
||||
* this page.reload() call is a hack to temporarily get around this issue
|
||||
|
|
|
|||
34
grafana-plugin/integration-tests/users/viewUsers.test.ts
Normal file
34
grafana-plugin/integration-tests/users/viewUsers.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { test, expect, Page } from '../fixtures';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
|
||||
test.describe('view list of users', () => {
|
||||
const testFlow = async (page: Page, isAllowedToView = true): Promise<void> => {
|
||||
await goToOnCallPage(page, 'users');
|
||||
|
||||
if (isAllowedToView) {
|
||||
const usersTableElement = page.getByTestId('users-table');
|
||||
await usersTableElement.waitFor({ state: 'visible' });
|
||||
|
||||
const userRowsContext = await usersTableElement.locator('tbody > tr').allTextContents();
|
||||
expect(userRowsContext.length).toBeGreaterThan(0);
|
||||
} else {
|
||||
const missingPermissionsMessageElement = page.getByTestId('view-users-missing-permission-message');
|
||||
await missingPermissionsMessageElement.waitFor({ state: 'visible' });
|
||||
|
||||
const missingPermissionMessage = await missingPermissionsMessageElement.textContent();
|
||||
expect(missingPermissionMessage).toMatch(/You are missing the .* to be able to view OnCall users/);
|
||||
}
|
||||
};
|
||||
|
||||
test('admin is allowed to', async ({ adminRolePage }) => {
|
||||
await testFlow(adminRolePage.page);
|
||||
});
|
||||
|
||||
test('editor is allowed to', async ({ editorRolePage }) => {
|
||||
await testFlow(editorRolePage.page);
|
||||
});
|
||||
|
||||
test('viewer is not allowed to', async ({ viewerRolePage }) => {
|
||||
await testFlow(viewerRolePage.page, false);
|
||||
});
|
||||
});
|
||||
116
grafana-plugin/integration-tests/utils/clients/grafana.ts
Normal file
116
grafana-plugin/integration-tests/utils/clients/grafana.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import { OrgRole } from '@grafana/data';
|
||||
import { expect, APIRequestContext } from '@playwright/test';
|
||||
|
||||
import { BASE_URL } from '../constants';
|
||||
|
||||
type UsersLookupResponse = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
type CreateUserResponse = {
|
||||
id: number;
|
||||
};
|
||||
|
||||
class GrafanaApiException extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export default class GrafanaAPIClient {
|
||||
userName: string;
|
||||
password: string;
|
||||
|
||||
constructor(userName: string, password: string) {
|
||||
this.userName = userName;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
get requestHeaders() {
|
||||
const base64encodedCredentials = Buffer.from(`${this.userName}:${this.password}`).toString('base64');
|
||||
return {
|
||||
Authorization: `Basic ${base64encodedCredentials}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* check that the grafana instance is available. If HTTP 503 is returned it means the
|
||||
* instance is currently unavailable. Poll until it is available
|
||||
*/
|
||||
pollInstanceUntilItIsHealthy = async (request: APIRequestContext): Promise<boolean> => {
|
||||
console.log('Polling the grafana instance to make sure it is healthy');
|
||||
|
||||
const res = await request.get(`${BASE_URL}/api/health`);
|
||||
|
||||
if (!res.ok()) {
|
||||
console.log(`Grafana instance is unavailable. Got HTTP ${res.status()}. Will wait 5 seconds and then try again`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
return this.pollInstanceUntilItIsHealthy(request);
|
||||
}
|
||||
console.log('Grafana instance is available');
|
||||
return true;
|
||||
};
|
||||
|
||||
getUserIdByUsername = async (request: APIRequestContext, userName: string): Promise<number> => {
|
||||
const res = await request.get(`${BASE_URL}/api/users/lookup?loginOrEmail=${userName}`, {
|
||||
headers: this.requestHeaders,
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
const responseData: UsersLookupResponse = await res.json();
|
||||
return responseData.id;
|
||||
};
|
||||
|
||||
updateUserRole = async (request: APIRequestContext, userId: number, role: OrgRole): Promise<void> => {
|
||||
const res = await request.patch(`${BASE_URL}/api/org/users/${userId}`, {
|
||||
data: { role },
|
||||
headers: this.requestHeaders,
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
};
|
||||
|
||||
/**
|
||||
* Should return one of the following two responses:
|
||||
* - HTTP 200 - user successfully created
|
||||
* - HTTP 412 - user w/ this username already exists (fine to ignore this)
|
||||
*/
|
||||
idempotentlyCreateUserWithRole = async (
|
||||
request: APIRequestContext,
|
||||
userName: string,
|
||||
password: string,
|
||||
role: OrgRole
|
||||
) => {
|
||||
const res = await request.post(`${BASE_URL}/api/admin/users`, {
|
||||
data: {
|
||||
name: `e2e user - ${userName}`,
|
||||
login: userName,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
let userId: number;
|
||||
const responseCode = res.status();
|
||||
|
||||
if (responseCode === 200) {
|
||||
// user was just created
|
||||
const respJson: CreateUserResponse = await res.json();
|
||||
userId = respJson.id;
|
||||
} else if (responseCode == 412) {
|
||||
// user already exists, go fetch their user id
|
||||
userId = await this.getUserIdByUsername(request, userName);
|
||||
} else {
|
||||
throw new GrafanaApiException(
|
||||
`Received unexpected status code while trying to idempotently create user - HTTP${responseCode}: ${await res.body()}`
|
||||
);
|
||||
}
|
||||
|
||||
await this.updateUserRole(request, userId, role);
|
||||
};
|
||||
|
||||
login = async (request: APIRequestContext, userName: string, password: string) => {
|
||||
const res = await request.post(`${BASE_URL}/login`, {
|
||||
data: { user: userName, password },
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,13 @@
|
|||
export const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
||||
export const ONCALL_API_URL = process.env.ONCALL_API_URL || 'http://host.docker.internal:8080';
|
||||
export const GRAFANA_USERNAME = process.env.GRAFANA_USERNAME || 'oncall';
|
||||
export const GRAFANA_PASSWORD = process.env.GRAFANA_PASSWORD || 'oncall';
|
||||
export const MAILSLURP_API_KEY = process.env.MAILSLURP_API_KEY;
|
||||
|
||||
export const GRAFANA_VIEWER_USERNAME = process.env.GRAFANA_VIEWER_USERNAME || 'viewer';
|
||||
export const GRAFANA_VIEWER_PASSWORD = process.env.GRAFANA_VIEWER_PASSWORD || 'viewer';
|
||||
export const GRAFANA_EDITOR_USERNAME = process.env.GRAFANA_EDITOR_USERNAME || 'editor';
|
||||
export const GRAFANA_EDITOR_PASSWORD = process.env.GRAFANA_EDITOR_PASSWORD || 'editor';
|
||||
export const GRAFANA_ADMIN_USERNAME = process.env.GRAFANA_ADMIN_USERNAME || 'oncall';
|
||||
export const GRAFANA_ADMIN_PASSWORD = process.env.GRAFANA_ADMIN_PASSWORD || 'oncall';
|
||||
|
||||
export const IS_OPEN_SOURCE = (process.env.IS_OPEN_SOURCE || 'true').toLowerCase() === 'true';
|
||||
export const IS_CLOUD = !IS_OPEN_SOURCE;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Page } from '@playwright/test';
|
||||
import { GRAFANA_USERNAME } from './constants';
|
||||
import { clickButton, fillInInput, selectDropdownValue, selectValuePickerValue } from './forms';
|
||||
import { clickButton, fillInInput, selectDropdownValue } from './forms';
|
||||
import { goToOnCallPage } from './navigation';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const createOnCallSchedule = async (page: Page, scheduleName: string): Promise<void> => {
|
||||
export const createOnCallSchedule = async (page: Page, scheduleName: string, userName: string): Promise<void> => {
|
||||
// go to the schedules page
|
||||
await goToOnCallPage(page, 'schedules');
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ export const createOnCallSchedule = async (page: Page, scheduleName: string): Pr
|
|||
page,
|
||||
selectType: 'grafanaSelect',
|
||||
placeholderText: 'Add user',
|
||||
value: GRAFANA_USERNAME,
|
||||
value: userName,
|
||||
});
|
||||
|
||||
await clickButton({ page, buttonText: 'Create' });
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import { devices } from '@playwright/test';
|
|||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
require('dotenv').config();
|
||||
require('dotenv').config({ path: path.resolve(process.cwd(), 'integration-tests/.env') });
|
||||
|
||||
export const STORAGE_STATE = path.join(__dirname, 'integration-tests/storageState.json');
|
||||
export const VIEWER_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/viewer.json');
|
||||
export const EDITOR_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/editor.json');
|
||||
export const ADMIN_USER_STORAGE_STATE = path.join(__dirname, 'integration-tests/.auth/admin.json');
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
|
|
@ -62,7 +64,6 @@ const config: PlaywrightTestConfig = {
|
|||
name: 'chromium',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
storageState: STORAGE_STATE,
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
|
|
@ -70,7 +71,6 @@ const config: PlaywrightTestConfig = {
|
|||
name: 'firefox',
|
||||
use: {
|
||||
...devices['Desktop Firefox'],
|
||||
storageState: STORAGE_STATE,
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
|
|
@ -78,7 +78,6 @@ const config: PlaywrightTestConfig = {
|
|||
name: 'webkit',
|
||||
use: {
|
||||
...devices['Desktop Safari'],
|
||||
storageState: STORAGE_STATE,
|
||||
},
|
||||
dependencies: ['setup'],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
</div>
|
||||
|
||||
<GTable
|
||||
data-testid="users-table"
|
||||
emptyText={initialUsersLoaded ? 'No users found' : 'Loading...'}
|
||||
rowKey="pk"
|
||||
data={results}
|
||||
|
|
@ -246,6 +247,7 @@ class Users extends React.Component<UsersProps, UsersState> {
|
|||
profile
|
||||
</>
|
||||
}
|
||||
data-testid="view-users-missing-permission-message"
|
||||
severity="info"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue