Skip to content

Commit

Permalink
Plugin E2E: Interact with Grafana http api on behalf of logged in user (
Browse files Browse the repository at this point in the history
  • Loading branch information
sunker authored Jun 24, 2024
1 parent 207825b commit 85d81fc
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 72 deletions.
5 changes: 2 additions & 3 deletions packages/plugin-e2e/src/auth/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { test as setup } from '../';
import { DEFAULT_ADMIN_USER } from '../options';

setup('authenticate', async ({ login, createUser, user }) => {
if (user && (user.user !== DEFAULT_ADMIN_USER.user || user.password !== DEFAULT_ADMIN_USER.password)) {
setup('authenticate', async ({ login, createUser, user, grafanaAPICredentials }) => {
if (user && (user.user !== grafanaAPICredentials.user || user.password !== grafanaAPICredentials.password)) {
await createUser();
}
await login();
Expand Down
56 changes: 4 additions & 52 deletions packages/plugin-e2e/src/fixtures/commands/createUser.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,13 @@
import { APIRequestContext, expect, TestFixture } from '@playwright/test';
import { PlaywrightArgs, User } from '../../types';
import { TestFixture } from '@playwright/test';
import { PlaywrightArgs } from '../../types';

type CreateUserFixture = TestFixture<() => Promise<void>, PlaywrightArgs>;

const getHeaders = (user: User) => ({
Authorization: `Basic ${Buffer.from(`${user.user}:${user.password}`).toString('base64')}`,
});

const getUserIdByUsername = async (
request: APIRequestContext,
userName: string,
grafanaAPIUser: User
): Promise<number> => {
const getUserIdByUserNameReq = await request.get(`/api/users/lookup?loginOrEmail=${userName}`, {
headers: getHeaders(grafanaAPIUser),
});
expect(getUserIdByUserNameReq.ok()).toBeTruthy();
const json = await getUserIdByUserNameReq.json();
return json.id;
};

export const createUser: CreateUserFixture = async ({ request, user, grafanaAPICredentials }, use) => {
export const createUser: CreateUserFixture = async ({ grafanaAPIClient, user }, use) => {
await use(async () => {
if (!user) {
throw new Error('Playwright option `User` was not provided');
}

const createUserReq = await request.post(`/api/admin/users`, {
data: {
name: user?.user,
login: user?.user,
password: user?.password,
},
headers: getHeaders(grafanaAPICredentials),
});

let userId: number | undefined;
if (createUserReq.ok()) {
const respJson = await createUserReq.json();
userId = respJson.id;
} else if (createUserReq.status() === 412) {
// user already exists
userId = await getUserIdByUsername(request, user?.user, grafanaAPICredentials);
} else {
throw new Error(`Could not create user '${user?.user}': ${await createUserReq.text()}`);
}

if (user.role) {
const updateRoleReq = await request.patch(`/api/org/users/${userId}`, {
data: { role: user.role },
headers: getHeaders(grafanaAPICredentials),
});
const updateRoleReqText = await updateRoleReq.text();
expect(
updateRoleReq.ok(),
`Could not assign role '${user.role}' to user '${user.user}': ${updateRoleReqText}`
).toBeTruthy();
}
await grafanaAPIClient.createUser(user);
});
};
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
import { TestFixture } from '@playwright/test';
import { DataSourceSettings, PlaywrightArgs } from '../../types';
import { PlaywrightArgs } from '../../types';
import { DataSourceConfigPage } from '../../models/pages/DataSourceConfigPage';

type GotoDataSourceConfigPageFixture = TestFixture<(uid: string) => Promise<DataSourceConfigPage>, PlaywrightArgs>;

export const gotoDataSourceConfigPage: GotoDataSourceConfigPageFixture = async (
{ request, page, selectors, grafanaVersion, grafanaAPICredentials },
{ request, page, selectors, grafanaVersion, grafanaAPIClient },
use,
testInfo
) => {
await use(async (uid) => {
const response = await request.get(`/api/datasources/uid/${uid}`, {
headers: {
Authorization: `Basic ${Buffer.from(`${grafanaAPICredentials.user}:${grafanaAPICredentials.password}`).toString(
'base64'
)}`,
},
});
if (!response.ok()) {
throw new Error(
`Failed to get datasource by uid: ${response.statusText()}. If you're using a provisioned data source, make sure it has a UID`
);
}
const settings: DataSourceSettings = await response.json();
const settings = await grafanaAPIClient.getDataSourceSettingsByUID(uid);
const dataSourceConfigPage = new DataSourceConfigPage(
{ page, selectors, grafanaVersion, request, testInfo },
settings
Expand Down
6 changes: 4 additions & 2 deletions packages/plugin-e2e/src/fixtures/commands/login.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'path';
import { expect, TestFixture } from '@playwright/test';
import { TestFixture } from '@playwright/test';
import { PlaywrightArgs } from '../../types';

type LoginFixture = TestFixture<() => Promise<void>, PlaywrightArgs>;
Expand All @@ -8,7 +8,9 @@ export const login: LoginFixture = async ({ request, user }, use) => {
await use(async () => {
const loginReq = await request.post('/login', { data: user });
const text = await loginReq.text();
expect.soft(loginReq.ok(), `Could not log in to Grafana: ${text}`).toBeTruthy();
if (!loginReq.ok()) {
throw new Error(`Could not login to Grafana using user '${user?.user}': ${text}`);
}
await request.storageState({ path: path.join(process.cwd(), `playwright/.auth/${user?.user}.json`) });
});
};
29 changes: 29 additions & 0 deletions packages/plugin-e2e/src/fixtures/grafanaAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import path from 'path';
import { TestFixture, expect, APIRequestContext } from '@playwright/test';
import { PlaywrightArgs, User } from '../types';
import { GrafanaAPIClient } from '../models/GrafanaAPIClient';

type GrafanaAPIClientFixture = TestFixture<GrafanaAPIClient, PlaywrightArgs>;

const adminClientStorageState = path.join(process.cwd(), `playwright/.auth/grafanaAPICredentials.json`);

export const createAdminClientStorageState = async (request: APIRequestContext, grafanaAPICredentials: User) => {
const loginReq = await request.post('/login', { data: grafanaAPICredentials });
const text = await loginReq.text();
await expect.soft(loginReq.ok(), `Could not log in to Grafana: ${text}`).toBeTruthy();
await request.storageState({ path: adminClientStorageState });
};

export const grafanaAPIClient: GrafanaAPIClientFixture = async ({ browser, grafanaAPICredentials }, use) => {
const context = await browser.newContext({ storageState: undefined });
const loginReq = await context.request.post('/login', { data: grafanaAPICredentials });
if (!loginReq.ok()) {
console.log(
`Could not login to grafana using credentials '${
grafanaAPICredentials?.user
}'. Find information on how user can be managed in the plugin-e2e docs: https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication#managing-users : ${await loginReq.text()}`
);
}
await context.request.storageState({ path: adminClientStorageState });
await use(new GrafanaAPIClient(context.request));
};
2 changes: 2 additions & 0 deletions packages/plugin-e2e/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { test as base, expect as baseExpect } from '@playwright/test';

import { AlertPageOptions, AlertVariant, ContainTextOptions, PluginFixture, PluginOptions } from './types';
import { annotationEditPage } from './fixtures/annotationEditPage';
import { grafanaAPIClient } from './fixtures/grafanaAPIClient';
import { createDataSource } from './fixtures/commands/createDataSource';
import { createDataSourceConfigPage } from './fixtures/commands/createDataSourceConfigPage';
import { createUser } from './fixtures/commands/createUser';
Expand Down Expand Up @@ -63,6 +64,7 @@ export const test = base.extend<PluginFixture, PluginOptions>({
selectors: e2eSelectors,
grafanaVersion,
login,
grafanaAPIClient,
createDataSourceConfigPage,
page,
dashboardPage,
Expand Down
56 changes: 56 additions & 0 deletions packages/plugin-e2e/src/models/GrafanaAPIClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { APIRequestContext } from '@playwright/test';
import { DataSourceSettings, User } from '../types';

export class GrafanaAPIClient {
constructor(private request: APIRequestContext) {}

async getUserIdByUsername(userName: string) {
const getUserIdByUserNameReq = await this.request.get(`/api/users/lookup?loginOrEmail=${userName}`);
const json = await getUserIdByUserNameReq.json();
return json.id;
}

async createUser(user: User) {
const createUserReq = await this.request.post(`/api/admin/users`, {
data: {
name: user?.user,
login: user?.user,
password: user?.password,
},
});
let userId: number | undefined;
if (createUserReq.ok()) {
const respJson = await createUserReq.json();
userId = respJson.id;
} else if (createUserReq.status() === 412) {
// user already exists
userId = await this.getUserIdByUsername(user?.user);
} else {
throw new Error(
`Could not create user '${
user?.user
}'. Find information on how user can be managed in the plugin-e2e docs: https://grafana.com/developers/plugin-tools/e2e-test-a-plugin/use-authentication#managing-users : ${await createUserReq.text()}`
);
}

if (user.role) {
const updateRoleReq = await this.request.patch(`/api/org/users/${userId}`, {
data: { role: user.role },
});
if (!updateRoleReq.ok()) {
throw new Error(`Could not assign role '${user.role}' to user '${user.user}': ${await updateRoleReq.text()}`);
}
}
}

async getDataSourceSettingsByUID(uid: string) {
const response = await this.request.get(`/api/datasources/uid/${uid}`);
if (!response.ok()) {
throw new Error(
`Failed to get datasource by uid: ${response.statusText()}. If you're using a provisioned data source, make sure it has a UID`
);
}
const settings: DataSourceSettings = await response.json();
return settings;
}
}
14 changes: 14 additions & 0 deletions packages/plugin-e2e/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PanelEditPage } from './models/pages/PanelEditPage';
import { VariableEditPage } from './models/pages/VariableEditPage';
import { VariablePage } from './models/pages/VariablePage';
import { AlertRuleEditPage } from './models/pages/AlertRuleEditPage';
import { GrafanaAPIClient } from './models/GrafanaAPIClient';

export type PluginOptions = {
/**
Expand Down Expand Up @@ -193,6 +194,19 @@ export type PluginFixture = {
*/
isFeatureToggleEnabled<T = object>(featureToggle: keyof T): Promise<boolean>;

/**
* Client that allows you to use certain endpoints in the Grafana http API.
*
The GrafanaAPIClient doesn't call the Grafana HTTP API on behalf of the logged in user -
* it uses the {@link types.grafanaAPICredentials} credentials. grafanaAPICredentials defaults to admin:admin, but you may override this
* by specifying grafanaAPICredentials in the playwright config options.
*
* Note that storage state for the admin client is not persisted throughout the test suite. For every test where the grafanaAPICredentials fixtures is used,
* new storage state is created.
*/

grafanaAPIClient: GrafanaAPIClient;

/**
* Isolated {@link DashboardPage} instance for each test.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from '../../../src';

test.use({ grafanaAPICredentials: { user: 'test', password: 'test' } });
test('should throw error when credentials are invalid', async ({ grafanaAPIClient }) => {
await expect(grafanaAPIClient.createUser({ user: 'testuser1', password: 'pass' })).rejects.toThrowError(
/Could not create user 'testuser1'.*/
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, test } from '../../../src';

test('should be possible to create user when credentials are valid', async ({ grafanaAPIClient }) => {
await expect(grafanaAPIClient.createUser({ user: 'testuser1', password: 'pass' })).resolves.toBeUndefined();
await expect(await grafanaAPIClient.getUserIdByUsername('testuser1')).toBeTruthy();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { expect, test } from '../../../src';

test.use({ grafanaAPICredentials: { user: 'test', password: 'test' } });
test('should throw error when credentials are invalid', async ({ grafanaAPIClient }) => {
await expect(grafanaAPIClient.createUser({ user: 'testuser1', password: 'pass' })).rejects.toThrowError(
/Could not create user 'testuser1'.*/
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from '../../../src';

test('using grafanaAPIClient should not change storage state for logged in user', async ({
grafanaAPIClient,
page,
}) => {
// assert one can create user on behalf of the admin credentials
await expect(grafanaAPIClient.createUser({ user: 'testuser1', password: 'pass' })).resolves.toBeUndefined();
await expect(await grafanaAPIClient.getUserIdByUsername('testuser1')).toBeTruthy();

// but logged in user should still only have viewer persmissions
await page.goto('/');
const homePageTitle = await page.title();
await page.goto('/datasources', { waitUntil: 'networkidle' });
expect(await page.title()).toEqual(homePageTitle);
});

0 comments on commit 85d81fc

Please sign in to comment.