Skip to content

Commit 930f0f7

Browse files
author
Shay
committed
Merge branch '284-spike-vervolg-ticket-apiclient-tool-bouwen' into 'main'
Resolve "[SPIKE] Vervolg ticket: ApiClient tool bouwen" See merge request elgentos/magento2-playwright!49
2 parents 486e641 + 0535595 commit 930f0f7

File tree

2 files changed

+168
-13
lines changed

2 files changed

+168
-13
lines changed

tests/setup.spec.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-check
22

3-
import { test as base } from '@playwright/test';
3+
import { test } from '@playwright/test';
44
import { inputValues } from '@config';
55
import { requireEnv } from '@utils/env.utils';
66
import { createLogger } from '@utils/logger';
@@ -13,14 +13,14 @@ const logger = createLogger('Setup');
1313
const magentoAdminUsername = requireEnv('MAGENTO_ADMIN_USERNAME');
1414
const magentoAdminPassword = requireEnv('MAGENTO_ADMIN_PASSWORD');
1515

16-
base.beforeEach(async ({ page }, testInfo) => {
16+
test.beforeEach(async ({ page }, testInfo) => {
1717
const magentoAdminPage = new MagentoAdminPage(page);
1818
await magentoAdminPage.login(magentoAdminUsername, magentoAdminPassword);
1919
});
2020

21-
base.describe('Setting up the testing environment', () => {
21+
test.describe('Setting up the testing environment', () => {
2222
// Set tests to serial mode to ensure the order is followed.
23-
base.describe.configure({mode:'serial'});
23+
test.describe.configure({mode:'serial'});
2424

2525
/**
2626
* @feature Magento Admin Configuration (disable login CAPTCHA)
@@ -33,14 +33,13 @@ base.describe('Setting up the testing environment', () => {
3333
* @but if the browser is not Chromium
3434
* @then the test is skipped with an appropriate message
3535
*/
36-
base('Disable_login_captcha', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
37-
base.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
36+
test('Disable_login_captcha', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
37+
test.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
3838

3939
const magentoAdminPage = new MagentoAdminPage(page);
4040
await magentoAdminPage.disableLoginCaptcha();
4141
});
4242

43-
4443
/**
4544
* @feature Magento Admin Configuration (Enable multiple admin logins)
4645
* @scenario Enable multiple admin logins only in Chromium browser
@@ -54,13 +53,12 @@ base.describe('Setting up the testing environment', () => {
5453
* @but if the browser is not Chromium
5554
* @then the test is skipped with an appropriate message
5655
*/
57-
base('Enable_multiple_admin_logins', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
58-
base.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
56+
test('Enable_multiple_admin_logins', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
57+
test.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
5958

6059
const magentoAdminPage = new MagentoAdminPage(page);
6160
await magentoAdminPage.enableMultipleAdminLogins();
6261
});
63-
6462

6563
/**
6664
* @feature Cart Price Rules Configuration
@@ -70,7 +68,7 @@ base.describe('Setting up the testing environment', () => {
7068
* @and the admin creates a new cart price rule with the specified coupon code
7169
* @then the coupon code is successfully saved and available for use
7270
*/
73-
base('Set_up_coupon_codes', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
71+
test('Set_up_coupon_codes', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
7472
const magentoAdminPage = new MagentoAdminPage(page);
7573
const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
7674
const couponCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
@@ -87,14 +85,14 @@ base.describe('Setting up the testing environment', () => {
8785
* @and submits the registration form with first name, last name, email, and password
8886
* @then a new customer account is successfully created for testing purposes
8987
*/
90-
base('Create_test_accounts', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
88+
test('Create_test_accounts', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
9189
const magentoAdminPage = new MagentoAdminPage(page);
9290
const registerPage = new RegisterPage(page);
9391
const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
9492
const accountEmail = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
9593
const accountPassword = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
9694

97-
await base.step(`Check if ${accountEmail} is already registered`, async () => {
95+
await test.step(`Check if ${accountEmail} is already registered`, async () => {
9896
const customerLookUp = await magentoAdminPage.checkIfCustomerExists(accountEmail);
9997
if(customerLookUp){
10098
testInfo.skip(true, `${accountEmail} was found in user table, this step is skipped. If you think this is incorrect, consider removing user from the table and try running the setup again.`);

tests/utils/apiClient.utils.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// @ts-check
2+
3+
import { request, expect, APIRequestContext, APIResponse } from '@playwright/test';
4+
import { requireEnv } from '@utils/env.utils';
5+
6+
class ApiClient {
7+
private context!: APIRequestContext;
8+
private token: string | undefined;
9+
private tokenExpiry: number | undefined;
10+
11+
constructor() {}
12+
13+
/**
14+
* Initializes the ApiClient by ensuring a valid token and setting up the request context.
15+
* @returns {Promise<ApiClient>} A Promise that resolves to an instance of ApiClient.
16+
*/
17+
async create(): Promise<ApiClient> {
18+
await this.ensureToken();
19+
20+
this.context = await request.newContext({
21+
baseURL: requireEnv('PLAYWRIGHT_BASE_URL'),
22+
extraHTTPHeaders: {
23+
'Content-Type': 'application/json',
24+
Authorization: `Bearer ${this.token}`,
25+
},
26+
});
27+
28+
return this;
29+
}
30+
31+
/**
32+
* Ensures the API token is valid, refreshing it if expired or absent.
33+
* @private
34+
* @returns {Promise<void>}
35+
*/
36+
private async ensureToken(): Promise<void> {
37+
if (!this.token || this.isTokenExpired()) {
38+
this.token = await this.refreshIntegrationToken();
39+
}
40+
}
41+
42+
/**
43+
* Fetches a new API token from the server and sets the expiry time.
44+
* @private
45+
* @returns {Promise<string>} A Promise that resolves to the token string.
46+
* @throws {Error} If token retrieval fails.
47+
*/
48+
private async refreshIntegrationToken(): Promise<string> {
49+
const tempContext = await request.newContext({
50+
baseURL: requireEnv('PLAYWRIGHT_BASE_URL'),
51+
extraHTTPHeaders: {
52+
'Content-Type': 'application/json',
53+
},
54+
});
55+
56+
const response = await tempContext.post('/rest/V1/integration/admin/token', {
57+
data: {
58+
username: requireEnv('MAGENTO_API_USERNAME'),
59+
password: requireEnv('MAGENTO_API_PASSWORD'),
60+
},
61+
});
62+
63+
if (!response.ok()) {
64+
const errorBody = await response.text();
65+
await tempContext.dispose();
66+
throw new Error(`Failed to obtain integration token: ${response.status()} ${errorBody}`);
67+
}
68+
69+
const token = await response.json();
70+
const expiresHeader = response.headers()['expires'];
71+
if (expiresHeader) {
72+
this.tokenExpiry = new Date(expiresHeader).getTime();
73+
} else {
74+
this.tokenExpiry = Date.now() + (3600 * 1000);
75+
}
76+
77+
await tempContext.dispose();
78+
return token;
79+
}
80+
81+
/**
82+
* Determines if the current token is expired.
83+
* @private
84+
* @returns {boolean} True if the token is expired, otherwise false.
85+
*/
86+
private isTokenExpired(): boolean {
87+
return !this.tokenExpiry || Date.now() >= this.tokenExpiry;
88+
}
89+
90+
/**
91+
* Performs a GET request to the specified URL.
92+
* @param {string} url The endpoint URL to send the request to.
93+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
94+
*/
95+
async get(url: string): Promise<any> {
96+
const response = await this.context.get(url);
97+
return this.handleResponse(response);
98+
}
99+
100+
/**
101+
* Performs a POST request with the given payload to the specified URL.
102+
* @param {string} url The endpoint URL to send the request to.
103+
* @param {Record<string, unknown>} payload The data payload to send with the request.
104+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
105+
* @throws {Error} If the response indicates failure.
106+
*/
107+
async post(url: string, payload: Record<string, unknown>): Promise<any> {
108+
const response = await this.context.post(url, { data: payload });
109+
return this.handleResponse(response);
110+
}
111+
112+
/**
113+
* Performs a PUT request with the given payload to the specified URL.
114+
* @param {string} url The endpoint URL to send the request to.
115+
* @param {Record<string, unknown>} payload The data payload to send with the request.
116+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
117+
* @throws {Error} If the response indicates failure.
118+
*/
119+
async put(url: string, payload: Record<string, unknown>): Promise<any> {
120+
const response = await this.context.put(url, { data: payload });
121+
return this.handleResponse(response);
122+
}
123+
124+
/**
125+
* Performs a DELETE request to the specified URL.
126+
* @param {string} url The endpoint URL to send the request to.
127+
* @returns {Promise<void>} A Promise indicating successful deletion.
128+
*/
129+
async delete(url: string): Promise<void> {
130+
const response = await this.context.delete(url);
131+
return this.handleResponse(response);
132+
}
133+
134+
/**
135+
* Handles an API response, checking for success and parsing the JSON body.
136+
* @param {APIResponse} response The response object to handle.
137+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
138+
* @throws {Error} If the response is not successful.
139+
*/
140+
async handleResponse(response: APIResponse): Promise<any> {
141+
if (!response.ok()) {
142+
const body = await response.text();
143+
throw new Error(`API call failed [${response.status()}]: ${body}`);
144+
}
145+
return await response.json();
146+
}
147+
148+
/**
149+
* Disposes of the current request context.
150+
* @returns {Promise<void>}
151+
*/
152+
async dispose(): Promise<void> {
153+
await this.context.dispose();
154+
}
155+
}
156+
157+
export default ApiClient;

0 commit comments

Comments
 (0)