Skip to content

Commit be9b83d

Browse files
committed
basic: replace system UI with new UI helper
Replace calls to the native system UI prompts with shelling out to the generic helper UI.
1 parent a1a358a commit be9b83d

File tree

10 files changed

+106
-153
lines changed

10 files changed

+106
-153
lines changed

src/shared/Core.Tests/Authentication/BasicAuthenticationTests.cs

Lines changed: 47 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
24
using GitCredentialManager.Authentication;
35
using GitCredentialManager.Tests.Objects;
46
using Moq;
@@ -14,11 +16,11 @@ public void BasicAuthentication_GetCredentials_NullResource_ThrowsException()
1416
var context = new TestCommandContext();
1517
var basicAuth = new BasicAuthentication(context);
1618

17-
Assert.Throws<ArgumentNullException>(() => basicAuth.GetCredentials(null));
19+
Assert.ThrowsAsync<ArgumentNullException>(() => basicAuth.GetCredentialsAsync(null));
1820
}
1921

2022
[Fact]
21-
public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials()
23+
public async Task BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUserName_PasswordPromptReturnsCredentials()
2224
{
2325
const string testResource = "https://example.com";
2426
const string testUserName = "john.doe";
@@ -29,14 +31,14 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_ResourceAndUser
2931

3032
var basicAuth = new BasicAuthentication(context);
3133

32-
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
34+
ICredential credential = await basicAuth.GetCredentialsAsync(testResource, testUserName);
3335

3436
Assert.Equal(testUserName, credential.Account);
3537
Assert.Equal(testPassword, credential.Password);
3638
}
3739

3840
[Fact]
39-
public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials()
41+
public async Task BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPassPromptReturnsCredentials()
4042
{
4143
const string testResource = "https://example.com";
4244
const string testUserName = "john.doe";
@@ -48,7 +50,7 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_Resource_UserPa
4850

4951
var basicAuth = new BasicAuthentication(context);
5052

51-
ICredential credential = basicAuth.GetCredentials(testResource);
53+
ICredential credential = await basicAuth.GetCredentialsAsync(testResource);
5254

5355
Assert.Equal(testUserName, credential.Account);
5456
Assert.Equal(testPassword, credential.Password);
@@ -67,100 +69,78 @@ public void BasicAuthentication_GetCredentials_NonDesktopSession_NoTerminalPromp
6769

6870
var basicAuth = new BasicAuthentication(context);
6971

70-
Assert.Throws<InvalidOperationException>(() => basicAuth.GetCredentials(testResource));
72+
Assert.ThrowsAsync<InvalidOperationException>(() => basicAuth.GetCredentialsAsync(testResource));
7173
}
7274

73-
[PlatformFact(Platforms.Windows)]
74-
public void BasicAuthentication_GetCredentials_DesktopSession_Resource_UserPassPromptReturnsCredentials()
75+
[Fact]
76+
public async Task BasicAuthentication_GetCredentials_DesktopSession_CallsHelper()
7577
{
7678
const string testResource = "https://example.com";
7779
const string testUserName = "john.doe";
7880
const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
7981

8082
var context = new TestCommandContext
8183
{
82-
SessionManager = {IsDesktopSession = true},
83-
SystemPrompts =
84-
{
85-
CredentialPrompt = (resource, userName) =>
86-
{
87-
Assert.Equal(testResource, resource);
88-
Assert.Null(userName);
89-
90-
return new GitCredential(testUserName, testPassword);
91-
}
92-
}
84+
SessionManager = {IsDesktopSession = true}
9385
};
9486

95-
var basicAuth = new BasicAuthentication(context);
96-
97-
ICredential credential = basicAuth.GetCredentials(testResource);
98-
99-
Assert.NotNull(credential);
100-
Assert.Equal(testUserName, credential.Account);
101-
Assert.Equal(testPassword, credential.Password);
102-
}
103-
104-
[PlatformFact(Platforms.Windows)]
105-
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptReturnsCredentials()
106-
{
107-
const string testResource = "https://example.com";
108-
const string testUserName = "john.doe";
109-
const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
110-
111-
var context = new TestCommandContext
112-
{
113-
SessionManager = {IsDesktopSession = true},
114-
SystemPrompts =
115-
{
116-
CredentialPrompt = (resource, userName) =>
87+
context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0];
88+
context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0];
89+
90+
var auth = new Mock<BasicAuthentication>(MockBehavior.Strict, context);
91+
auth.Setup(x => x.InvokeHelperAsync(
92+
It.IsAny<string>(),
93+
$"basic --resource {testResource}",
94+
It.IsAny<IDictionary<string, string>>(),
95+
It.IsAny<System.Threading.CancellationToken>()))
96+
.ReturnsAsync(
97+
new Dictionary<string, string>
11798
{
118-
Assert.Equal(testResource, resource);
119-
Assert.Equal(testUserName, userName);
120-
121-
return new GitCredential(testUserName, testPassword);
99+
["username"] = testUserName,
100+
["password"] = testPassword
122101
}
123-
}
124-
};
125-
126-
var basicAuth = new BasicAuthentication(context);
102+
);
127103

128-
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
104+
ICredential credential = await auth.Object.GetCredentialsAsync(testResource);
129105

130106
Assert.NotNull(credential);
131107
Assert.Equal(testUserName, credential.Account);
132108
Assert.Equal(testPassword, credential.Password);
133109
}
134110

135-
[PlatformFact(Platforms.Windows)]
136-
public void BasicAuthentication_GetCredentials_DesktopSession_ResourceAndUser_PassPromptDiffUserReturnsCredentials()
111+
[Fact]
112+
public async Task BasicAuthentication_GetCredentials_DesktopSession_UserName_CallsHelper()
137113
{
138114
const string testResource = "https://example.com";
139115
const string testUserName = "john.doe";
140-
const string newUserName = "jane.doe";
141116
const string testPassword = "letmein123"; // [SuppressMessage("Microsoft.Security", "CS001:SecretInline", Justification="Fake credential")]
142117

143118
var context = new TestCommandContext
144119
{
145-
SessionManager = {IsDesktopSession = true},
146-
SystemPrompts =
147-
{
148-
CredentialPrompt = (resource, userName) =>
149-
{
150-
Assert.Equal(testResource, resource);
151-
Assert.Equal(testUserName, userName);
152-
153-
return new GitCredential(newUserName, testPassword);
154-
}
155-
}
120+
SessionManager = {IsDesktopSession = true}
156121
};
157122

158-
var basicAuth = new BasicAuthentication(context);
123+
context.FileSystem.Files["/usr/local/bin/git-credential-manager-ui"] = new byte[0];
124+
context.FileSystem.Files[@"C:\Program Files\Git Credential Manager Core\git-credential-manager-ui.exe"] = new byte[0];
125+
126+
var auth = new Mock<BasicAuthentication>(MockBehavior.Strict, context);
127+
auth.Setup(x => x.InvokeHelperAsync(
128+
It.IsAny<string>(),
129+
$"basic --resource {testResource} --username {testUserName}",
130+
It.IsAny<IDictionary<string, string>>(),
131+
It.IsAny<System.Threading.CancellationToken>()))
132+
.ReturnsAsync(
133+
new Dictionary<string, string>
134+
{
135+
["username"] = testUserName,
136+
["password"] = testPassword
137+
}
138+
);
159139

160-
ICredential credential = basicAuth.GetCredentials(testResource, testUserName);
140+
ICredential credential = await auth.Object.GetCredentialsAsync(testResource, testUserName);
161141

162142
Assert.NotNull(credential);
163-
Assert.Equal(newUserName, credential.Account);
143+
Assert.Equal(testUserName, credential.Account);
164144
Assert.Equal(testPassword, credential.Password);
165145
}
166146
}

src/shared/Core.Tests/GenericHostProviderTests.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_Return
8383
Settings = {IsWindowsIntegratedAuthenticationEnabled = false}
8484
};
8585
var basicAuthMock = new Mock<IBasicAuthentication>();
86-
basicAuthMock.Setup(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()))
87-
.Returns(basicCredential)
86+
basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))
87+
.ReturnsAsync(basicCredential)
8888
.Verifiable();
8989
var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();
9090

@@ -96,7 +96,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_WiaNotAllowed_Return
9696
Assert.Equal(testUserName, credential.Account);
9797
Assert.Equal(testPassword, credential.Password);
9898
wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny<Uri>()), Times.Never);
99-
basicAuthMock.Verify(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
99+
basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
100100
}
101101

102102
[Fact]
@@ -117,8 +117,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic
117117
Settings = {LegacyAuthorityOverride = "basic"}
118118
};
119119
var basicAuthMock = new Mock<IBasicAuthentication>();
120-
basicAuthMock.Setup(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()))
121-
.Returns(basicCredential)
120+
basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))
121+
.ReturnsAsync(basicCredential)
122122
.Verifiable();
123123
var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();
124124

@@ -130,7 +130,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_LegacyAuthorityBasic
130130
Assert.Equal(testUserName, credential.Account);
131131
Assert.Equal(testPassword, credential.Password);
132132
wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny<Uri>()), Times.Never);
133-
basicAuthMock.Verify(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
133+
basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
134134
}
135135

136136
[Fact]
@@ -148,8 +148,8 @@ public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_Retu
148148

149149
var context = new TestCommandContext();
150150
var basicAuthMock = new Mock<IBasicAuthentication>();
151-
basicAuthMock.Setup(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()))
152-
.Returns(basicCredential)
151+
basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))
152+
.ReturnsAsync(basicCredential)
153153
.Verifiable();
154154
var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();
155155

@@ -161,7 +161,7 @@ public async Task GenericHostProvider_CreateCredentialAsync_NonHttpProtocol_Retu
161161
Assert.Equal(testUserName, credential.Account);
162162
Assert.Equal(testPassword, credential.Password);
163163
wiaAuthMock.Verify(x => x.GetIsSupportedAsync(It.IsAny<Uri>()), Times.Never);
164-
basicAuthMock.Verify(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
164+
basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
165165
}
166166

167167
[PlatformFact(Platforms.Posix)]
@@ -194,7 +194,7 @@ private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(bool
194194

195195
var context = new TestCommandContext();
196196
var basicAuthMock = new Mock<IBasicAuthentication>();
197-
basicAuthMock.Setup(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()))
197+
basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))
198198
.Verifiable();
199199
var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();
200200
wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny<Uri>()))
@@ -207,7 +207,7 @@ private static async Task TestCreateCredentialAsync_ReturnsEmptyCredential(bool
207207
Assert.NotNull(credential);
208208
Assert.Equal(string.Empty, credential.Account);
209209
Assert.Equal(string.Empty, credential.Password);
210-
basicAuthMock.Verify(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
210+
basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
211211
}
212212

213213
private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool wiaSupported)
@@ -224,8 +224,8 @@ private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool
224224

225225
var context = new TestCommandContext();
226226
var basicAuthMock = new Mock<IBasicAuthentication>();
227-
basicAuthMock.Setup(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()))
228-
.Returns(basicCredential)
227+
basicAuthMock.Setup(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()))
228+
.ReturnsAsync(basicCredential)
229229
.Verifiable();
230230
var wiaAuthMock = new Mock<IWindowsIntegratedAuthentication>();
231231
wiaAuthMock.Setup(x => x.GetIsSupportedAsync(It.IsAny<Uri>()))
@@ -238,7 +238,7 @@ private static async Task TestCreateCredentialAsync_ReturnsBasicCredential(bool
238238
Assert.NotNull(credential);
239239
Assert.Equal(testUserName, credential.Account);
240240
Assert.Equal(testPassword, credential.Password);
241-
basicAuthMock.Verify(x => x.GetCredentials(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
241+
basicAuthMock.Verify(x => x.GetCredentialsAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
242242
}
243243

244244
#endregion

src/shared/Core/Authentication/BasicAuthentication.cs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Threading.Tasks;
25

36
namespace GitCredentialManager.Authentication
47
{
58
public interface IBasicAuthentication
69
{
7-
ICredential GetCredentials(string resource, string userName);
10+
Task<ICredential> GetCredentialsAsync(string resource, string userName);
811
}
912

1013
public static class BasicAuthenticationExtensions
1114
{
12-
public static ICredential GetCredentials(this IBasicAuthentication basicAuth, string resource)
15+
public static Task<ICredential> GetCredentialsAsync(this IBasicAuthentication basicAuth, string resource)
1316
{
14-
return basicAuth.GetCredentials(resource, null);
17+
return basicAuth.GetCredentialsAsync(resource, null);
1518
}
1619
}
1720

@@ -25,17 +28,16 @@ public class BasicAuthentication : AuthenticationBase, IBasicAuthentication
2528
public BasicAuthentication(ICommandContext context)
2629
: base (context) { }
2730

28-
public ICredential GetCredentials(string resource, string userName)
31+
public async Task<ICredential> GetCredentialsAsync(string resource, string userName)
2932
{
3033
EnsureArgument.NotNullOrWhiteSpace(resource, nameof(resource));
3134

3235
ThrowIfUserInteractionDisabled();
3336

34-
// TODO: we only support system GUI prompts on Windows currently
3537
if (Context.Settings.IsGuiPromptsEnabled && Context.SessionManager.IsDesktopSession &&
36-
PlatformUtils.IsWindows())
38+
TryFindHelperExecutablePath(out string helperPath))
3739
{
38-
return GetCredentialsByUi(resource, userName);
40+
return await GetCredentialsByUiAsync(helperPath, resource, userName);
3941
}
4042

4143
ThrowIfTerminalPromptsDisabled();
@@ -64,14 +66,42 @@ private ICredential GetCredentialsByTty(string resource, string userName)
6466
return new GitCredential(userName, password);
6567
}
6668

67-
private ICredential GetCredentialsByUi(string resource, string userName)
69+
private async Task<ICredential> GetCredentialsByUiAsync(string helperPath, string resource, string userName)
6870
{
69-
if (!Context.SystemPrompts.ShowCredentialPrompt(resource, userName, out ICredential credential))
71+
var promptArgs = new StringBuilder("basic");
72+
73+
if (!string.IsNullOrWhiteSpace(resource))
74+
{
75+
promptArgs.AppendFormat(" --resource {0}", QuoteCmdArg(resource));
76+
}
77+
78+
if (!string.IsNullOrWhiteSpace(userName))
79+
{
80+
promptArgs.AppendFormat(" --username {0}", QuoteCmdArg(userName));
81+
}
82+
83+
IDictionary<string, string> resultDict = await InvokeHelperAsync(helperPath, promptArgs.ToString(), null);
84+
85+
if (!resultDict.TryGetValue("username", out userName))
86+
{
87+
throw new Exception("Missing 'username' in response");
88+
}
89+
90+
if (!resultDict.TryGetValue("password", out string password))
7091
{
71-
throw new Exception("User cancelled the authentication prompt.");
92+
throw new Exception("Missing 'password' in response");
7293
}
7394

74-
return credential;
95+
return new GitCredential(userName, password);
96+
}
97+
98+
private bool TryFindHelperExecutablePath(out string path)
99+
{
100+
return TryFindHelperExecutablePath(
101+
Constants.EnvironmentVariables.GcmUiHelper,
102+
Constants.GitConfiguration.Credential.UiHelper,
103+
Constants.DefaultUiHelper,
104+
out path);
75105
}
76106
}
77107
}

0 commit comments

Comments
 (0)