Skip to content

Commit 4f02496

Browse files
authored
Merge pull request #713 from mjcheetham/bb-links
Show Bitbucket Server/Data Center URL in UI and use better help links
2 parents 5277ecc + b9c3f41 commit 4f02496

File tree

12 files changed

+221
-9
lines changed

12 files changed

+221
-9
lines changed

src/shared/Atlassian.Bitbucket.Tests/BitbucketAuthenticationTest.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
24
using System.Threading.Tasks;
5+
using GitCredentialManager;
36
using GitCredentialManager.Tests.Objects;
7+
using Moq;
48
using Xunit;
59

610
namespace Atlassian.Bitbucket.Tests
@@ -118,5 +122,113 @@ public async Task BitbucketAuthentication_ShowOAuthRequiredPromptAsync_SucceedsA
118122
Assert.Equal($"Your account has two-factor authentication enabled.{Environment.NewLine}" +
119123
$"To continue you must complete authentication in your web browser.{Environment.NewLine}", context.Terminal.Messages[0].Item1);
120124
}
125+
126+
[Fact]
127+
public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBCloud_HelperCmdLine()
128+
{
129+
var targetUri = new Uri("https://bitbucket.org");
130+
131+
var helperPath = "/usr/bin/test-helper";
132+
var expectedUserName = "jsquire";
133+
var expectedPassword = "password";
134+
var resultDict = new Dictionary<string, string>
135+
{
136+
["username"] = expectedUserName,
137+
["password"] = expectedPassword
138+
};
139+
140+
string expectedArgs = $"userpass --show-oauth";
141+
142+
var context = new TestCommandContext();
143+
context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection
144+
145+
var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };
146+
authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
147+
.Returns(true);
148+
authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))
149+
.ReturnsAsync(resultDict);
150+
151+
BitbucketAuthentication auth = authMock.Object;
152+
CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);
153+
154+
Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
155+
Assert.Equal(result.Credential.Account, expectedUserName);
156+
Assert.Equal(result.Credential.Password, expectedPassword);
157+
158+
authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
159+
Times.Once);
160+
}
161+
162+
[Fact]
163+
public async Task BitbucketAuthentication_GetCredentialsAsync_BasicOnly_User_BBCloud_HelperCmdLine()
164+
{
165+
var targetUri = new Uri("https://bitbucket.org");
166+
167+
var helperPath = "/usr/bin/test-helper";
168+
var expectedUserName = "jsquire";
169+
var expectedPassword = "password";
170+
var resultDict = new Dictionary<string, string>
171+
{
172+
["username"] = expectedUserName,
173+
["password"] = expectedPassword
174+
};
175+
176+
string expectedArgs = $"userpass --username {expectedUserName}";
177+
178+
var context = new TestCommandContext();
179+
context.SessionManager.IsDesktopSession = true; // Enable UI helper selection
180+
181+
var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };
182+
authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
183+
.Returns(true);
184+
authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))
185+
.ReturnsAsync(resultDict);
186+
187+
BitbucketAuthentication auth = authMock.Object;
188+
CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, expectedUserName, AuthenticationModes.Basic);
189+
190+
Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
191+
Assert.Equal(result.Credential.Account, expectedUserName);
192+
Assert.Equal(result.Credential.Password, expectedPassword);
193+
194+
authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
195+
Times.Once);
196+
}
197+
198+
[Fact]
199+
public async Task BitbucketAuthentication_GetCredentialsAsync_AllModes_NoUser_BBServerDC_HelperCmdLine()
200+
{
201+
var targetUri = new Uri("https://example.com/bitbucket");
202+
203+
var helperPath = "/usr/bin/test-helper";
204+
var expectedUserName = "jsquire";
205+
var expectedPassword = "password";
206+
var resultDict = new Dictionary<string, string>
207+
{
208+
["username"] = expectedUserName,
209+
["password"] = expectedPassword
210+
};
211+
212+
string expectedArgs = $"userpass --url {targetUri} --show-oauth";
213+
214+
var context = new TestCommandContext();
215+
context.SessionManager.IsDesktopSession = true; // Enable OAuth and UI helper selection
216+
217+
var authMock = new Mock<BitbucketAuthentication>(context) { CallBase = true };
218+
authMock.Setup(x => x.TryFindHelperExecutablePath(out helperPath))
219+
.Returns(true);
220+
authMock.Setup(x => x.InvokeHelperAsync(It.IsAny<string>(), It.IsAny<string>(), null, CancellationToken.None))
221+
.ReturnsAsync(resultDict);
222+
223+
BitbucketAuthentication auth = authMock.Object;
224+
CredentialsPromptResult result = await auth.GetCredentialsAsync(targetUri, null, AuthenticationModes.All);
225+
226+
Assert.Equal(AuthenticationModes.Basic, result.AuthenticationMode);
227+
Assert.Equal(result.Credential.Account, expectedUserName);
228+
Assert.Equal(result.Credential.Password, expectedPassword);
229+
230+
authMock.Verify(x => x.InvokeHelperAsync(helperPath, expectedArgs, null, CancellationToken.None),
231+
Times.Once);
232+
}
121233
}
122234
}

src/shared/Atlassian.Bitbucket.Tests/BitbucketHostProviderTest.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,58 @@ public class BitbucketHostProviderTest
2121
private Mock<IBitbucketAuthentication> bitbucketAuthentication = new Mock<IBitbucketAuthentication>(MockBehavior.Strict);
2222
private Mock<IBitbucketRestApi> bitbucketApi = new Mock<IBitbucketRestApi>(MockBehavior.Strict);
2323

24+
[Theory]
25+
[InlineData(null, false)]
26+
[InlineData("", false)]
27+
[InlineData(" ", false)]
28+
[InlineData("bitbucket.org", true)]
29+
[InlineData("BITBUCKET.ORG", true)]
30+
[InlineData("BiTbUcKeT.OrG", true)]
31+
[InlineData("bitbucket.example.com", false)]
32+
[InlineData("bitbucket.example.org", false)]
33+
[InlineData("bitbucket.org.com", false)]
34+
[InlineData("bitbucket.org.org", false)]
35+
public void BitbucketHostProvider_IsBitbucketOrg_StringHost(string str, bool expected)
36+
{
37+
bool actual = BitbucketHostProvider.IsBitbucketOrg(str);
38+
Assert.Equal(expected, actual);
39+
}
40+
41+
[Theory]
42+
[InlineData("http://bitbucket.org", true)]
43+
[InlineData("https://bitbucket.org", true)]
44+
[InlineData("http://bitbucket.org/path", true)]
45+
[InlineData("https://bitbucket.org/path", true)]
46+
[InlineData("http://BITBUCKET.ORG", true)]
47+
[InlineData("https://BITBUCKET.ORG", true)]
48+
[InlineData("http://BITBUCKET.ORG/PATH", true)]
49+
[InlineData("https://BITBUCKET.ORG/PATH", true)]
50+
[InlineData("http://BiTbUcKeT.OrG", true)]
51+
[InlineData("https://BiTbUcKeT.OrG", true)]
52+
[InlineData("http://BiTbUcKeT.OrG/pAtH", true)]
53+
[InlineData("https://BiTbUcKeT.OrG/pAtH", true)]
54+
[InlineData("http://bitbucket.example.com", false)]
55+
[InlineData("https://bitbucket.example.com", false)]
56+
[InlineData("http://bitbucket.example.com/path", false)]
57+
[InlineData("https://bitbucket.example.com/path", false)]
58+
[InlineData("http://bitbucket.example.org", false)]
59+
[InlineData("https://bitbucket.example.org", false)]
60+
[InlineData("http://bitbucket.example.org/path", false)]
61+
[InlineData("https://bitbucket.example.org/path", false)]
62+
[InlineData("http://bitbucket.org.com", false)]
63+
[InlineData("https://bitbucket.org.com", false)]
64+
[InlineData("http://bitbucket.org.com/path", false)]
65+
[InlineData("https://bitbucket.org.com/path", false)]
66+
[InlineData("http://bitbucket.org.org", false)]
67+
[InlineData("https://bitbucket.org.org", false)]
68+
[InlineData("http://bitbucket.org.org/path", false)]
69+
[InlineData("https://bitbucket.org.org/path", false)]
70+
public void BitbucketHostProvider_IsBitbucketOrg_Uri(string str, bool expected)
71+
{
72+
bool actual = BitbucketHostProvider.IsBitbucketOrg(new Uri(str));
73+
Assert.Equal(expected, actual);
74+
}
75+
2476
[Theory]
2577
[InlineData("https", null, false)]
2678
// We report that we support unencrypted HTTP here so that we can fail and

src/shared/Atlassian.Bitbucket.UI.Avalonia/Views/CredentialsView.axaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
<StackPanel DockPanel.Dock="Top" Margin="0,0,0,30">
2424
<Image HorizontalAlignment="Center" Source="/Assets/atlassian-logo.png" />
2525
<TextBlock HorizontalAlignment="Center" Text="Log in to your account"/>
26+
<TextBlock HorizontalAlignment="Center" Text="{Binding Url}"
27+
IsVisible="{Binding Url, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
28+
Margin="0,10,0,0"/>
2629
</StackPanel>
2730

2831
<StackPanel Width="288">

src/shared/Atlassian.Bitbucket.UI/Commands/CredentialsCommand.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public abstract class CredentialsCommand : HelperCommand
1515
protected CredentialsCommand(ICommandContext context)
1616
: base(context, "userpass", "Show authentication prompt.")
1717
{
18+
AddOption(
19+
new Option<string>("--url", "Bitbucket Server or Data Center URL")
20+
);
21+
1822
AddOption(
1923
new Option<string>("--username", "Username or email.")
2024
);
@@ -23,13 +27,14 @@ protected CredentialsCommand(ICommandContext context)
2327
new Option("--show-oauth", "Show OAuth option.")
2428
);
2529

26-
Handler = CommandHandler.Create<string, bool>(ExecuteAsync);
30+
Handler = CommandHandler.Create<Uri, string, bool>(ExecuteAsync);
2731
}
2832

29-
private async Task<int> ExecuteAsync(string userName, bool showOAuth)
33+
private async Task<int> ExecuteAsync(Uri url, string userName, bool showOAuth)
3034
{
3135
var viewModel = new CredentialsViewModel(Context.Environment)
3236
{
37+
Url = url,
3338
UserName = userName,
3439
ShowOAuth = showOAuth
3540
};

src/shared/Atlassian.Bitbucket.UI/Commands/OAuthCommand.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.CommandLine;
34
using System.CommandLine.Invocation;
45
using System.Threading;
56
using System.Threading.Tasks;
@@ -14,6 +15,10 @@ public abstract class OAuthCommand : HelperCommand
1415
protected OAuthCommand(ICommandContext context)
1516
: base(context, "oauth", "Show OAuth required prompt.")
1617
{
18+
AddOption(
19+
new Option<string>("--url", "Bitbucket Server or Data Center URL")
20+
);
21+
1722
Handler = CommandHandler.Create(ExecuteAsync);
1823
}
1924

src/shared/Atlassian.Bitbucket.UI/ViewModels/CredentialsViewModel.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.ComponentModel;
23
using System.Windows.Input;
34
using GitCredentialManager;
@@ -10,6 +11,7 @@ public class CredentialsViewModel : WindowViewModel
1011
{
1112
private readonly IEnvironment _environment;
1213

14+
private Uri _url;
1315
private string _userName;
1416
private string _password;
1517
private bool _showOAuth;
@@ -64,12 +66,26 @@ private bool CanAcceptOAuth()
6466

6567
private void ForgotPassword()
6668
{
67-
BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/password/reset/");
69+
Uri passwordResetUri = _url is null
70+
? new Uri(BitbucketConstants.HelpUrls.PasswordReset)
71+
: new Uri(_url, BitbucketConstants.HelpUrls.DataCenterPasswordReset);
72+
73+
BrowserUtils.OpenDefaultBrowser(_environment, passwordResetUri);
6874
}
6975

7076
private void SignUp()
7177
{
72-
BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/signup/");
78+
Uri signUpUri = _url is null
79+
? new Uri(BitbucketConstants.HelpUrls.SignUp)
80+
: new Uri(_url, BitbucketConstants.HelpUrls.DataCenterLogin);
81+
82+
BrowserUtils.OpenDefaultBrowser(_environment, signUpUri);
83+
}
84+
85+
public Uri Url
86+
{
87+
get => _url;
88+
set => SetAndRaisePropertyChanged(ref _url, value);
7389
}
7490

7591
public string UserName

src/shared/Atlassian.Bitbucket.UI/ViewModels/OAuthViewModel.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,18 @@ public OAuthViewModel(IEnvironment environment)
3030

3131
private void LearnMore()
3232
{
33-
BrowserUtils.OpenDefaultBrowser(_environment, "https://confluence.atlassian.com/bitbucket/two-step-verification-777023203.html");
33+
// 2FA is not supported on Server/DC so this prompt will never be seen outside of Bitbucket Cloud
34+
BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.TwoFactor);
3435
}
3536

3637
private void ForgotPassword()
3738
{
38-
BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/password/reset/");
39+
BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.PasswordReset);
3940
}
4041

4142
private void SignUp()
4243
{
43-
BrowserUtils.OpenDefaultBrowser(_environment, "https://bitbucket.org/account/signup/");
44+
BrowserUtils.OpenDefaultBrowser(_environment, BitbucketConstants.HelpUrls.SignUp);
4445
}
4546

4647
/// <summary>

src/shared/Atlassian.Bitbucket/BitbucketAuthentication.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ public async Task<CredentialsPromptResult> GetCredentialsAsync(Uri targetUri, st
9494
TryFindHelperExecutablePath(out string helperPath))
9595
{
9696
var cmdArgs = new StringBuilder("userpass");
97+
if (!BitbucketHostProvider.IsBitbucketOrg(targetUri))
98+
{
99+
cmdArgs.AppendFormat(" --url {0}", QuoteCmdArg(targetUri.ToString()));
100+
}
101+
97102
if (!string.IsNullOrWhiteSpace(userName))
98103
{
99104
cmdArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
@@ -240,7 +245,7 @@ public async Task<OAuth2TokenResult> RefreshOAuthCredentialsAsync(string refresh
240245

241246
#region Private Methods
242247

243-
private bool TryFindHelperExecutablePath(out string path)
248+
protected internal virtual bool TryFindHelperExecutablePath(out string path)
244249
{
245250
return TryFindHelperExecutablePath(
246251
BitbucketConstants.EnvironmentVariables.AuthenticationHelper,

src/shared/Atlassian.Bitbucket/BitbucketConstants.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ public static class Credential
4949
}
5050
}
5151

52+
public static class HelpUrls
53+
{
54+
public const string DataCenterPasswordReset = "/passwordreset";
55+
public const string DataCenterLogin = "/login";
56+
public const string PasswordReset = "https://bitbucket.org/account/password/reset/";
57+
public const string SignUp = "https://bitbucket.org/account/signup/";
58+
public const string TwoFactor = "https://support.atlassian.com/bitbucket-cloud/docs/enable-two-step-verification/";
59+
}
60+
5261
/// <summary>
5362
/// Supported authentication modes for Bitbucket.org
5463
/// </summary>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Atlassian.Bitbucket.Tests")]

0 commit comments

Comments
 (0)