Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin E2E: Interact with Grafana http api on behalf of logged in user #965

Merged
merged 7 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
Loading