Skip to content

Commit a400e80

Browse files
WIP - uri handler rework
1 parent 9af2bbf commit a400e80

28 files changed

+845
-36
lines changed

__mocks__/vscode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable @typescript-eslint/no-require-imports */
2+
module.exports = {
3+
...require('jest-mock-vscode').createVSCodeMock(jest),
4+
env: {
5+
uriScheme: 'vscode'
6+
}
7+
}

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module.exports = {
2-
roots: ['<rootDir>/src'],
2+
roots: ['<rootDir>'],
33
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'],
44
transform: {
55
'^.+\\.(min.js|ts|tsx)$': [
@@ -9,7 +9,7 @@ module.exports = {
99
},
1010
],
1111
},
12-
transformIgnorePatterns: ['/node_modules/'],
12+
moduleDirectories: ['node_modules', 'src'],
1313
verbose: true,
1414
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
1515
// coverage configuration

package-lock.json

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,7 @@
15171517
"fork-ts-checker-webpack-plugin": "^9.0.2",
15181518
"html-webpack-plugin": "^5.6.0",
15191519
"jest": "^29.7.0",
1520+
"jest-mock-vscode": "^4.0.4",
15201521
"license-checker": "^25.0.1",
15211522
"mini-css-extract-plugin": "^2.9.1",
15221523
"npm-run-all": "^4.1.5",

src/atlclients/authStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { Tokens } from './tokens';
2626
import crypto from 'crypto';
2727
import { keychain } from '../util/keychain';
2828
import { loggedOutEvent } from '../analytics';
29-
import { Container } from 'src/container';
29+
import { Container } from '../container';
3030
const keychainServiceNameV3 = version.endsWith('-insider') ? 'atlascode-insiders-authinfoV3' : 'atlascode-authinfoV3';
3131

3232
enum Priority {

src/atlclients/oauthRefresher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AxiosUserAgent } from '../constants';
66
import { ConnectionTimeout } from '../util/time';
77
import { Container } from '../container';
88
import { Disposable } from 'vscode';
9-
import { Logger } from 'src/logger';
9+
import { Logger } from '../logger';
1010
import { addCurlLogging } from './interceptors';
1111
import { getAgent } from '../jira/jira-client/providers';
1212
import { strategyForProvider } from './strategy';

src/container.ts

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { AtlascodeUriHandler, ONBOARDING_URL, SETTINGS_URL } from './uriHandler';
1+
import { LegacyAtlascodeUriHandler, ONBOARDING_URL, SETTINGS_URL } from './uriHandler/legacyUriHandler';
22
import { BitbucketIssue, BitbucketSite, PullRequest, WorkspaceRepo } from './bitbucket/model';
3-
import { Disposable, ExtensionContext, UriHandler, env, workspace, UIKind } from 'vscode';
3+
import { Disposable, ExtensionContext, env, workspace, UIKind } from 'vscode';
44
import { IConfig, configuration } from './config/configuration';
55

66
import { analyticsClient } from './analytics-node-client/src/client.min.js';
@@ -65,8 +65,9 @@ import { VSCWelcomeActionApi } from './webview/welcome/vscWelcomeActionApi';
6565
import { VSCWelcomeWebviewControllerFactory } from './webview/welcome/vscWelcomeWebviewControllerFactory';
6666
import { WelcomeAction } from './lib/ipc/fromUI/welcome';
6767
import { WelcomeInitMessage } from './lib/ipc/toUI/welcome';
68-
import { FeatureFlagClient } from './util/featureFlags';
68+
import { FeatureFlagClient, Features } from './util/featureFlags';
6969
import { EventBuilder } from './util/featureFlags/eventBuilder';
70+
import { AtlascodeUriHandler } from './uriHandler';
7071
import { CheckoutHelper } from './bitbucket/interfaces';
7172

7273
const isDebuggingRegex = /^--(debug|inspect)\b(-brk\b|(?!-))=?/;
@@ -86,14 +87,6 @@ export class Container {
8687
enable: this.getAnalyticsEnable(),
8788
});
8889

89-
FeatureFlagClient.initialize({
90-
analyticsClient: this._analyticsClient,
91-
identifiers: {
92-
analyticsAnonymousId: env.machineId,
93-
},
94-
eventBuilder: new EventBuilder(),
95-
});
96-
9790
this._cancellationManager = new Map();
9891
this._analyticsApi = new VSCAnalyticsApi(this._analyticsClient, this.isRemote, this.isWebUI);
9992
this._commonMessageHandler = new VSCCommonMessageHandler(this._analyticsApi, this._cancellationManager);
@@ -189,9 +182,6 @@ export class Container {
189182

190183
this._loginManager = new LoginManager(this._credentialManager, this._siteManager, this._analyticsClient);
191184
this._bitbucketHelper = new BitbucketCheckoutHelper(context.globalState);
192-
context.subscriptions.push(
193-
(this._uriHandler = new AtlascodeUriHandler(this._analyticsApi, this._bitbucketHelper)),
194-
);
195185

196186
if (config.jira.explorer.enabled) {
197187
context.subscriptions.push((this._jiraExplorer = new JiraContext()));
@@ -206,13 +196,44 @@ export class Container {
206196
}
207197

208198
context.subscriptions.push((this._helpExplorer = new HelpExplorer()));
199+
200+
FeatureFlagClient.initialize({
201+
analyticsClient: this._analyticsClient,
202+
identifiers: {
203+
analyticsAnonymousId: env.machineId,
204+
},
205+
eventBuilder: new EventBuilder(),
206+
}).then(() => {
207+
this.initializeUriHandler(context, this._analyticsApi, this._bitbucketHelper);
208+
});
209209
}
210210

211211
static getAnalyticsEnable(): boolean {
212212
const telemetryConfig = workspace.getConfiguration('telemetry');
213213
return telemetryConfig.get<boolean>('enableTelemetry', true);
214214
}
215215

216+
static initializeUriHandler(
217+
context: ExtensionContext,
218+
analyticsApi: VSCAnalyticsApi,
219+
bitbucketHelper: CheckoutHelper,
220+
) {
221+
FeatureFlagClient.checkGate(Features.EnableNewUriHandler)
222+
.then((enabled) => {
223+
if (enabled) {
224+
console.log('Using new URI handler');
225+
context.subscriptions.push(new AtlascodeUriHandler(analyticsApi, bitbucketHelper));
226+
} else {
227+
context.subscriptions.push(new LegacyAtlascodeUriHandler(analyticsApi, bitbucketHelper));
228+
}
229+
})
230+
.catch((err) => {
231+
// Not likely that we'd land here - but if anything goes wrong, default to legacy handler
232+
console.error(`Error checking feature flag ${Features.EnableNewUriHandler}: ${err}`);
233+
context.subscriptions.push(new LegacyAtlascodeUriHandler(analyticsApi, bitbucketHelper));
234+
});
235+
}
236+
216237
static initializeBitbucket(bbCtx: BitbucketContext) {
217238
this._bitbucketContext = bbCtx;
218239
this._pipelinesExplorer = new PipelinesExplorer(bbCtx);
@@ -288,11 +309,6 @@ export class Container {
288309
this._context.globalState.update(ConfigTargetKey, target);
289310
}
290311

291-
private static _uriHandler: UriHandler;
292-
static get uriHandler() {
293-
return this._uriHandler;
294-
}
295-
296312
private static _version: string;
297313
static get version() {
298314
return this._version;

src/jira/jira-client/providers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AgentProvider, getProxyHostAndPort, shouldTunnelHost } from '@atlassian
33
import axios, { AxiosInstance } from 'axios';
44
import * as fs from 'fs';
55
import * as https from 'https';
6-
import { Logger } from 'src/logger';
6+
import { Logger } from '../../logger';
77
import * as sslRootCas from 'ssl-root-cas';
88
import { DetailedSiteInfo, SiteInfo } from '../../atlclients/authInfo';
99
import { BasicInterceptor } from '../../atlclients/basicInterceptor';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Uri, window } from 'vscode';
2+
import { CheckoutBranchUriHandlerAction } from './checkoutBranch';
3+
4+
describe('CheckoutBranchUriHandlerAction', () => {
5+
const mockAnalyticsApi = {
6+
fireDeepLinkEvent: jest.fn(),
7+
};
8+
const mockCheckoutHelper = {
9+
checkoutRef: jest.fn().mockResolvedValue(true),
10+
};
11+
let action: CheckoutBranchUriHandlerAction;
12+
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
action = new CheckoutBranchUriHandlerAction(mockCheckoutHelper as any, mockAnalyticsApi as any);
16+
});
17+
18+
describe('isAccepted', () => {
19+
it('only accepts URIs ending with checkoutBranch', () => {
20+
expect(action.isAccepted(Uri.parse('https://some-uri/checkoutBranch'))).toBe(true);
21+
expect(action.isAccepted(Uri.parse('https://some-uri/otherThing'))).toBe(false);
22+
});
23+
});
24+
25+
describe('handle', () => {
26+
it('throws if required query params are missing', async () => {
27+
await expect(action.handle(Uri.parse('https://some-uri/checkoutBranch'))).rejects.toThrow();
28+
await expect(
29+
action.handle(Uri.parse('https://some-uri/checkoutBranch?cloneUrl=...&ref=...')),
30+
).rejects.toThrow();
31+
await expect(
32+
action.handle(Uri.parse('https://some-uri/checkoutBranch?cloneUrl=...&refType=...')),
33+
).rejects.toThrow();
34+
await expect(
35+
action.handle(Uri.parse('https://some-uri/checkoutBranch?ref=...&refType=...')),
36+
).rejects.toThrow();
37+
});
38+
39+
it('checks out the branch and fires an event on success', async () => {
40+
mockCheckoutHelper.checkoutRef.mockResolvedValue(true);
41+
await action.handle(Uri.parse('https://some-uri/checkoutBranch?cloneUrl=one&ref=two&refType=three'));
42+
43+
expect(mockCheckoutHelper.checkoutRef).toHaveBeenCalledWith('one', 'two', 'three', '');
44+
expect(mockAnalyticsApi.fireDeepLinkEvent).toHaveBeenCalled();
45+
});
46+
47+
it('shows an error message on failure', async () => {
48+
mockCheckoutHelper.checkoutRef.mockRejectedValue(new Error('oh no'));
49+
await action.handle(Uri.parse('https://some-uri/checkoutBranch?cloneUrl=one&ref=two&refType=three'));
50+
51+
expect(window.showErrorMessage).toHaveBeenCalled();
52+
});
53+
});
54+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Uri, window } from 'vscode';
2+
import { isAcceptedBySuffix, UriHandlerAction } from '../uriHandlerAction';
3+
import { CheckoutHelper } from '../../bitbucket/interfaces';
4+
import { AnalyticsApi } from '../../lib/analyticsApi';
5+
import { Logger } from '../../logger';
6+
7+
export class CheckoutBranchUriHandlerAction implements UriHandlerAction {
8+
constructor(
9+
private bitbucketHelper: CheckoutHelper,
10+
private analyticsApi: AnalyticsApi,
11+
) {}
12+
13+
isAccepted(uri: Uri): boolean {
14+
return isAcceptedBySuffix(uri, 'checkoutBranch');
15+
}
16+
17+
async handle(uri: Uri) {
18+
const query = new URLSearchParams(uri.query);
19+
const cloneUrl = decodeURIComponent(query.get('cloneUrl') || '');
20+
const sourceCloneUrl = decodeURIComponent(query.get('sourceCloneUrl') || ''); //For branches originating from a forked repo
21+
const ref = query.get('ref');
22+
const refType = query.get('refType');
23+
if (!ref || !cloneUrl || !refType) {
24+
throw new Error(`Query params are missing data: ${query}`);
25+
}
26+
27+
try {
28+
const success = await this.bitbucketHelper.checkoutRef(cloneUrl, ref, refType, sourceCloneUrl);
29+
30+
if (success) {
31+
this.analyticsApi.fireDeepLinkEvent(
32+
decodeURIComponent(query.get('source') || 'unknown'),
33+
'checkoutBranch',
34+
);
35+
}
36+
} catch (e) {
37+
Logger.debug('error checkout out branch:', e);
38+
window.showErrorMessage('Error checkout out branch (check log for details)');
39+
}
40+
}
41+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Uri, window } from 'vscode';
2+
import { CloneRepositoryUriHandlerAction } from './cloneRepository';
3+
4+
describe('CloneRepositoryUriHandlerAction', () => {
5+
const mockAnalyticsApi = {
6+
fireDeepLinkEvent: jest.fn(),
7+
};
8+
const mockCheckoutHelper = {
9+
cloneRepository: jest.fn(),
10+
};
11+
let action: CloneRepositoryUriHandlerAction;
12+
13+
beforeEach(() => {
14+
jest.clearAllMocks();
15+
action = new CloneRepositoryUriHandlerAction(mockCheckoutHelper as any, mockAnalyticsApi as any);
16+
});
17+
18+
describe('isAccepted', () => {
19+
it('only accepts URIs ending with cloneRepository', () => {
20+
expect(action.isAccepted(Uri.parse('https://some-uri/cloneRepository'))).toBe(true);
21+
expect(action.isAccepted(Uri.parse('https://some-uri/otherThing'))).toBe(false);
22+
});
23+
});
24+
25+
describe('handle', () => {
26+
it('throws if required query params are missing', async () => {
27+
await expect(action.handle(Uri.parse('https://some-uri/cloneRepository'))).rejects.toThrow();
28+
});
29+
30+
it('clones the repo and fires an event on success', async () => {
31+
mockCheckoutHelper.cloneRepository.mockResolvedValue(null);
32+
await action.handle(Uri.parse('https://some-uri/cloneRepository?q=one'));
33+
34+
expect(mockCheckoutHelper.cloneRepository).toHaveBeenCalledWith('one');
35+
expect(mockAnalyticsApi.fireDeepLinkEvent).toHaveBeenCalled();
36+
});
37+
38+
it('shows an error message on failure', async () => {
39+
mockCheckoutHelper.cloneRepository.mockRejectedValue(new Error('oh no'));
40+
await action.handle(Uri.parse('https://some-uri/cloneRepository?q=one'));
41+
42+
expect(window.showErrorMessage).toHaveBeenCalled();
43+
});
44+
});
45+
});

0 commit comments

Comments
 (0)