Skip to content

Commit 04bf7aa

Browse files
[AXON-412] Use correct client & metadata for cloud API tokens (#403)
* [AXON-412] Use correct client & metadata for cloud API tokens * [AXON-412] chore: remove `server` from names where appropriate * [AXON-412] chore: rename & clean up fetchSiteId * [AXON-412] chore: various fixes towards E2E tests passing
1 parent 0dca035 commit 04bf7aa

File tree

8 files changed

+132
-53
lines changed

8 files changed

+132
-53
lines changed

e2e/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
### What is this?
2+
3+
This is the WIP suite of end-to-end (E2E) tests we've implemented for Atlascode.
4+
5+
How does it work? In summary:
6+
7+
- We mock the various API calls made from the extension using wiremock
8+
- We spin up a browser-based extension and use playwright to perform various actions
9+
- All of that is run during the build in a docker container, with a supporting `wiremock` instance using docker-compose
10+
11+
### How do I use it?
12+
13+
To run the tests locally, it should be enough to do the following:
14+
15+
1. First, prepare mock certificates for wiremock, using
16+
17+
```sh
18+
npm run test:e2e:sslcerts
19+
```
20+
21+
2. Build a docker image that we use for testing:
22+
23+
```sh
24+
npm run test:e2e:docker:build
25+
```
26+
27+
3. Run tests headless in a docker container:
28+
29+
```sh
30+
npm run test:e2e:docker
31+
```
32+
33+
4. Check the output, and the artifacts provided in `./test-results`
34+
35+
---
36+
37+
⚠️ **Note**: Please be aware that the tests leverage a `.vsix` artifact produced by the build
38+
To run E2E tests against changed code, you might need to re-build the extension by running
39+
40+
```sh
41+
npm run extension:package
42+
```

e2e/compose.yml

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
networks:
2-
e2e:
2+
e2e:
33

4-
services:
5-
wiremock-mockedteams:
6-
image: wiremock/wiremock
7-
command: >
8-
--https-port 443
9-
--verbose
10-
--https-keystore /home/wiremock_ssl_certs/wiremock-mockedteams.p12
11-
volumes:
12-
- ./wiremock-mappings/mockedteams:/home/wiremock/mappings
13-
- ./sslcerts:/home/wiremock_ssl_certs
14-
networks:
15-
e2e:
16-
aliases:
17-
- mockedteams.atlassian.net
18-
expose:
19-
- 443
4+
services:
5+
wiremock-mockedteams:
6+
image: wiremock/wiremock
7+
command: >
8+
--https-port 443
9+
--verbose
10+
--https-keystore /home/wiremock_ssl_certs/wiremock-mockedteams.p12
11+
volumes:
12+
- ./wiremock-mappings/mockedteams:/home/wiremock/mappings
13+
- ./sslcerts:/home/wiremock_ssl_certs
14+
networks:
15+
e2e:
16+
aliases:
17+
- mockedteams.atlassian.net
18+
expose:
19+
- 443
2020

21-
atlascode-e2e:
22-
image: atlascode-e2e
23-
volumes:
24-
- ${PWD}/..:/atlascode
25-
environment:
26-
- NODE_TLS_REJECT_UNAUTHORIZED=0
27-
depends_on:
28-
- wiremock-mockedteams
29-
networks:
30-
- e2e
31-
ports:
32-
- "9988:9988"
21+
atlascode-e2e:
22+
image: atlascode-e2e
23+
volumes:
24+
- ${PWD}/..:/atlascode
25+
environment:
26+
- NODE_TLS_REJECT_UNAUTHORIZED=0
27+
depends_on:
28+
- wiremock-mockedteams
29+
networks:
30+
- e2e
31+
ports:
32+
- '9988:9988'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"request": {
3+
"method": "GET",
4+
"urlPath": "/_edge/tenant_info"
5+
},
6+
"response": {
7+
"status": 200,
8+
"body": "{\"cloudId\":\"3036c423-78e7-442a-8bed-47870b5a7526\"}",
9+
"headers": {
10+
"Content-Type": "application/json"
11+
}
12+
}
13+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"test:unit": "jest -c jest.unit.config.ts",
6565
"test:react": "jest -c jest.react.config.ts",
6666
"test:e2e:sslcerts": "cd e2e/sslcerts && ./generate-certs.sh",
67-
"test:e2e:docker": "cd e2e && docker compose run atlascode-e2e",
67+
"test:e2e:docker": "cd e2e && docker compose run atlascode-e2e; docker compose down",
6868
"test:e2e:docker:build": "docker build --tag atlascode-e2e - <e2e/Dockerfile",
6969
"devcompile": "npm run clean && npm run devcompile:react && npm run devcompile:extension",
7070
"devcompile:react": "webpack --mode development --config webpack.react.dev.js --fail-on-warnings",

playwright.config.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ export default defineConfig({
55
viewport: {
66
width: 1600,
77
height: 800,
8-
}
8+
},
99
},
10-
});
10+
use: {
11+
// Docs: https://playwright.dev/docs/videos
12+
// To see all of the videos, change this to 'on'
13+
video: 'retain-on-failure',
14+
},
15+
});

src/atlclients/clientManager.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ export class ClientManager implements Disposable {
168168
jiraTokenAuthProvider(info.access),
169169
getAgent,
170170
);
171+
} else if (isBasicAuthInfo(info) && site.isCloud) {
172+
Logger.debug(`${tag}: creating cloud client for ${site.baseApiUrl}`);
173+
client = new JiraCloudClient(
174+
site,
175+
basicJiraTransportFactory(site),
176+
jiraBasicAuthProvider(info.username, info.password),
177+
getAgent,
178+
);
171179
} else if (isBasicAuthInfo(info)) {
172180
client = new JiraServerClient(
173181
site,

src/atlclients/loginManager.test.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('LoginManager', () => {
162162

163163
describe('userInitiatedServerLogin', () => {
164164
it.each([ProductJira, ProductBitbucket])(
165-
'should call saveDetailsForServerSite with correct parameters for BasicAuthInfo',
165+
'should call saveDetailsForSite with correct parameters for BasicAuthInfo',
166166
async (product: Product) => {
167167
const site: SiteInfo = { host: `${product.key}.atlassian.com`, product };
168168
const user = forceCastTo<UserInfo>({ id: 'user' });
@@ -174,35 +174,33 @@ describe('LoginManager', () => {
174174
};
175175
const siteDetails = forceCastTo<DetailedSiteInfo>({ host: `${product.key}.atlassian.com`, product });
176176

177-
jest.spyOn(loginManager as any, 'saveDetailsForServerSite').mockResolvedValue(
178-
Promise.resolve(siteDetails),
179-
);
177+
jest.spyOn(loginManager as any, 'saveDetailsForSite').mockResolvedValue(Promise.resolve(siteDetails));
180178
jest.spyOn(authInfo, 'isBasicAuthInfo').mockReturnValue(true);
181179
jest.spyOn(authInfo, 'isPATAuthInfo').mockReturnValue(false);
182180
jest.spyOn(loginManager['_analyticsClient'], 'sendTrackEvent');
183181

184182
await loginManager.userInitiatedServerLogin(site, authInfoData);
185183

186-
expect(loginManager['saveDetailsForServerSite']).toHaveBeenCalledWith(site, authInfoData);
184+
expect(loginManager['saveDetailsForSite']).toHaveBeenCalledWith(site, authInfoData);
187185
expect(loginManager['_analyticsClient'].sendTrackEvent).toHaveBeenCalled();
188186
},
189187
);
190188

191189
it.each([ProductJira, ProductBitbucket])(
192-
'should call saveDetailsForServerSite with correct parameters for PATAuthInfo',
190+
'should call saveDetailsForSite with correct parameters for PATAuthInfo',
193191
async (product: Product) => {
194192
const site: SiteInfo = { host: `${product.key}.atlassian.com`, product };
195193
const authInfoData = { token: 'token' } as unknown as PATAuthInfo;
196194
const siteDetails = forceCastTo<DetailedSiteInfo>({ host: `${product.key}.atlassian.com`, product });
197195

198-
jest.spyOn(loginManager as any, 'saveDetailsForServerSite').mockResolvedValue(siteDetails);
196+
jest.spyOn(loginManager as any, 'saveDetailsForSite').mockResolvedValue(siteDetails);
199197
jest.spyOn(authInfo, 'isBasicAuthInfo').mockReturnValue(false);
200198
jest.spyOn(authInfo, 'isPATAuthInfo').mockReturnValue(true);
201199
jest.spyOn(loginManager['_analyticsClient'], 'sendTrackEvent');
202200

203201
await loginManager.userInitiatedServerLogin(site, authInfoData);
204202

205-
expect(loginManager['saveDetailsForServerSite']).toHaveBeenCalledWith(site, authInfoData);
203+
expect(loginManager['saveDetailsForSite']).toHaveBeenCalledWith(site, authInfoData);
206204
expect(loginManager['_analyticsClient'].sendTrackEvent).toHaveBeenCalled();
207205
},
208206
);
@@ -219,7 +217,7 @@ describe('LoginManager', () => {
219217
state: AuthInfoState.Valid,
220218
};
221219

222-
jest.spyOn(loginManager as any, 'saveDetailsForServerSite').mockRejectedValue(
220+
jest.spyOn(loginManager as any, 'saveDetailsForSite').mockRejectedValue(
223221
new Error('Authentication failed'),
224222
);
225223
jest.spyOn(authInfo, 'isBasicAuthInfo').mockReturnValue(true);
@@ -256,7 +254,7 @@ describe('LoginManager', () => {
256254
});
257255

258256
describe('updatedServerInfo', () => {
259-
it('should call saveDetailsForServerSite with correct parameters for BasicAuthInfo', async () => {
257+
it('should call saveDetailsForSite with correct parameters for BasicAuthInfo', async () => {
260258
const site: SiteInfo = { host: 'jira.atlassian.com', product: ProductJira };
261259
const user = forceCastTo<UserInfo>({ id: 'user' });
262260
const authInfoData: BasicAuthInfo = {
@@ -266,13 +264,13 @@ describe('LoginManager', () => {
266264
state: AuthInfoState.Valid,
267265
};
268266

269-
jest.spyOn(loginManager as any, 'saveDetailsForServerSite').mockResolvedValue(site);
267+
jest.spyOn(loginManager as any, 'saveDetailsForSite').mockResolvedValue(site);
270268
jest.spyOn(authInfo, 'isBasicAuthInfo').mockReturnValue(true);
271269
jest.spyOn(loginManager['_analyticsClient'], 'sendTrackEvent');
272270

273-
await loginManager.updatedServerInfo(site, authInfoData);
271+
await loginManager.updateInfo(site, authInfoData);
274272

275-
expect(loginManager['saveDetailsForServerSite']).toHaveBeenCalledWith(site, authInfoData);
273+
expect(loginManager['saveDetailsForSite']).toHaveBeenCalledWith(site, authInfoData);
276274
expect(loginManager['_analyticsClient'].sendTrackEvent).toHaveBeenCalled();
277275
});
278276

@@ -286,12 +284,10 @@ describe('LoginManager', () => {
286284
state: AuthInfoState.Valid,
287285
};
288286

289-
jest.spyOn(loginManager as any, 'saveDetailsForServerSite').mockRejectedValue(
290-
new Error('Authentication failed'),
291-
);
287+
jest.spyOn(loginManager as any, 'saveDetailsForSite').mockRejectedValue(new Error('Authentication failed'));
292288
jest.spyOn(authInfo, 'isBasicAuthInfo').mockReturnValue(true);
293289

294-
await expect(loginManager.updatedServerInfo(site, authInfoData)).rejects.toEqual(
290+
await expect(loginManager.updateInfo(site, authInfoData)).rejects.toEqual(
295291
'Error authenticating with Jira: Error: Authentication failed',
296292
);
297293
});

src/atlclients/loginManager.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { BitbucketAuthenticator } from './bitbucketAuthenticator';
2828
import { JiraAuthentictor as JiraAuthenticator } from './jiraAuthenticator';
2929
import { OAuthDancer } from './oauthDancer';
3030

31+
const CLOUD_TLD = '.atlassian.net';
32+
3133
export class LoginManager {
3234
private _dancer: OAuthDancer = OAuthDancer.Instance;
3335
private _jiraAuthenticator: JiraAuthenticator;
@@ -123,7 +125,7 @@ export class LoginManager {
123125
public async userInitiatedServerLogin(site: SiteInfo, authInfo: AuthInfo, isOnboarding?: boolean): Promise<void> {
124126
if (isBasicAuthInfo(authInfo) || isPATAuthInfo(authInfo)) {
125127
try {
126-
const siteDetails = await this.saveDetailsForServerSite(site, authInfo);
128+
const siteDetails = await this.saveDetailsForSite(site, authInfo);
127129

128130
authenticatedEvent(siteDetails, isOnboarding).then((e) => {
129131
this._analyticsClient.sendTrackEvent(e);
@@ -135,10 +137,10 @@ export class LoginManager {
135137
}
136138
}
137139

138-
public async updatedServerInfo(site: SiteInfo, authInfo: AuthInfo): Promise<void> {
140+
public async updateInfo(site: SiteInfo, authInfo: AuthInfo): Promise<void> {
139141
if (isBasicAuthInfo(authInfo)) {
140142
try {
141-
const siteDetails = await this.saveDetailsForServerSite(site, authInfo);
143+
const siteDetails = await this.saveDetailsForSite(site, authInfo);
142144
editedEvent(siteDetails).then((e) => {
143145
this._analyticsClient.sendTrackEvent(e);
144146
});
@@ -159,7 +161,7 @@ export class LoginManager {
159161
return '';
160162
}
161163

162-
private async saveDetailsForServerSite(
164+
private async saveDetailsForSite(
163165
site: SiteInfo,
164166
credentials: BasicAuthInfo | PATAuthInfo,
165167
): Promise<DetailedSiteInfo> {
@@ -233,6 +235,13 @@ export class LoginManager {
233235
pfxPassphrase: site.pfxPassphrase,
234236
};
235237

238+
if (site.host.endsWith(CLOUD_TLD)) {
239+
// Special case to accomodate for API key login to cloud instances
240+
siteDetails.isCloud = true;
241+
siteDetails.userId = json.accountId;
242+
siteDetails.id = await this.fetchCloudSiteId(siteDetails.host);
243+
}
244+
236245
if (site.product.key === ProductJira.key) {
237246
credentials.user = {
238247
displayName: json.displayName,
@@ -255,4 +264,10 @@ export class LoginManager {
255264

256265
return siteDetails;
257266
}
267+
268+
private async fetchCloudSiteId(host: string): Promise<string> {
269+
const response = await fetch(`https://${host}/_edge/tenant_info`);
270+
const data = await response.json();
271+
return data.cloudId;
272+
}
258273
}

0 commit comments

Comments
 (0)