Skip to content

Commit 92f9aad

Browse files
committed
Merge remote-tracking branch 'origin/O11y-Rerun' into O11y-Rerun-Test
# Conflicts: # setup-env/config/constants.js # setup-env/dist/index.js
2 parents 93504fe + c0f7ff9 commit 92f9aad

File tree

8 files changed

+266
-47
lines changed

8 files changed

+266
-47
lines changed

setup-env/config/constants.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
BROWSERSTACK_PROJECT_NAME: 'BROWSERSTACK_PROJECT_NAME',
1616
},
1717

18-
BROWSERSTACK_TEMPLATE: {
18+
BROWSERSTACK_INTEGRATIONS: {
1919
// DETAILS_API_URL: 'https://integrate.browserstack.com/api/ci-tools/v1/builds/{runId}/rebuild/details?tool=github-actions&as_bot=true',
2020
DETAILS_API_URL: 'https://8c12-2405-201-6806-d09b-4d8a-335-3b92-fcbe.ngrok-free.app/api/ci-tools/v1/builds/{runId}/rebuild/details?tool=github-actions&as_bot=true',
2121
},

setup-env/src/actionInput/index.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const core = require('@actions/core');
22
const axios = require('axios');
33
const InputValidator = require('./inputValidator');
44
const constants = require('../../config/constants');
5-
const { BROWSERSTACK_TEMPLATE } = require("../../config/constants");
5+
const { BROWSERSTACK_INTEGRATIONS } = require("../../config/constants");
66

77
const {
88
INPUT,
@@ -82,7 +82,6 @@ class ActionInput {
8282

8383
async checkIfBStackReRun() {
8484
// Using !! ensures that the function returns true or false, regardless of the input values.
85-
core.info(`The variables set are: rerunAttempt - ${this.rerunAttempt}, runId - ${this.runId}, repository - ${this.repository}, githubToken - ${this.githubToken}`);
8685
if (!this.rerunAttempt || !this.rerunAttempt > 1) {
8786
return false;
8887
}
@@ -116,7 +115,7 @@ class ActionInput {
116115
// Check if the run was triggered by the BrowserStack rerun bot
117116
core.info('The re-run was triggered by the GitHub App from BrowserStack.');
118117

119-
const browserStackApiUrl = BROWSERSTACK_TEMPLATE.DETAILS_API_URL.replace('{runId}', this.runId);
118+
const browserStackApiUrl = BROWSERSTACK_INTEGRATIONS.DETAILS_API_URL.replace('{runId}', this.runId);
120119

121120
// Call BrowserStack API to get the tests to rerun
122121
const bsApiResponse = await axios.get(browserStackApiUrl, {

setup-env/src/actionInput/inputValidator.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,30 +123,42 @@ class InputValidator {
123123
* @throws {Error} If the input is not a valid non-empty string
124124
*/
125125
static validateGithubToken(githubToken) {
126-
if (githubToken && githubToken.toLowerCase() !== 'none') {
127-
if (typeof githubToken === 'string' && githubToken.trim().length > 0) {
128-
return githubToken;
129-
}
126+
if (typeof githubToken !== 'string') {
130127
throw new Error("Invalid input for 'github-token'. Must be a valid non-empty string.");
131128
}
132-
return 'none';
129+
130+
if (githubToken.toLowerCase() === 'none') {
131+
return 'none';
132+
}
133+
134+
if (githubToken.trim().length > 0) {
135+
return githubToken;
136+
}
137+
138+
throw new Error("Invalid input for 'github-token'. Must be a valid non-empty string.");
133139
}
134140

135141
/**
136142
* Validates the app name input to ensure it is a valid non-empty string.
137-
* If the input is 'none' or not provided, it returns 'none'.
138-
* @param {string} githubAppName Input for 'repository'
139-
* @returns {string} Validated app name, or 'none' if input is 'none' or invalid
143+
* If the input is 'none' or not provided, it returns 'browserstack[bot]'.
144+
* @param {string} githubAppName Input for 'github-app'
145+
* @returns {string} Validated app name, or 'browserstack[bot]' if input is 'none' or invalid
140146
* @throws {Error} If the input is not a valid non-empty string
141147
*/
142148
static validateGithubAppName(githubAppName) {
143-
if (githubAppName && githubAppName.toLowerCase() !== 'browserstack[bot]') {
144-
if (typeof githubAppName === 'string' && githubAppName.trim().length > 0) {
145-
return githubAppName;
146-
}
149+
if (typeof githubAppName !== 'string') {
147150
throw new Error("Invalid input for 'github-app'. Must be a valid string.");
148151
}
149-
return 'browserstack[bot]';
152+
153+
if (githubAppName.toLowerCase() === 'browserstack[bot]') {
154+
return 'browserstack[bot]';
155+
}
156+
157+
if (githubAppName.trim().length > 0) {
158+
return githubAppName;
159+
}
160+
161+
throw new Error("Invalid input for 'github-app'. Must be a valid string.");
150162
}
151163
}
152164

setup-env/src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const ActionInput = require('./actionInput');
99
const run = async () => {
1010
try {
1111
const inputParser = new ActionInput();
12-
inputParser.setEnvVariables();
12+
await inputParser.setEnvVariables();
1313
} catch (e) {
1414
core.setFailed(`Action Failed: ${e}`);
1515
}

setup-env/test/actionInput/index.test.js

Lines changed: 189 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const { expect } = require('chai');
22
const sinon = require('sinon');
3+
const axios = require('axios');
34
const core = require('@actions/core');
45
const ActionInput = require('../../src/actionInput');
56
const InputValidator = require('../../src/actionInput/inputValidator');
@@ -19,29 +20,29 @@ describe('Action Input operations for fetching all inputs, triggering validation
1920
sinon.stub(InputValidator, 'updateUsername').returns('validatedUsername');
2021
sinon.stub(InputValidator, 'validateBuildName').returns('validatedBuildName');
2122
sinon.stub(InputValidator, 'validateProjectName').returns('validatedProjectName');
23+
sinon.stub(InputValidator, 'validateGithubToken').returns('validatedToken');
24+
sinon.stub(InputValidator, 'validateGithubAppName').returns('validatedAppName');
25+
26+
// Provide required inputs
27+
stubbedInput.withArgs(INPUT.USERNAME, { required: true }).returns('someUsername');
28+
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).returns('someAccessKey');
29+
2230
process.env.GITHUB_REPOSITORY = 'browserstack/github-actions';
31+
process.env.GITHUB_RUN_ID = '12345';
32+
process.env.GITHUB_RUN_ATTEMPT = '2';
2333
});
2434

2535
afterEach(() => {
26-
core.getInput.restore();
27-
InputValidator.updateUsername.restore();
28-
InputValidator.validateBuildName.restore();
29-
InputValidator.validateProjectName.restore();
36+
sinon.restore();
37+
delete process.env.GITHUB_REPOSITORY;
38+
delete process.env.GITHUB_RUN_ID;
39+
delete process.env.GITHUB_RUN_ATTEMPT;
3040
});
3141

3242
it('Takes input and validates it successfully', () => {
33-
stubbedInput.withArgs(INPUT.USERNAME, { required: true }).returns('someUsername');
34-
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).returns('someAccessKey');
3543
stubbedInput.withArgs(INPUT.BUILD_NAME).returns('someBuildName');
3644
stubbedInput.withArgs(INPUT.PROJECT_NAME).returns('someProjectName');
3745
const actionInput = new ActionInput();
38-
sinon.assert.calledWith(core.getInput, INPUT.USERNAME, { required: true });
39-
sinon.assert.calledWith(core.getInput, INPUT.ACCESS_KEY, { required: true });
40-
sinon.assert.calledWith(core.getInput, INPUT.BUILD_NAME);
41-
sinon.assert.calledWith(core.getInput, INPUT.PROJECT_NAME);
42-
sinon.assert.calledWith(InputValidator.updateUsername, 'someUsername');
43-
sinon.assert.calledWith(InputValidator.validateBuildName, 'someBuildName');
44-
sinon.assert.calledWith(InputValidator.validateProjectName, 'someProjectName');
4546
expect(actionInput.username).to.eq('validatedUsername');
4647
expect(actionInput.buildName).to.eq('validatedBuildName');
4748
expect(actionInput.projectName).to.eq('validatedProjectName');
@@ -55,22 +56,24 @@ describe('Action Input operations for fetching all inputs, triggering validation
5556
} catch (e) {
5657
expect(e.message).to.eq('Action input failed for reason: Username Required');
5758
}
58-
sinon.assert.notCalled(InputValidator.updateUsername);
59-
sinon.assert.notCalled(InputValidator.validateBuildName);
60-
sinon.assert.notCalled(InputValidator.validateProjectName);
6159
});
6260

63-
it('Takes input and throws error if accesskey is not provided in input', () => {
61+
it('Takes input and throws error if access key is not provided in input', () => {
6462
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).throws(Error('Access Key Required'));
6563
try {
6664
// eslint-disable-next-line no-new
6765
new ActionInput();
6866
} catch (e) {
6967
expect(e.message).to.eq('Action input failed for reason: Access Key Required');
7068
}
71-
sinon.assert.notCalled(InputValidator.updateUsername);
72-
sinon.assert.notCalled(InputValidator.validateBuildName);
73-
sinon.assert.notCalled(InputValidator.validateProjectName);
69+
});
70+
71+
it('Takes input and validates GitHub token and app name successfully', () => {
72+
stubbedInput.withArgs(INPUT.GITHUB_TOKEN).returns('someToken');
73+
stubbedInput.withArgs(INPUT.GITHUB_APP).returns('someApp');
74+
const actionInput = new ActionInput();
75+
expect(actionInput.githubToken).to.eq('validatedToken');
76+
expect(actionInput.githubApp).to.eq('validatedAppName');
7477
});
7578
});
7679

@@ -85,15 +88,10 @@ describe('Action Input operations for fetching all inputs, triggering validation
8588
});
8689

8790
afterEach(() => {
88-
core.exportVariable.restore();
89-
core.info.restore();
90-
core.startGroup.restore();
91-
core.endGroup.restore();
92-
ActionInput.prototype._fetchAllInput.restore();
93-
ActionInput.prototype._validateInput.restore();
91+
sinon.restore();
9492
});
9593

96-
it('Sets the environment variables required to in test scripts for BrowserStack', () => {
94+
it('Sets the environment variables required in test scripts for BrowserStack', () => {
9795
const actionInput = new ActionInput();
9896
actionInput.username = 'someUsername';
9997
actionInput.accessKey = 'someAccessKey';
@@ -106,4 +104,168 @@ describe('Action Input operations for fetching all inputs, triggering validation
106104
sinon.assert.calledWith(core.exportVariable, ENV_VARS.BROWSERSTACK_BUILD_NAME, 'someBuildName');
107105
});
108106
});
107+
108+
context('Check if BrowserStack Rerun', () => {
109+
let stubbedInput;
110+
111+
beforeEach(() => {
112+
stubbedInput = sinon.stub(core, 'getInput');
113+
sinon.stub(InputValidator, 'updateUsername').returns('validatedUsername');
114+
sinon.stub(InputValidator, 'validateBuildName').returns('validatedBuildName');
115+
sinon.stub(InputValidator, 'validateProjectName').returns('validatedProjectName');
116+
sinon.stub(InputValidator, 'validateGithubToken').returns('validatedToken');
117+
sinon.stub(InputValidator, 'validateGithubAppName').returns('validatedAppName');
118+
119+
// Provide required inputs
120+
stubbedInput.withArgs(INPUT.USERNAME, { required: true }).returns('someUsername');
121+
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).returns('someAccessKey');
122+
123+
process.env.GITHUB_REPOSITORY = 'browserstack/github-actions';
124+
process.env.GITHUB_RUN_ID = '12345';
125+
process.env.GITHUB_RUN_ATTEMPT = '2';
126+
});
127+
128+
afterEach(() => {
129+
sinon.restore();
130+
});
131+
132+
it('Returns false if rerun attempt is less than or equal to 1', async () => {
133+
// stubbedInput.withArgs(INPUT.GITHUB_APP).returns('someApp');
134+
const actionInput = new ActionInput();
135+
actionInput.rerunAttempt = '1';
136+
const result = await actionInput.checkIfBStackReRun();
137+
// eslint-disable-next-line no-unused-expressions
138+
expect(result).to.be.false;
139+
});
140+
141+
it('Returns false if runId, repository, or token are invalid', async () => {
142+
const actionInput = new ActionInput();
143+
actionInput.runId = '';
144+
const result = await actionInput.checkIfBStackReRun();
145+
// eslint-disable-next-line no-unused-expressions
146+
expect(result).to.be.false;
147+
});
148+
149+
it('Returns true if rerun was triggered by the GitHub App', async () => {
150+
const actionInput = new ActionInput();
151+
sinon.stub(actionInput, 'identifyRunFromBStack').returns(Promise.resolve('validatedAppName'));
152+
const result = await actionInput.checkIfBStackReRun();
153+
// eslint-disable-next-line no-unused-expressions
154+
expect(result).to.be.true;
155+
});
156+
157+
it('Returns false if rerun was not triggered by the GitHub App', async () => {
158+
const actionInput = new ActionInput();
159+
sinon.stub(actionInput, 'identifyRunFromBStack').returns(Promise.resolve('otherActor'));
160+
const result = await actionInput.checkIfBStackReRun();
161+
// eslint-disable-next-line no-unused-expressions
162+
expect(result).to.be.false;
163+
});
164+
});
165+
166+
context('Identify Run From BrowserStack', () => {
167+
let axiosGetStub;
168+
let stubbedInput;
169+
170+
beforeEach(() => {
171+
stubbedInput = sinon.stub(core, 'getInput');
172+
sinon.stub(InputValidator, 'updateUsername').returns('validatedUsername');
173+
sinon.stub(InputValidator, 'validateBuildName').returns('validatedBuildName');
174+
sinon.stub(InputValidator, 'validateProjectName').returns('validatedProjectName');
175+
sinon.stub(InputValidator, 'validateGithubToken').returns('validatedToken');
176+
sinon.stub(InputValidator, 'validateGithubAppName').returns('validatedAppName');
177+
178+
// Provide required inputs
179+
stubbedInput.withArgs(INPUT.USERNAME, { required: true }).returns('someUsername');
180+
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).returns('someAccessKey');
181+
182+
process.env.GITHUB_REPOSITORY = 'browserstack/github-actions';
183+
process.env.GITHUB_RUN_ID = '12345';
184+
process.env.GITHUB_RUN_ATTEMPT = '2';
185+
186+
// Stub the axios.get method
187+
axiosGetStub = sinon.stub(axios, 'get');
188+
// Stub core.info to prevent it from throwing an error
189+
sinon.stub(core, 'info');
190+
});
191+
192+
afterEach(() => {
193+
sinon.restore();
194+
});
195+
196+
it('Returns the triggering actor from the GitHub API', async () => {
197+
const actionInput = new ActionInput();
198+
axiosGetStub.resolves({
199+
data: {
200+
triggering_actor: { login: 'someActor' },
201+
},
202+
});
203+
const result = await actionInput.identifyRunFromBStack();
204+
expect(result).to.eq('someActor');
205+
});
206+
207+
it('Handles errors and returns undefined when GitHub API fails', async () => {
208+
const actionInput = new ActionInput();
209+
axiosGetStub.rejects(new Error('API failed'));
210+
const result = await actionInput.identifyRunFromBStack();
211+
// eslint-disable-next-line no-unused-expressions
212+
expect(result).to.be.undefined;
213+
sinon.assert.calledOnce(core.info);
214+
});
215+
});
216+
217+
context('Set BrowserStack Rerun Environment Variables', () => {
218+
let axiosGetStub;
219+
let stubbedInput;
220+
221+
beforeEach(() => {
222+
stubbedInput = sinon.stub(core, 'getInput');
223+
sinon.stub(InputValidator, 'updateUsername').returns('validatedUsername');
224+
sinon.stub(InputValidator, 'validateBuildName').returns('validatedBuildName');
225+
sinon.stub(InputValidator, 'validateProjectName').returns('validatedProjectName');
226+
sinon.stub(InputValidator, 'validateGithubToken').returns('validatedToken');
227+
sinon.stub(InputValidator, 'validateGithubAppName').returns('validatedAppName');
228+
229+
// Provide required inputs
230+
stubbedInput.withArgs(INPUT.USERNAME, { required: true }).returns('someUsername');
231+
stubbedInput.withArgs(INPUT.ACCESS_KEY, { required: true }).returns('someAccessKey');
232+
233+
process.env.GITHUB_REPOSITORY = 'browserstack/github-actions';
234+
process.env.GITHUB_RUN_ID = '12345';
235+
process.env.GITHUB_RUN_ATTEMPT = '2';
236+
237+
// Stub the axios.get method
238+
axiosGetStub = sinon.stub(axios, 'get');
239+
// Stub core.info to prevent it from throwing an error
240+
sinon.stub(core, 'exportVariable');
241+
sinon.stub(core, 'info');
242+
});
243+
244+
afterEach(() => {
245+
sinon.restore();
246+
});
247+
248+
it('Sets environment variables from BrowserStack API response', async () => {
249+
const actionInput = new ActionInput();
250+
const variables = { VAR1: 'value1', VAR2: 'value2' };
251+
axiosGetStub.resolves({
252+
data: { data: { variables } },
253+
});
254+
255+
await actionInput.setBStackRerunEnvVars();
256+
257+
sinon.assert.calledWith(core.exportVariable, 'VAR1', 'value1');
258+
sinon.assert.calledWith(core.exportVariable, 'VAR2', 'value2');
259+
});
260+
261+
it('Handles errors when BrowserStack API fails', async () => {
262+
const actionInput = new ActionInput();
263+
axiosGetStub.rejects(new Error('API failed'));
264+
265+
await actionInput.setBStackRerunEnvVars();
266+
267+
sinon.assert.calledTwice(core.info);
268+
sinon.assert.neverCalledWith(core.exportVariable, sinon.match.any, sinon.match.any);
269+
});
270+
});
109271
});

0 commit comments

Comments
 (0)