From f3b045aaaf28d77afeb32409867b8be9afe534e9 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Tue, 14 Nov 2023 11:09:05 -0800 Subject: [PATCH 01/27] Explicitly disallow multiple accounts on GitHub extension --- .../DeveloperId/LoginUIController.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index da19b399..0a5744fb 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -41,13 +41,22 @@ public IAsyncOperation OnAction(string action, string i { case LoginUIState.LoginPage: { - // Inputs are validated at this point. - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); - Log.Logger()?.ReportDebug($"inputs: {inputs}"); - try { - var devId = await (DeveloperIdProvider.GetInstance() as DeveloperIdProvider).LoginNewDeveloperIdAsync(); + // If there is already a developer id, we should block another login. + if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) + { + Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); + break; + } + + // Inputs are validated at this point. + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); + Log.Logger()?.ReportDebug($"inputs: {inputs}"); + + var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); if (devId != null) { var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); From fc2e7c879f03f7b5f760a8fb017f620bda016e6d Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sat, 11 Nov 2023 19:59:24 -0800 Subject: [PATCH 02/27] Fix constructor bug --- .../DeveloperId/DeveloperIdProvider.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 7fef1124..70830e5f 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -52,9 +52,15 @@ private DeveloperIdProvider() lock (DeveloperIdsLock) { DeveloperIds ??= new List(); - - // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(CredentialVault.GetAllSavedLoginIds()); + try + { + // Retrieve and populate Logged in DeveloperIds from previous launch. + RestoreDeveloperIds(CredentialVault.GetAllSavedLoginIds()); + } + catch (Exception error) + { + Log.Logger()?.ReportError($"Error while restoring DeveloperIds: {error.Message}. Proceeding without restoring."); + } } } From 8f5923842cdb6908ef800e213d80c226121c6bc4 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sun, 12 Nov 2023 17:19:51 -0800 Subject: [PATCH 03/27] Tested GHES --- .../Client/GithubClientProvider.cs | 13 ++++-- src/GitHubExtension/Constants.cs | 1 + .../DeveloperId/CredentialVault.cs | 2 +- .../DeveloperId/DeveloperId.cs | 4 +- .../DeveloperId/DeveloperIdProvider.cs | 40 +++++++++++++++---- .../Widgets/GitHubReviewWidget.cs | 2 +- src/GitHubExtension/Widgets/GitHubWidget.cs | 5 +-- 7 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/GitHubExtension/Client/GithubClientProvider.cs b/src/GitHubExtension/Client/GithubClientProvider.cs index 764ea64a..f8af81e9 100644 --- a/src/GitHubExtension/Client/GithubClientProvider.cs +++ b/src/GitHubExtension/Client/GithubClientProvider.cs @@ -82,9 +82,16 @@ public async Task GetClientForLoggedInDeveloper(bool logRateLimit } if (logRateLimit) - { - var miscRateLimit = await client.RateLimit.GetRateLimits(); - Log.Logger()?.ReportInfo($"Rate Limit: Remaining: {miscRateLimit.Resources.Core.Remaining} Total: {miscRateLimit.Resources.Core.Limit} Resets: {miscRateLimit.Resources.Core.Reset.ToStringInvariant()}"); + { + try + { + var miscRateLimit = await client.RateLimit.GetRateLimits(); + Log.Logger()?.ReportInfo($"Rate Limit: Remaining: {miscRateLimit.Resources.Core.Remaining} Total: {miscRateLimit.Resources.Core.Limit} Resets: {miscRateLimit.Resources.Core.Reset.ToStringInvariant()}"); + } + catch (Exception ex) + { + Log.Logger()?.ReportInfo($"Rate limiting not enabled for server: {ex.Message}"); + } } return client; diff --git a/src/GitHubExtension/Constants.cs b/src/GitHubExtension/Constants.cs index 28aa4b62..bdac2e0f 100644 --- a/src/GitHubExtension/Constants.cs +++ b/src/GitHubExtension/Constants.cs @@ -6,5 +6,6 @@ internal class Constants { #pragma warning disable SA1310 // Field names should not contain underscore public const string DEV_HOME_APPLICATION_NAME = "DevHome"; + public const string GITHUB_COM_URL = "https://api.github.com/"; #pragma warning restore SA1310 // Field names should not contain underscore } diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index ca24e133..2258bb1d 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -126,7 +126,7 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId) } } - public static IEnumerable GetAllSavedLoginIds() + public static IEnumerable GetAllSavedLoginIdsOrUrls() { var ptrToCredential = IntPtr.Zero; diff --git a/src/GitHubExtension/DeveloperId/DeveloperId.cs b/src/GitHubExtension/DeveloperId/DeveloperId.cs index 8d97599a..0b13852e 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperId.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperId.cs @@ -55,7 +55,7 @@ public Windows.Security.Credentials.PasswordCredential GetCredential(bool refres return RefreshDeveloperId(); } - return CredentialVault.GetCredentialFromLocker(LoginId); + return CredentialVault.GetCredentialFromLocker(Url); } public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() @@ -63,7 +63,7 @@ public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() // Setting to MaxValue, since GitHub doesn't forcibly expire tokens currently. CredentialExpiryTime = DateTime.MaxValue; DeveloperIdProvider.GetInstance().RefreshDeveloperId(this); - var credential = CredentialVault.GetCredentialFromLocker(LoginId); + var credential = CredentialVault.GetCredentialFromLocker(Url); GitHubClient.Credentials = new (credential.Password); return credential; } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 70830e5f..abb7e01d 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Net; using System.Security; using Microsoft.UI; using Microsoft.Windows.DevHome.SDK; @@ -55,7 +56,7 @@ private DeveloperIdProvider() try { // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(CredentialVault.GetAllSavedLoginIds()); + RestoreDeveloperIds(CredentialVault.GetAllSavedLoginIdsOrUrls()); } catch (Exception error) { @@ -109,7 +110,7 @@ public IAsyncOperation LoginNewDeveloperIdAsync() }).AsAsyncOperation(); } - private DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken) + public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken) { try { @@ -247,7 +248,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString try { // Save the credential to Credential Vault. - CredentialVault.SaveAccessTokenToVault(duplicateDeveloperIds.Single().LoginId, accessToken); + CredentialVault.SaveAccessTokenToVault(duplicateDeveloperIds.Single().Url, accessToken); try { @@ -302,14 +303,20 @@ private DeveloperId CreateOrUpdateDeveloperIdFromOauthRequest(OAuthRequest oauth return newDeveloperId; } - private void RestoreDeveloperIds(IEnumerable loginIds) + private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) { - foreach (var loginId in loginIds) + foreach (var loginIdOrUrl in loginIdsAndUrls) { - var gitHubClient = new GitHubClient(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)) + var isUrl = loginIdOrUrl.Contains('/'); + + // For loginIds without URL, use GitHub.com as default. + var hostAddress = isUrl ? new Uri(loginIdOrUrl) : new Uri(Constants.GITHUB_COM_URL); + + GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new Credentials(CredentialVault.GetCredentialFromLocker(loginId).Password), + Credentials = new (CredentialVault.GetCredentialFromLocker(loginIdOrUrl).Password), }; + var user = gitHubClient.User.Current().Result; DeveloperId developerId = new (user.Login, user.Name, user.Email, user.Url, gitHubClient); @@ -319,7 +326,24 @@ private void RestoreDeveloperIds(IEnumerable loginIds) DeveloperIds.Add(developerId); } - Log.Logger()?.ReportInfo($"Restored DeveloperId"); + Log.Logger()?.ReportInfo($"Restored DeveloperId {user.Url}"); + + // If loginId is currently used to save credential, remove it, and use URL instead. + if (!isUrl) + { + try + { + CredentialVault.SaveAccessTokenToVault( + user.Url, + new NetworkCredential(string.Empty, CredentialVault.GetCredentialFromLocker(loginIdOrUrl).Password).SecurePassword); + CredentialVault.RemoveAccessTokenFromVault(loginIdOrUrl); + Log.Logger()?.ReportInfo($"Replaced {loginIdOrUrl} with {user.Url} in CredentialManager"); + } + catch (Exception error) + { + Log.Logger()?.ReportError($"Error while replacing {loginIdOrUrl} with {user.Url} in CredentialManager: {error.Message}"); + } + } } return; diff --git a/src/GitHubExtension/Widgets/GitHubReviewWidget.cs b/src/GitHubExtension/Widgets/GitHubReviewWidget.cs index f52f42e3..1c311580 100644 --- a/src/GitHubExtension/Widgets/GitHubReviewWidget.cs +++ b/src/GitHubExtension/Widgets/GitHubReviewWidget.cs @@ -129,7 +129,7 @@ public override void RequestContentData() UsePublicClientAsFallback = true, }; - SearchIssuesRequest request = new SearchIssuesRequest($"review-requested:{ReferredName}"); + var request = new SearchIssuesRequest($"review-requested:{ReferredName}"); var searchManager = GitHubSearchManager.CreateInstance(); searchManager?.SearchForGitHubIssuesOrPRs(request, Name, SearchCategory.PullRequests, requestOptions); Log.Logger()?.ReportInfo(Name, ShortId, $"Requested search for {referredName}"); diff --git a/src/GitHubExtension/Widgets/GitHubWidget.cs b/src/GitHubExtension/Widgets/GitHubWidget.cs index 90865037..4f944a91 100644 --- a/src/GitHubExtension/Widgets/GitHubWidget.cs +++ b/src/GitHubExtension/Widgets/GitHubWidget.cs @@ -245,11 +245,8 @@ public string GetConfiguration(string data) try { // Get client for logged in user. - var client = GitHubClientProvider.Instance.GetClientForLoggedInDeveloper(true).Result; - if (client == null) - { + var client = GitHubClientProvider.Instance.GetClientForLoggedInDeveloper(true).Result ?? throw new InvalidOperationException("Failed getting GitHubClient."); - } // Get repository for the URL, which is "data" in this case. var ownerName = Validation.ParseOwnerFromGitHubURL(data); From 2c4ed2efb9d6f40b05655108dd6a681d8e805549 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Tue, 14 Nov 2023 10:52:29 -0800 Subject: [PATCH 04/27] Stashing --- .../DeveloperId/LoginUIController.cs | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index 0a5744fb..429b672b 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Windows.ApplicationModel.Resources; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; @@ -275,6 +277,238 @@ internal string GetLoginUITemplate(string loginUIState) ""verticalContentAlignment"": ""Top"", ""rtl"": false } +"; + + var enterpriseServerPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""ColumnSet"", + ""spacing"": ""Large"", + ""columns"": [ + { + ""type"": ""Column"", + ""items"": [ + { + ""type"": ""Image"", + ""style"": ""Person"", + ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", + ""size"": ""Small"", + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TextBlock"", + ""weight"": ""Bolder"", + ""text"": ""GitHub"", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""Small"", + ""size"": ""Large"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""Enterprise Server"", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"", + ""size"": ""Small"" + }, + { + ""type"": ""TextBlock"", + ""text"": """", + ""wrap"": true, + ""spacing"": ""Large"", + ""horizontalAlignment"": ""Center"", + ""isSubtle"": true + } + ], + ""width"": ""stretch"", + ""separator"": true, + ""spacing"": ""Medium"" + } + ] + }, + { + ""type"": ""Input.Text"", + ""placeholder"": ""Enter server address here"", + ""id"": ""EnterpriseServer"", + ""style"": ""Url"", + ""isRequired"": true, + ""spacing"": ""ExtraLarge"" + }, + { + ""type"": ""ColumnSet"", + ""horizontalAlignment"": ""Center"", + ""height"": ""stretch"", + ""columns"": [ + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": ""Cancel"", + ""id"": ""Cancel"", + ""role"": ""Button"" + } + ] + } + ] + }, + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": ""Next"", + ""id"": ""Next"", + ""style"": ""positive"", + ""role"": ""Button"" + } + ] + } + ] + } + ], + ""spacing"": ""Small"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""350px"", + ""verticalContentAlignment"": ""Top"", + ""rtl"": false +} +"; + + var enterpriseServerPATPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""ColumnSet"", + ""spacing"": ""Large"", + ""columns"": [ + { + ""type"": ""Column"", + ""items"": [ + { + ""type"": ""Image"", + ""style"": ""Person"", + ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", + ""size"": ""Small"", + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TextBlock"", + ""weight"": ""Bolder"", + ""text"": ""GitHub"", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""Small"", + ""size"": ""Large"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""Enterprise Server"", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"", + ""size"": ""Small"" + } + ], + ""width"": ""stretch"", + ""separator"": true, + ""spacing"": ""Medium"" + } + ] + }, + { + ""type"": ""RichTextBlock"", + ""inlines"": [ + { + ""type"": ""TextRun"", + ""text"": ""Please enter your Personal Access Token (PAT) to connect to . To create a new PAT, "" + }, + { + ""type"": ""TextRun"", + ""text"": ""click here."", + ""selectAction"": { + ""type"": ""Action.OpenUrl"", + ""url"": ""https://adaptivecards.io"" + } + } + ] + }, + { + ""type"": ""Input.Text"", + ""placeholder"": ""Enter personal access token"", + ""id"": ""EnterpriseServer"", + ""style"": ""Url"", + ""isRequired"": true, + ""spacing"": ""Large"", + ""errorMessage"": ""Invalid Url"" + }, + { + ""type"": ""ColumnSet"", + ""horizontalAlignment"": ""Center"", + ""height"": ""stretch"", + ""columns"": [ + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": ""Cancel"", + ""id"": ""Cancel"", + ""role"": ""Button"" + } + ] + } + ] + }, + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": ""Connect"", + ""id"": ""Connect"", + ""style"": ""positive"", + ""role"": ""Button"" + } + ] + } + ] + } + ], + ""spacing"": ""Small"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""350px"", + ""verticalContentAlignment"": ""Top"", + ""rtl"": false +} "; var waitingPage = @" @@ -367,6 +601,16 @@ internal string GetLoginUITemplate(string loginUIState) return loginPage; } + case LoginUIState.EnterpriseServerPage: + { + return enterpriseServerPage; + } + + case LoginUIState.EnterpriseServerPATPage: + { + return enterpriseServerPATPage; + } + case LoginUIState.WaitingPage: { return waitingPage; @@ -390,10 +634,38 @@ internal string GetLoginUITemplate(string loginUIState) } } + private class LoginPageActionPayload + { + public string? Style + { + get; set; + } + + public string? Title + { + get; set; + } + + public string? Type + { + get; set; + } + } + + private class LoginPageInputPayload + { + public string? EnterpriseServer + { + get; set; + } + } + // This class cannot be an enum, since we are passing this to the core app as State parameter. private class LoginUIState { internal const string LoginPage = "LoginPage"; + internal const string EnterpriseServerPage = "EnterpriseServerPage"; + internal const string EnterpriseServerPATPage = "EnterpriseServerPATPage"; internal const string WaitingPage = "WaitingPage"; internal const string LoginFailedPage = "LoginFailedPage"; internal const string LoginSucceededPage = "LoginSucceededPage"; From 2355ed8a8e4c2bfa31ab108ea313edf50e48c985 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Thu, 16 Nov 2023 15:50:09 -0800 Subject: [PATCH 05/27] Basic flow works --- .../DeveloperId/LoginUIController.cs | 163 ++++++++++++++---- 1 file changed, 128 insertions(+), 35 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index 429b672b..d00fb13b 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Net; using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; using Microsoft.Windows.ApplicationModel.Resources; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; @@ -12,6 +14,7 @@ internal class LoginUIController : IExtensionAdaptiveCardSession { private IExtensionAdaptiveCard? _loginUI; private static readonly LoginUITemplate _loginUITemplate = new (); + private Uri? _hostAddress; public LoginUIController() { @@ -46,17 +49,29 @@ public IAsyncOperation OnAction(string action, string i try { // If there is already a developer id, we should block another login. - if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) + /*if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) { Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); break; - } + }*/ // Inputs are validated at this point. _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); - Log.Logger()?.ReportDebug($"inputs: {inputs}"); + var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid action"); + + if (loginPageActionPayload?.Id == "Enterprise") + { + Log.Logger()?.ReportInfo($"Show Enterprise Page"); + + // Update UI with Enterprise Server page and return. + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), null, LoginUIState.EnterpriseServerPage); + break; + } var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); if (devId != null) @@ -81,6 +96,84 @@ public IAsyncOperation OnAction(string action, string i break; } + case LoginUIState.EnterpriseServerPage: + { + Log.Logger()?.ReportDebug($"inputs: {inputs}"); + var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid inputs"); + Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); + + // TODO: Validate inputs. Check that server is reachable + if (enterprisePageInputPayload?.EnterpriseServer == null) + { + Log.Logger()?.ReportError($"EnterpriseServer is null"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "EnterpriseServer is null", "EnterpriseServer is null"); + break; + } + + _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); + + if (false/* server unreachable using Octokit */) + { +#pragma warning disable CS0162 // Unreachable code detected + Log.Logger()?.ReportError($"{enterprisePageInputPayload?.EnterpriseServer} isn't a valid GHES endpoint"); +#pragma warning restore CS0162 // Unreachable code detected + } + else + { + // Update the PAT page with the server input + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), null, LoginUIState.EnterpriseServerPATPage); + } + + break; + } + + case LoginUIState.EnterpriseServerPATPage: + { + Log.Logger()?.ReportDebug($"inputs: {inputs}"); + var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid inputs"); + Log.Logger()?.ReportInfo($"PAT Received"); + + if (enterprisePATPageInputPayload?.PAT == null) + { + Log.Logger()?.ReportError($"PAT is null"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "PAT is null", "PAT is null"); + break; + } + + // TODO: Call login with PAT + if (_hostAddress == null) + { + Log.Logger()?.ReportError($"Host address is null"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Host address is null", "Host address is null"); + break; + } + + var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; + var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); + + if (devId != null) + { + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage).Replace("${message}", $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}"), null, LoginUIState.LoginSucceededPage); + } + else + { + Log.Logger()?.ReportError($"PAT doesn't work for the given GHES endpoint"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); + + // TODO: replace this with UI Update within PAT page + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + } + + break; + } + // These pages only have close actions. case LoginUIState.LoginSucceededPage: case LoginUIState.LoginFailedPage: @@ -215,37 +308,11 @@ internal string GetLoginUITemplate(string loginUIState) ""type"": ""ActionSet"", ""actions"": [ { - ""type"": ""Action.ShowCard"", + ""type"": ""Action.Submit"", ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Text")}" + @""", - ""isEnabled"": false, + ""isEnabled"": true, ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2ToolTip")}" + @""", - ""id"": ""Enterprise"", - ""card"": { - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""Input.Text"", - ""placeholder"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Flyout_Text_PlaceHolder")}" + @""", - ""style"": ""Url"", - ""isRequired"": true, - ""id"": ""Enterprise.server"", - ""label"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Flyout_Text_Label")}" + @""", - ""value"": ""github.com"", - ""errorMessage"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Flyout_Text_ErrorMessage")}" + @""" - }, - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Flyout_Button")}" + @""", - ""style"": ""positive"", - ""associatedInputs"": ""auto"" - } - ] - } - ] - } + ""id"": ""Enterprise"" } ], ""spacing"": ""None"", @@ -453,7 +520,7 @@ internal string GetLoginUITemplate(string loginUIState) { ""type"": ""Input.Text"", ""placeholder"": ""Enter personal access token"", - ""id"": ""EnterpriseServer"", + ""id"": ""PAT"", ""style"": ""Url"", ""isRequired"": true, ""spacing"": ""Large"", @@ -634,13 +701,23 @@ internal string GetLoginUITemplate(string loginUIState) } } - private class LoginPageActionPayload + private class ButtonClickActionPayload { + public string? Id + { + get; set; + } + public string? Style { get; set; } + public string? ToolTip + { + get; set; + } + public string? Title { get; set; @@ -652,7 +729,15 @@ public string? Type } } - private class LoginPageInputPayload + private class LoginPageActionPayload : ButtonClickActionPayload + { + } + + private class EnterprisePageActionPayload : ButtonClickActionPayload + { + } + + private class EnterprisePageInputPayload { public string? EnterpriseServer { @@ -660,6 +745,14 @@ public string? EnterpriseServer } } + private class EnterprisePATPageInputPayload + { + public string? PAT + { + get; set; + } + } + // This class cannot be an enum, since we are passing this to the core app as State parameter. private class LoginUIState { From cb53cc664c8904c3c93f00aa69b4e9f914267234 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Thu, 16 Nov 2023 22:48:52 -0800 Subject: [PATCH 06/27] Validation added --- .../DeveloperId/LoginUIController.cs | 104 +++++++++++++++--- 1 file changed, 86 insertions(+), 18 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index d00fb13b..e7247753 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -7,6 +7,7 @@ using GitHubExtension.Helpers; using Microsoft.Windows.ApplicationModel.Resources; using Microsoft.Windows.DevHome.SDK; +using Octokit; using Windows.Foundation; namespace GitHubExtension.DeveloperId; @@ -98,6 +99,20 @@ public IAsyncOperation OnAction(string action, string i case LoginUIState.EnterpriseServerPage: { + // Check if the user clicked on Cancel button. + var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid action"); + + if (enterprisePageActionPayload?.Id == "Cancel") + { + Log.Logger()?.ReportInfo($"Cancel clicked"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginPage), null, LoginUIState.LoginPage); + break; + } + + // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. Log.Logger()?.ReportDebug($"inputs: {inputs}"); var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions { @@ -105,33 +120,69 @@ public IAsyncOperation OnAction(string action, string i }) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); - // TODO: Validate inputs. Check that server is reachable if (enterprisePageInputPayload?.EnterpriseServer == null) { Log.Logger()?.ReportError($"EnterpriseServer is null"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "EnterpriseServer is null", "EnterpriseServer is null"); + + // TODO: replace this with UI Update within Enterprise page break; } - _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); + try + { + _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); + var gitHubClient = new GitHubClient(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), _hostAddress); + + // set timeout to 1 second + gitHubClient.Connection.SetRequestTimeout(System.TimeSpan.FromSeconds(1)); + var enterpriseVersion = (await gitHubClient.Meta.GetMetadata()).InstalledVersion ?? throw new InvalidOperationException(); - if (false/* server unreachable using Octokit */) + // If we are able to get the version, we can assume that the endpoint is valid. + Log.Logger()?.ReportInfo($"Enterprise Server version: {enterpriseVersion}"); + } + catch (Octokit.NotFoundException nfe) { -#pragma warning disable CS0162 // Unreachable code detected - Log.Logger()?.ReportError($"{enterprisePageInputPayload?.EnterpriseServer} isn't a valid GHES endpoint"); -#pragma warning restore CS0162 // Unreachable code detected + Log.Logger()?.ReportError($"{_hostAddress?.OriginalString} isn't a valid GHES endpoint"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, nfe, $"Octokit client could not be created with {_hostAddress?.OriginalString}", nfe.Message); + + // TODO: replace this with UI Update within Enterprise page + break; + } + catch (UriFormatException ufe) + { + Log.Logger()?.ReportError($"Error: {ufe}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ufe, $"{enterprisePageInputPayload.EnterpriseServer} isn't a valid URI", ufe.Message); + + // TODO: replace this with UI Update within Enterprise page + break; } - else + catch (Exception ex) { - // Update the PAT page with the server input - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), null, LoginUIState.EnterpriseServerPATPage); + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, $"Octokit client could not be created with {_hostAddress?.OriginalString}", ex.Message); + break; } + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), null, LoginUIState.EnterpriseServerPATPage); break; } case LoginUIState.EnterpriseServerPATPage: { + // Check if the user clicked on Cancel button. + var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid action"); + + if (enterprisePATPageActionPayload?.Id == "Cancel") + { + Log.Logger()?.ReportInfo($"Cancel clicked"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), null, LoginUIState.EnterpriseServerPage); + break; + } + Log.Logger()?.ReportDebug($"inputs: {inputs}"); var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions { @@ -143,32 +194,45 @@ public IAsyncOperation OnAction(string action, string i { Log.Logger()?.ReportError($"PAT is null"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "PAT is null", "PAT is null"); + + // TODO: replace this with UI Update within Enterprise page break; } - // TODO: Call login with PAT if (_hostAddress == null) { + // This should never happen. Log.Logger()?.ReportError($"Host address is null"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Host address is null", "Host address is null"); break; } var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; - var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); - if (devId != null) + try { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage).Replace("${message}", $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}"), null, LoginUIState.LoginSucceededPage); + var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); + + if (devId != null) + { + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage).Replace("${message}", $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}"), null, LoginUIState.LoginSucceededPage); + } + else + { + Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); + + // TODO: replace this with UI Update within PAT page + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + } } - else + catch (Exception ex) { - Log.Logger()?.ReportError($"PAT doesn't work for the given GHES endpoint"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); // TODO: replace this with UI Update within PAT page - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); } break; @@ -737,6 +801,10 @@ private class EnterprisePageActionPayload : ButtonClickActionPayload { } + private class EnterprisePATPageActionPayload : ButtonClickActionPayload + { + } + private class EnterprisePageInputPayload { public string? EnterpriseServer From 1e48c331bba9a758694d0405e6a03d66299fc752 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Thu, 16 Nov 2023 23:30:48 -0800 Subject: [PATCH 07/27] Minor updates --- .../DeveloperId/LoginUIController.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index e7247753..bb4f8f95 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -39,11 +39,17 @@ public IAsyncOperation OnAction(string action, string i { return Task.Run(async () => { + if (_loginUI == null) + { + Log.Logger()?.ReportError($"_loginUI is null"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "_loginUI is null", "_loginUI is null"); + } + ProviderOperationResult operationResult; - Log.Logger()?.ReportInfo($"OnAction() called with state:{_loginUI?.State}"); + Log.Logger()?.ReportInfo($"OnAction() called with state:{_loginUI.State}"); Log.Logger()?.ReportDebug($"action: {action}"); - switch (_loginUI?.State) + switch (_loginUI.State) { case LoginUIState.LoginPage: { @@ -179,6 +185,8 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePATPageActionPayload?.Id == "Cancel") { Log.Logger()?.ReportInfo($"Cancel clicked"); + + // TODO: Replace Hostaddress in template with the one entered by user in Enterprise page already operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), null, LoginUIState.EnterpriseServerPage); break; } @@ -251,8 +259,8 @@ public IAsyncOperation OnAction(string action, string i case LoginUIState.WaitingPage: default: { - Log.Logger()?.ReportError($"Unexpected state:{_loginUI?.State}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error occurred in :{_loginUI?.State}", $"Error occurred in :{_loginUI?.State}"); + Log.Logger()?.ReportError($"Unexpected state:{_loginUI.State}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error occurred in :{_loginUI.State}", $"Error occurred in :{_loginUI.State}"); break; } } From 452f39ddde9724f6c8a1d31fa666500d7936d583 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 17 Nov 2023 10:30:43 -0800 Subject: [PATCH 08/27] Minor Widget update --- .../Widgets/GitHubMentionedInWidget.cs | 44 +++++++------------ 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs b/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs index 0667fac8..edfcecc8 100644 --- a/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs +++ b/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs @@ -7,6 +7,7 @@ using GitHubExtension.DataManager; using GitHubExtension.Helpers; using GitHubExtension.Widgets.Enums; +using Microsoft.Windows.DevHome.SDK; using Microsoft.Windows.Widgets.Providers; using Octokit; @@ -44,7 +45,9 @@ internal class GitHubMentionedInWidget : GitHubWidget private static Dictionary Templates { get; set; } = new (); - protected static readonly new string Name = nameof(GitHubMentionedInWidget); + protected static readonly new string Name = nameof(GitHubMentionedInWidget); + + private readonly IDeveloperId? mentionedDeveloperId = DeveloperId.DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIds().DeveloperIds.Last(); private SearchCategory ShowCategory { @@ -55,21 +58,7 @@ private SearchCategory ShowCategory private SearchCategory? savedShowCategory; - private string mentionedName = string.Empty; - - private string MentionedName - { - get - { - if (string.IsNullOrEmpty(mentionedName)) - { - GetMentionedName(); - } - - return mentionedName; - } - set => mentionedName = value; - } + private string MentionedName => (mentionedDeveloperId != null) ? mentionedDeveloperId.LoginId : string.Empty; public GitHubMentionedInWidget() : base() @@ -83,15 +72,6 @@ public GitHubMentionedInWidget() GitHubSearchManager.OnResultsAvailable -= SearchManagerResultsAvailableHandler; } - private void GetMentionedName() - { - var devIds = DeveloperId.DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal(); - if ((devIds != null) && devIds.Any()) - { - mentionedName = devIds.First().LoginId; - } - } - public override void DeleteWidget(string widgetId, string customState) { // Remove event handler. @@ -173,8 +153,14 @@ public override void RequestContentData() } try - { - Log.Logger()?.ReportInfo(Name, ShortId, $"Requesting search for mentioned user {mentionedName}"); + { + if (mentionedDeveloperId == null) + { + Log.Logger()?.ReportError($"MentionedDeveloperId is null"); + return; + } + + Log.Logger()?.ReportInfo(Name, ShortId, $"Requesting search for mentioned user {mentionedDeveloperId.LoginId}"); var requestOptions = new RequestOptions { ApiOptions = new ApiOptions @@ -192,8 +178,8 @@ public override void RequestContentData() }; var searchManager = GitHubSearchManager.CreateInstance(); - searchManager?.SearchForGitHubIssuesOrPRs(request, Name, ShowCategory, requestOptions); - Log.Logger()?.ReportInfo(Name, ShortId, $"Requested search for {mentionedName}"); + searchManager?.SearchForGitHubIssuesOrPRs(request, Name, ShowCategory, mentionedDeveloperId, requestOptions); + Log.Logger()?.ReportInfo(Name, ShortId, $"Requested search for {mentionedDeveloperId?.LoginId}"); DataState = WidgetDataState.Requested; } catch (Exception ex) From 84ca2ffb8a345e16540d756efc3118d695568bb8 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 17 Nov 2023 10:31:49 -0800 Subject: [PATCH 09/27] SearchManager minor update --- .../DataManager/GitHubSearchManger.cs | 40 ++++++++++++++++++- .../DataManager/IGitHubSearchManager.cs | 7 +++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/GitHubExtension/DataManager/GitHubSearchManger.cs b/src/GitHubExtension/DataManager/GitHubSearchManger.cs index ee3bae17..b026bcc9 100644 --- a/src/GitHubExtension/DataManager/GitHubSearchManger.cs +++ b/src/GitHubExtension/DataManager/GitHubSearchManger.cs @@ -4,7 +4,8 @@ using GitHubExtension.Client; using GitHubExtension.DataManager; using GitHubExtension.DataModel; - +using Microsoft.Windows.DevHome.SDK; + namespace GitHubExtension; public delegate void SearchManagerResultsAvailableEventHandler(IEnumerable results, string resultType); @@ -31,7 +32,42 @@ public GitHubSearchManager() Environment.FailFast(e.Message, e); return null; } - } + } + + public async Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, IDeveloperId developerId, RequestOptions? options = null) + { + Log.Logger()?.ReportInfo(Name, $"Searching for issues or pull requests for widget {initiator}"); + request.State = Octokit.ItemState.Open; + request.Archived = false; + request.PerPage = 10; + request.SortField = Octokit.IssueSearchSort.Updated; + request.Order = Octokit.SortDirection.Descending; + + var client = GitHubClientProvider.Instance.GetClient(developerId.Url) ?? throw new InvalidOperationException($"Client does not exist for {developerId.Url}"); + + // Set is: parameter according to the search category. + // For the case we are searching for both we don't have to set the parameter + if (category.Equals(SearchCategory.Issues)) + { + request.Is = new List() { Octokit.IssueIsQualifier.Issue }; + } + else if (category.Equals(SearchCategory.PullRequests)) + { + request.Is = new List() { Octokit.IssueIsQualifier.PullRequest }; + } + + var octokitResult = await client.Search.SearchIssues(request); + if (octokitResult == null) + { + Log.Logger()?.ReportDebug($"No issues or PRs found."); + SendResultsAvailable(new List(), initiator); + } + else + { + Log.Logger()?.ReportDebug(Name, $"Results contain {octokitResult.Items.Count} items."); + SendResultsAvailable(octokitResult.Items, initiator); + } + } public async Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null) { diff --git a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs index ebf345e2..dad5a644 100644 --- a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs +++ b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs @@ -2,10 +2,13 @@ // Licensed under the MIT license. using GitHubExtension.DataManager; - +using Microsoft.Windows.DevHome.SDK; + namespace GitHubExtension; public interface IGitHubSearchManager : IDisposable { - Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null); + Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null); + + Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, IDeveloperId developerId, RequestOptions? options = null); } From 542305af29442ef6eca4b31cf75b4842d2796ca7 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Thu, 23 Nov 2023 10:42:07 -0800 Subject: [PATCH 10/27] Fixed LoginUI --- src/GitHubExtension/Client/Validation.cs | 11 + .../DeveloperId/DeveloperIdProvider.cs | 4 +- .../DeveloperId/LoginUIController.cs | 487 +++++++++++------- .../DeveloperId/OAuthRequest.cs | 2 +- .../Strings/en-US/Resources.resw | 63 ++- 5 files changed, 373 insertions(+), 194 deletions(-) diff --git a/src/GitHubExtension/Client/Validation.cs b/src/GitHubExtension/Client/Validation.cs index 79e1332d..fb1f088d 100644 --- a/src/GitHubExtension/Client/Validation.cs +++ b/src/GitHubExtension/Client/Validation.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. using GitHubExtension.DataModel; +using Octokit; namespace GitHubExtension.Client; @@ -217,4 +218,14 @@ private static string AddProtocolToString(string s) return n; } + + public static bool IsReachableGitHubEnterpriseServerURL(Uri server) + { + if (new EnterpriseProbe(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)).Probe(server).Result != EnterpriseProbeResult.Ok) + { + return false; + } + + return true; + } } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index abb7e01d..c70c6eaa 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -166,7 +166,7 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) return new ProviderOperationResult(ProviderOperationStatus.Failure, new ArgumentNullException(nameof(developerId)), "The developer account to log out does not exist", "Unable to find DeveloperId to logout"); } - CredentialVault.RemoveAccessTokenFromVault(developerIdToLogout.LoginId); + CredentialVault.RemoveAccessTokenFromVault(developerIdToLogout.Url); DeveloperIds?.Remove(developerIdToLogout); } @@ -272,7 +272,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString DeveloperIds.Add(newDeveloperId); } - CredentialVault.SaveAccessTokenToVault(newDeveloperId.LoginId, accessToken); + CredentialVault.SaveAccessTokenToVault(newDeveloperId.Url, accessToken); try { diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index bb4f8f95..d108bf4a 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Drawing.Drawing2D; using System.Net; using System.Text.Json; -using System.Text.Json.Serialization; -using GitHubExtension.Helpers; -using Microsoft.Windows.ApplicationModel.Resources; +using GitHubExtension.Client; using Microsoft.Windows.DevHome.SDK; -using Octokit; +using Windows.ApplicationModel.Activation; using Windows.Foundation; +using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; namespace GitHubExtension.DeveloperId; internal class LoginUIController : IExtensionAdaptiveCardSession @@ -52,217 +52,271 @@ public IAsyncOperation OnAction(string action, string i switch (_loginUI.State) { case LoginUIState.LoginPage: - { - try { - // If there is already a developer id, we should block another login. - /*if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) + try + { + // If there is already a developer id, we should block another login. + /*if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) + { + Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); + break; + }*/ + + // Inputs are validated at this point. + var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid action"); + + if (loginPageActionPayload?.Id == "Enterprise") + { + Log.Logger()?.ReportInfo($"Show Enterprise Page"); + + // Update UI with Enterprise Server page and return. + var pageData = new EnterpriseServerPageData() + { + EnterpriseServerInputValue = string.Empty, + EnterpriseServerPageErrorValue = string.Empty, + EnterpriseServerPageErrorVisible = false, + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + break; + } + + // Display Waiting page before Browser launch in LoginNewDeveloperIdAsync() + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); + var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); + if (devId != null) + { + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + var pageData = new LoginSucceededPageData + { + Message = $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage), JsonSerializer.Serialize(pageData), LoginUIState.LoginSucceededPage); + } + else + { + Log.Logger()?.ReportError($"Unable to create DeveloperId"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); + _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + } + } + catch (Exception ex) { - Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); - break; - }*/ + } + + break; + } - // Inputs are validated at this point. - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); - var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + case LoginUIState.EnterpriseServerPage: + { + // Check if the user clicked on Cancel button. + var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid action"); - if (loginPageActionPayload?.Id == "Enterprise") + if (enterprisePageActionPayload?.Id == "Cancel") { - Log.Logger()?.ReportInfo($"Show Enterprise Page"); + Log.Logger()?.ReportInfo($"Cancel clicked"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginPage), null, LoginUIState.LoginPage); + break; + } - // Update UI with Enterprise Server page and return. - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), null, LoginUIState.EnterpriseServerPage); + // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. + Log.Logger()?.ReportDebug($"inputs: {inputs}"); + var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid inputs"); + Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); + + if (enterprisePageInputPayload?.EnterpriseServer == null) + { + Log.Logger()?.ReportError($"EnterpriseServer is null"); + var pageData = new EnterpriseServerPageData() + { + EnterpriseServerInputValue = string.Empty, + EnterpriseServerPageErrorValue = "EnterpriseServer is null", + EnterpriseServerPageErrorVisible = true, + }; + + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); break; } - var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); - if (devId != null) + try { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage).Replace("${message}", $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}"), null, LoginUIState.LoginSucceededPage); + // Probe for Enterprise Server instance + _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); + if (!Validation.IsReachableGitHubEnterpriseServerURL(_hostAddress)) + { + var pageData = new EnterpriseServerPageData() + { + EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, + EnterpriseServerPageErrorValue = "Enterprise Server is not reachable", + EnterpriseServerPageErrorVisible = true, + }; + + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + break; + } } - else + catch (UriFormatException ufe) { - Log.Logger()?.ReportError($"Unable to create DeveloperId"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + Log.Logger()?.ReportError($"Error: {ufe}"); + var pageData = new EnterpriseServerPageData() + { + EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, + EnterpriseServerPageErrorValue = "Enterprise Server URL is invalid", + EnterpriseServerPageErrorVisible = true, + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + break; + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error: {ex}"); + var pageData = new EnterpriseServerPageData() + { + EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, + EnterpriseServerPageErrorValue = $"Somthing went wrong: {ex}", + EnterpriseServerPageErrorVisible = true, + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + break; } - } - catch (Exception ex) - { - Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); - } - - break; - } - - case LoginUIState.EnterpriseServerPage: - { - // Check if the user clicked on Cancel button. - var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid action"); - - if (enterprisePageActionPayload?.Id == "Cancel") - { - Log.Logger()?.ReportInfo($"Cancel clicked"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginPage), null, LoginUIState.LoginPage); - break; - } - - // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. - Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid inputs"); - Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); - - if (enterprisePageInputPayload?.EnterpriseServer == null) - { - Log.Logger()?.ReportError($"EnterpriseServer is null"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "EnterpriseServer is null", "EnterpriseServer is null"); - - // TODO: replace this with UI Update within Enterprise page - break; - } - - try - { - _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); - var gitHubClient = new GitHubClient(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), _hostAddress); - - // set timeout to 1 second - gitHubClient.Connection.SetRequestTimeout(System.TimeSpan.FromSeconds(1)); - var enterpriseVersion = (await gitHubClient.Meta.GetMetadata()).InstalledVersion ?? throw new InvalidOperationException(); - - // If we are able to get the version, we can assume that the endpoint is valid. - Log.Logger()?.ReportInfo($"Enterprise Server version: {enterpriseVersion}"); - } - catch (Octokit.NotFoundException nfe) - { - Log.Logger()?.ReportError($"{_hostAddress?.OriginalString} isn't a valid GHES endpoint"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, nfe, $"Octokit client could not be created with {_hostAddress?.OriginalString}", nfe.Message); - // TODO: replace this with UI Update within Enterprise page - break; - } - catch (UriFormatException ufe) - { - Log.Logger()?.ReportError($"Error: {ufe}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ufe, $"{enterprisePageInputPayload.EnterpriseServer} isn't a valid URI", ufe.Message); + var pageData1 = new EnterpriseServerPATPageData() + { + EnterpriseServerPATPageErrorValue = string.Empty, + EnterpriseServerPATPageErrorVisible = false, + EnterpriseServerPATPageInputValue = string.Empty, + EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", + EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, + }; + try + { + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData1), LoginUIState.EnterpriseServerPATPage); + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + } - // TODO: replace this with UI Update within Enterprise page break; } - catch (Exception ex) - { - Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, $"Octokit client could not be created with {_hostAddress?.OriginalString}", ex.Message); - break; - } - - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), null, LoginUIState.EnterpriseServerPATPage); - break; - } case LoginUIState.EnterpriseServerPATPage: - { - // Check if the user clicked on Cancel button. - var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid action"); - - if (enterprisePATPageActionPayload?.Id == "Cancel") - { - Log.Logger()?.ReportInfo($"Cancel clicked"); + if (_hostAddress == null) + { + // This should never happen. + Log.Logger()?.ReportError($"Host address is null"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Host address is null", "Host address is null"); + break; + } - // TODO: Replace Hostaddress in template with the one entered by user in Enterprise page already - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), null, LoginUIState.EnterpriseServerPage); - break; - } + // Check if the user clicked on Cancel button. + var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid action"); - Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid inputs"); - Log.Logger()?.ReportInfo($"PAT Received"); + if (enterprisePATPageActionPayload?.Id == "Cancel") + { + Log.Logger()?.ReportInfo($"Cancel clicked"); - if (enterprisePATPageInputPayload?.PAT == null) - { - Log.Logger()?.ReportError($"PAT is null"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "PAT is null", "PAT is null"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(new EnterpriseServerPageData()), LoginUIState.EnterpriseServerPage); + break; + } - // TODO: replace this with UI Update within Enterprise page - break; - } + Log.Logger()?.ReportDebug($"inputs: {inputs}"); + var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + }) ?? throw new InvalidOperationException("Invalid inputs"); + Log.Logger()?.ReportInfo($"PAT Received"); - if (_hostAddress == null) - { - // This should never happen. - Log.Logger()?.ReportError($"Host address is null"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Host address is null", "Host address is null"); - break; - } + if (enterprisePATPageInputPayload?.PAT == null) + { + Log.Logger()?.ReportError($"PAT is null"); + var pageData = new EnterpriseServerPATPageData + { + EnterpriseServerPATPageInputValue = enterprisePATPageInputPayload?.PAT, + EnterpriseServerPATPageErrorValue = $"Please enter the PAT", + EnterpriseServerPATPageErrorVisible = true, + EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", + EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPATPage); - var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; + break; + } - try - { - var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); + var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; - if (devId != null) + try { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage).Replace("${message}", $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}"), null, LoginUIState.LoginSucceededPage); + var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); + + if (devId != null) + { + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + var pageData = new LoginSucceededPageData() + { + Message = $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage), JsonSerializer.Serialize(pageData), LoginUIState.LoginSucceededPage); + break; + } + else + { + Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}"); + var pageData = new EnterpriseServerPATPageData + { + EnterpriseServerPATPageInputValue = enterprisePATPageInputPayload?.PAT, + EnterpriseServerPATPageErrorValue = $"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}", + EnterpriseServerPATPageErrorVisible = true, + EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", + EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, + }; + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPATPage); + break; + } } - else + catch (Exception ex) { - Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); - - // TODO: replace this with UI Update within PAT page - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + break; } } - catch (Exception ex) - { - Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); - - // TODO: replace this with UI Update within PAT page - } - - break; - } // These pages only have close actions. case LoginUIState.LoginSucceededPage: case LoginUIState.LoginFailedPage: - { - Log.Logger()?.ReportInfo($"State:{_loginUI.State}"); - operationResult = _loginUI.Update(null, null, LoginUIState.End); - break; - } + { + Log.Logger()?.ReportInfo($"State:{_loginUI.State}"); + operationResult = _loginUI.Update(null, null, LoginUIState.End); + break; + } // These pages do not have any actions. We should never be here. case LoginUIState.WaitingPage: default: - { - Log.Logger()?.ReportError($"Unexpected state:{_loginUI.State}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error occurred in :{_loginUI.State}", $"Error occurred in :{_loginUI.State}"); - break; - } + { + Log.Logger()?.ReportError($"Unexpected state:{_loginUI.State}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error occurred in :{_loginUI.State}", $"Error occurred in :{_loginUI.State}"); + break; + } } return operationResult; @@ -357,7 +411,6 @@ internal string GetLoginUITemplate(string loginUIState) ""spacing"": ""None"" } ], - ""isVisible"": true, ""verticalContentAlignment"": ""Center"", ""height"": ""stretch"", ""spacing"": ""None"", @@ -440,7 +493,7 @@ internal string GetLoginUITemplate(string loginUIState) { ""type"": ""TextBlock"", ""weight"": ""Bolder"", - ""text"": ""GitHub"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""Small"", @@ -448,7 +501,7 @@ internal string GetLoginUITemplate(string loginUIState) }, { ""type"": ""TextBlock"", - ""text"": ""Enterprise Server"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""None"", @@ -471,11 +524,21 @@ internal string GetLoginUITemplate(string loginUIState) }, { ""type"": ""Input.Text"", - ""placeholder"": ""Enter server address here"", + ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePage_InputText_PlaceHolder")}" + @""", ""id"": ""EnterpriseServer"", ""style"": ""Url"", - ""isRequired"": true, - ""spacing"": ""ExtraLarge"" + ""spacing"": ""ExtraLarge"", + ""value"": ""${EnterpriseServerInputValue}"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""${EnterpriseServerPageErrorValue}"", + ""wrap"": true, + ""horizontalAlignment"": ""Left"", + ""spacing"": ""small"", + ""size"": ""small"", + ""color"": ""attention"", + ""isVisible"": ""${EnterpriseServerPageErrorVisible}"" }, { ""type"": ""ColumnSet"", @@ -491,7 +554,7 @@ internal string GetLoginUITemplate(string loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": ""Cancel"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Cancel")}" + @""", ""id"": ""Cancel"", ""role"": ""Button"" } @@ -508,7 +571,7 @@ internal string GetLoginUITemplate(string loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": ""Next"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Next")}" + @""", ""id"": ""Next"", ""style"": ""positive"", ""role"": ""Button"" @@ -551,7 +614,7 @@ internal string GetLoginUITemplate(string loginUIState) { ""type"": ""TextBlock"", ""weight"": ""Bolder"", - ""text"": ""GitHub"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""Small"", @@ -559,7 +622,7 @@ internal string GetLoginUITemplate(string loginUIState) }, { ""type"": ""TextBlock"", - ""text"": ""Enterprise Server"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""None"", @@ -577,26 +640,34 @@ internal string GetLoginUITemplate(string loginUIState) ""inlines"": [ { ""type"": ""TextRun"", - ""text"": ""Please enter your Personal Access Token (PAT) to connect to . To create a new PAT, "" + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Text")}" + @""" }, { ""type"": ""TextRun"", - ""text"": ""click here."", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_HighlightedText")}" + @""", ""selectAction"": { ""type"": ""Action.OpenUrl"", - ""url"": ""https://adaptivecards.io"" + ""url"": ""${EnterpriseServerPATPageCreatePATUrlValue}"" } } ] }, { ""type"": ""Input.Text"", - ""placeholder"": ""Enter personal access token"", + ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_InputText_PlaceHolder")}" + @""", ""id"": ""PAT"", - ""style"": ""Url"", - ""isRequired"": true, ""spacing"": ""Large"", - ""errorMessage"": ""Invalid Url"" + ""value"": ""${EnterpriseServerPATPageInputValue}"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""${EnterpriseServerPATPageErrorValue}"", + ""wrap"": true, + ""horizontalAlignment"": ""Left"", + ""spacing"": ""small"", + ""size"": ""small"", + ""color"": ""attention"", + ""isVisible"": ""${EnterpriseServerPATPageErrorVisible}"" }, { ""type"": ""ColumnSet"", @@ -612,7 +683,7 @@ internal string GetLoginUITemplate(string loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": ""Cancel"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Cancel")}" + @""", ""id"": ""Cancel"", ""role"": ""Button"" } @@ -629,7 +700,7 @@ internal string GetLoginUITemplate(string loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": ""Connect"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Connect")}" + @""", ""id"": ""Connect"", ""style"": ""positive"", ""role"": ""Button"" @@ -829,6 +900,44 @@ public string? PAT } } + private class PageData + { + } + + private class LoginSucceededPageData : PageData + { + public string? Message { get; set; } = string.Empty; + } + + private class EnterpriseServerPageData : PageData + { + public string EnterpriseServerInputValue { get; set; } = string.Empty; + + // Default is false + public bool EnterpriseServerPageErrorVisible + { + get; set; + } + + public string EnterpriseServerPageErrorValue { get; set; } = string.Empty; + } + + private class EnterpriseServerPATPageData : PageData + { + public string? EnterpriseServerPATPageInputValue { get; set; } = string.Empty; + + public bool? EnterpriseServerPATPageErrorVisible + { + get; set; + } + + public string? EnterpriseServerPATPageErrorValue { get; set; } = string.Empty; + + public string? EnterpriseServerPATPageCreatePATUrlValue { get; set; } = "https://github.com/"; + + public string? EnterpriseServerPATPageServerUrlValue { get; set; } = "https://github.com/"; + } + // This class cannot be an enum, since we are passing this to the core app as State parameter. private class LoginUIState { diff --git a/src/GitHubExtension/DeveloperId/OAuthRequest.cs b/src/GitHubExtension/DeveloperId/OAuthRequest.cs index f70ea269..f5464389 100644 --- a/src/GitHubExtension/DeveloperId/OAuthRequest.cs +++ b/src/GitHubExtension/DeveloperId/OAuthRequest.cs @@ -53,7 +53,7 @@ private Uri CreateOauthRequestUri() var request = new OauthLoginRequest(OauthConfiguration.GetClientId()) { - Scopes = { "user", "notifications", "repo", "read:org" }, + Scopes = { "read:user", "notifications", "repo", "read:org" }, State = State, RedirectUri = new Uri(OauthConfiguration.RedirectUri), }; diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index d0f59455..a560429a 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -207,15 +207,19 @@ Something went wrong :( + Generic error text shown in LoginUI failure page Please consider filing feedback + Generic error text shown in LoginUI failure page Sign in to github.com + Button name to login to GitHub.com Opens the browser to log you into GitHub + Tooltip text on mouse hover over login button Go @@ -230,22 +234,28 @@ <Enter the GitHub Server link> - Sign in to GitHub Enterprise (Coming Soon!) + Sign in to GitHub Enterprise Server + Button name to login to GHES - Coming soon! + Lets you enter the host address of your GitHub Enterprise Server + Tooltip text on mouse hover over GHES button GitHub + Page title Sign in + Page subtitle has logged in successfully! You may close this window. + Partial text for when a user signs in successfully Waiting for browser... + Shown in Waiting page Sign in to use this widget. @@ -365,6 +375,7 @@ Note: Browser may have launched in the background. + Shown in Waiting page not known @@ -416,4 +427,52 @@ Cancel Shown in Widget, Button tooltip + + Cancel + LoginUI Button text + + + Next + LoginUI Button text + + + Enterprise Server is not valid + LoginUI Error text + + + GitHub + LoginUI Page Title + + + Enter server address here + LoginUI placeholder text + + + Enterprise Server + LoginUI Page subtitle + + + Cancel + LoginUI Button text + + + Connect + LoginUI Button text + + + Unable to connect to the Enterprise Server + LoginUI Button text + + + click here. + LoginUI Instructional text with hyperlink + + + Enter personal access token + LoginUI placeholder text + + + Enter your Personal Access Token (PAT) to connect to ${EnterpriseServerPATPageServerUrlValue}. To create a new PAT, + LoginUI Instructional text + \ No newline at end of file From f9bdd76f3f32adf6cd357b3012908880e9967328 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 24 Nov 2023 00:33:28 -0800 Subject: [PATCH 11/27] Created separate states for pages --- .../DeveloperId/Enums/LoginUIState.cs | 28 + .../DeveloperId/LoginUI/EndPage.cs | 12 + .../LoginUI/EnterpriseServerPATPage.cs | 64 ++ .../LoginUI/EnterpriseServerPage.cs | 66 ++ .../DeveloperId/LoginUI/LoginFailedPage.cs | 31 + .../DeveloperId/LoginUI/LoginPage.cs | 35 + .../DeveloperId/LoginUI/LoginSucceededPage.cs | 39 + .../DeveloperId/LoginUI/LoginUIPage.cs | 590 +++++++++++++ .../DeveloperId/LoginUI/WaitingPage.cs | 31 + .../DeveloperId/LoginUIController.cs | 811 ++---------------- .../Strings/en-US/Resources.resw | 28 +- 11 files changed, 1002 insertions(+), 733 deletions(-) create mode 100644 src/GitHubExtension/DeveloperId/Enums/LoginUIState.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/EndPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs create mode 100644 src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs diff --git a/src/GitHubExtension/DeveloperId/Enums/LoginUIState.cs b/src/GitHubExtension/DeveloperId/Enums/LoginUIState.cs new file mode 100644 index 00000000..2a340c82 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/Enums/LoginUIState.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace GitHubExtension.DeveloperId; + +public enum LoginUIState +{ + // Login UI starts on the LoginPage. + LoginPage, + + // Enterprise Server is selected on the LoginPage + EnterpriseServerPage, + + // User has entered a server URL on the EnterpriseServerPage + EnterpriseServerPATPage, + + // The user has clicked the "Sign in with GitHub" button and is waiting for the GitHub login page to load. + WaitingPage, + + // Login has failed and the user is shown the LoginFailedPage. + LoginFailedPage, + + // Login has succeeded and the user is shown the LoginSucceededPage. + LoginSucceededPage, + + // LoginUI is not visible and is in the process of being disposed. + End, +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EndPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EndPage.cs new file mode 100644 index 00000000..c66895e7 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/EndPage.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class EndPage : LoginUIPage +{ + public EndPage() + : base(LoginUIState.End) + { + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs new file mode 100644 index 00000000..a878cab5 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class EnterpriseServerPATPage : LoginUIPage +{ + public EnterpriseServerPATPage(Uri hostAddress, string errorText, string? inputPAT) + : base(LoginUIState.EnterpriseServerPATPage) + { + Data = new PageData() + { + EnterpriseServerPATPageInputValue = inputPAT ?? string.Empty, + EnterpriseServerPATPageErrorValue = errorText ?? string.Empty, + EnterpriseServerPATPageErrorVisible = !string.IsNullOrEmpty(errorText), + EnterpriseServerPATPageCreatePATUrlValue = hostAddress?.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", + EnterpriseServerPATPageServerUrlValue = hostAddress?.OriginalString ?? string.Empty, + }; + } + + internal class PageData : ILoginUIPageData + { + public string EnterpriseServerPATPageInputValue { get; set; } = string.Empty; + + public bool EnterpriseServerPATPageErrorVisible { get; set; } + + public string EnterpriseServerPATPageErrorValue { get; set; } = string.Empty; + + public string EnterpriseServerPATPageCreatePATUrlValue { get; set; } = "https://github.com/"; + + public string EnterpriseServerPATPageServerUrlValue { get; set; } = "https://github.com/"; + + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } + + internal class ActionPayload : SubmitActionPayload + { + public string? URL + { + get; set; + } + } + + internal class InputPayload + { + public string? PAT + { + get; set; + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs new file mode 100644 index 00000000..d75304cc --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class EnterpriseServerPage : LoginUIPage +{ + public EnterpriseServerPage(Uri? hostAddress, string errorText) + : base(LoginUIState.EnterpriseServerPage) + { + Data = new PageData() + { + EnterpriseServerInputValue = hostAddress?.ToString() ?? string.Empty, + EnterpriseServerPageErrorValue = errorText ?? string.Empty, + EnterpriseServerPageErrorVisible = !string.IsNullOrEmpty(errorText), + }; + } + + public EnterpriseServerPage(string hostAddress, string errorText) + : base(LoginUIState.EnterpriseServerPage) + { + Data = new PageData() + { + EnterpriseServerInputValue = hostAddress, + EnterpriseServerPageErrorValue = errorText ?? string.Empty, + EnterpriseServerPageErrorVisible = !string.IsNullOrEmpty(errorText), + }; + } + + internal class PageData : ILoginUIPageData + { + public string EnterpriseServerInputValue { get; set; } = string.Empty; + + // Default is false + public bool EnterpriseServerPageErrorVisible { get; set; } + + public string EnterpriseServerPageErrorValue { get; set; } = string.Empty; + + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } + + internal class ActionPayload : SubmitActionPayload + { + } + + internal class InputPayload + { + public string? EnterpriseServer + { + get; set; + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs new file mode 100644 index 00000000..93566fe8 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class LoginFailedPage : LoginUIPage +{ + public LoginFailedPage() + : base(LoginUIState.LoginFailedPage) + { + Data = new LoginFailedPageData(); + } + + internal class LoginFailedPageData : ILoginUIPageData + { + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs new file mode 100644 index 00000000..11ed48b6 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class LoginPage : LoginUIPage +{ + public LoginPage() + : base(LoginUIState.LoginPage) + { + Data = new PageData(); + } + + internal class PageData : ILoginUIPageData + { + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } + + internal class ActionPayload : SubmitActionPayload + { + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs new file mode 100644 index 00000000..ab36bdc4 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Windows.DevHome.SDK; +using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class LoginSucceededPage : LoginUIPage +{ + public LoginSucceededPage(IDeveloperId developerId) + : base(LoginUIState.LoginSucceededPage) + { + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + Data = new LoginSucceededPageData() + { + Message = $"{developerId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", + }; + } + + internal class LoginSucceededPageData : ILoginUIPageData + { + public string? Message { get; set; } = string.Empty; + + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs new file mode 100644 index 00000000..89a68886 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -0,0 +1,590 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; +using GitHubExtension.DeveloperId.LoginUI; +using Microsoft.Windows.DevHome.SDK; +using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; + +namespace GitHubExtension.DeveloperId; + +internal class LoginUIPage +{ + private readonly string _template; + private readonly LoginUIState _state; + private ILoginUIPageData? _data; + + public interface ILoginUIPageData + { + public abstract string GetJson(); + } + + protected ILoginUIPageData Data + { + get => _data ?? throw new InvalidOperationException(); + set => _data = value; + } + + public LoginUIPage(LoginUIState state) + { + _template = GetTemplate(state); + _state = state; + } + + public ProviderOperationResult UpdateExtensionAdaptiveCard(IExtensionAdaptiveCard adaptiveCard) + { + if (adaptiveCard == null) + { + throw new ArgumentNullException(nameof(adaptiveCard)); + } + + var x = _data?.GetJson(); + return adaptiveCard.Update(_template, x, Enum.GetName(typeof(LoginUIState), _state)); + } + + private string GetTemplate(LoginUIState loginUIState) + { + var loader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); + + var loginPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""ColumnSet"", + ""spacing"": ""Large"", + ""columns"": [ + { + ""type"": ""Column"", + ""items"": [ + { + ""type"": ""Image"", + ""style"": ""Person"", + ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", + ""size"": ""small"", + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TextBlock"", + ""weight"": ""Bolder"", + ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Heading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""Small"", + ""size"": ""large"" + }, + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Subheading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"", + ""size"": ""small"" + }, + { + ""type"": ""TextBlock"", + ""text"": """", + ""wrap"": true, + ""spacing"": ""Large"", + ""horizontalAlignment"": ""Center"", + ""isSubtle"": true + } + ], + ""width"": ""stretch"", + ""separator"": true, + ""spacing"": ""Medium"" + } + ] + }, + { + ""type"": ""Table"", + ""columns"": [ + { + ""width"": 1 + } + ], + ""rows"": [ + { + ""type"": ""TableRow"", + ""cells"": [ + { + ""type"": ""TableCell"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1Text")}" + @""", + ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1ToolTip")}" + @""", + ""style"": ""positive"", + ""isEnabled"": true, + ""id"": ""Personal"" + } + ], + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + } + ], + ""verticalContentAlignment"": ""Center"", + ""height"": ""stretch"", + ""spacing"": ""None"", + ""horizontalAlignment"": ""Center"" + } + ], + ""horizontalAlignment"": ""Center"", + ""height"": ""stretch"", + ""horizontalCellContentAlignment"": ""Center"", + ""verticalCellContentAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TableRow"", + ""cells"": [ + { + ""type"": ""TableCell"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Text")}" + @""", + ""isEnabled"": true, + ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2ToolTip")}" + @""", + ""id"": ""Enterprise"" + } + ], + ""spacing"": ""None"", + ""horizontalAlignment"": ""Center"" + } + ], + ""verticalContentAlignment"": ""Center"", + ""spacing"": ""None"", + ""horizontalAlignment"": ""Center"" + } + ], + ""spacing"": ""None"", + ""horizontalAlignment"": ""Center"", + ""horizontalCellContentAlignment"": ""Center"", + ""verticalCellContentAlignment"": ""Center"" + } + ], + ""firstRowAsHeaders"": false, + ""spacing"": ""Medium"", + ""horizontalAlignment"": ""Center"", + ""horizontalCellContentAlignment"": ""Center"", + ""verticalCellContentAlignment"": ""Center"", + ""showGridLines"": false + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""350px"", + ""verticalContentAlignment"": ""Top"", + ""rtl"": false +} +"; + + var enterpriseServerPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""ColumnSet"", + ""spacing"": ""Large"", + ""columns"": [ + { + ""type"": ""Column"", + ""items"": [ + { + ""type"": ""Image"", + ""style"": ""Person"", + ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", + ""size"": ""Small"", + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TextBlock"", + ""weight"": ""Bolder"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""Small"", + ""size"": ""Large"" + }, + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"", + ""size"": ""Small"" + }, + { + ""type"": ""TextBlock"", + ""text"": """", + ""wrap"": true, + ""spacing"": ""Large"", + ""horizontalAlignment"": ""Center"", + ""isSubtle"": true + } + ], + ""width"": ""stretch"", + ""separator"": true, + ""spacing"": ""Medium"" + } + ] + }, + { + ""type"": ""Input.Text"", + ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePage_InputText_PlaceHolder")}" + @""", + ""id"": ""EnterpriseServer"", + ""style"": ""Url"", + ""spacing"": ""ExtraLarge"", + ""value"": ""${EnterpriseServerInputValue}"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""${EnterpriseServerPageErrorValue}"", + ""wrap"": true, + ""horizontalAlignment"": ""Left"", + ""spacing"": ""small"", + ""size"": ""small"", + ""color"": ""attention"", + ""isVisible"": ""${EnterpriseServerPageErrorVisible}"" + }, + { + ""type"": ""ColumnSet"", + ""horizontalAlignment"": ""Center"", + ""height"": ""stretch"", + ""columns"": [ + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Cancel")}" + @""", + ""id"": ""Cancel"", + ""role"": ""Button"" + } + ] + } + ] + }, + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Next")}" + @""", + ""id"": ""Next"", + ""style"": ""positive"", + ""role"": ""Button"" + } + ] + } + ] + } + ], + ""spacing"": ""Small"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""350px"", + ""verticalContentAlignment"": ""Top"", + ""rtl"": false +} +"; + + var enterpriseServerPATPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""ColumnSet"", + ""spacing"": ""Large"", + ""columns"": [ + { + ""type"": ""Column"", + ""items"": [ + { + ""type"": ""Image"", + ""style"": ""Person"", + ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", + ""size"": ""Small"", + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"" + }, + { + ""type"": ""TextBlock"", + ""weight"": ""Bolder"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""Small"", + ""size"": ""Large"" + }, + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""None"", + ""size"": ""Small"" + } + ], + ""width"": ""stretch"", + ""separator"": true, + ""spacing"": ""Medium"" + } + ] + }, + { + ""type"": ""RichTextBlock"", + ""inlines"": [ + { + ""type"": ""TextRun"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Text")} " + @""" + }, + { + ""type"": ""TextRun"", + ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_HighlightedText")}" + @""", + ""selectAction"": { + ""type"": ""Action.OpenUrl"", + ""url"": ""${EnterpriseServerPATPageCreatePATUrlValue}"" + } + } + ] + }, + { + ""type"": ""Input.Text"", + ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_InputText_PlaceHolder")}" + @""", + ""id"": ""PAT"", + ""spacing"": ""Large"", + ""value"": ""${EnterpriseServerPATPageInputValue}"" + }, + { + ""type"": ""TextBlock"", + ""text"": ""${EnterpriseServerPATPageErrorValue}"", + ""wrap"": true, + ""horizontalAlignment"": ""Left"", + ""spacing"": ""small"", + ""size"": ""small"", + ""color"": ""attention"", + ""isVisible"": ""${EnterpriseServerPATPageErrorVisible}"" + }, + { + ""type"": ""ColumnSet"", + ""horizontalAlignment"": ""Center"", + ""height"": ""stretch"", + ""columns"": [ + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Cancel")}" + @""", + ""id"": ""Cancel"", + ""role"": ""Button"" + } + ] + } + ] + }, + { + ""type"": ""Column"", + ""width"": ""stretch"", + ""items"": [ + { + ""type"": ""ActionSet"", + ""actions"": [ + { + ""type"": ""Action.Submit"", + ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Connect")}" + @""", + ""id"": ""Connect"", + ""style"": ""positive"", + ""role"": ""Button"" + } + ] + } + ] + } + ], + ""spacing"": ""Small"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""350px"", + ""verticalContentAlignment"": ""Top"", + ""rtl"": false +} +"; + + var waitingPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_WaitingPage_Text")}" + @""", + ""isSubtle"": false, + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""ExtraLarge"", + ""size"": ""Large"", + ""weight"": ""Lighter"", + ""height"": ""stretch"", + ""style"": ""heading"" + }, + { + ""type"" : ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_WaitingPageBrowserLaunch_Text")}" + @""", + ""isSubtle"": false, + ""horizontalAlignment"": ""Center"", + ""weight"": ""Lighter"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""100px"" +} +"; + + var loginSucceededPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""TextBlock"", + ""text"": ""${message}"", + ""isSubtle"": false, + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""ExtraLarge"", + ""size"": ""Large"", + ""weight"": ""Lighter"", + ""style"": ""heading"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""200px"" +} +"; + + var loginFailedPage = @" +{ + ""type"": ""AdaptiveCard"", + ""body"": [ + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text1")}" + @""", + ""isSubtle"": false, + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""spacing"": ""ExtraLarge"", + ""size"": ""Large"", + ""weight"": ""Lighter"", + ""style"": ""heading"" + }, + { + ""type"": ""TextBlock"", + ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text2")}" + @""", + ""isSubtle"": true, + ""wrap"": true, + ""horizontalAlignment"": ""Center"", + ""size"": ""medium"", + ""weight"": ""Lighter"" + } + ], + ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", + ""version"": ""1.5"", + ""minHeight"": ""200px"" +} +"; + + switch (loginUIState) + { + case LoginUIState.LoginPage: + { + return loginPage; + } + + case LoginUIState.EnterpriseServerPage: + { + return enterpriseServerPage; + } + + case LoginUIState.EnterpriseServerPATPage: + { + return enterpriseServerPATPage; + } + + case LoginUIState.WaitingPage: + { + return waitingPage; + } + + case LoginUIState.LoginFailedPage: + { + return loginFailedPage; + } + + case LoginUIState.LoginSucceededPage: + { + return loginSucceededPage; + } + + default: + { + throw new InvalidOperationException(); + } + } + } + + internal class SubmitActionPayload + { + public string? Id + { + get; set; + } + + public string? Style + { + get; set; + } + + public string? ToolTip + { + get; set; + } + + public string? Title + { + get; set; + } + + public string? Type + { + get; set; + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs new file mode 100644 index 00000000..2f6c298e --- /dev/null +++ b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace GitHubExtension.DeveloperId.LoginUI; + +internal class WaitingPage : LoginUIPage +{ + public WaitingPage() + : base(LoginUIState.WaitingPage) + { + Data = new WaitingPageData(); + } + + internal class WaitingPageData : ILoginUIPageData + { + public string GetJson() + { + return JsonSerializer.Serialize( + this, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + } +} diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index d108bf4a..052a4712 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Drawing.Drawing2D; using System.Net; using System.Text.Json; using GitHubExtension.Client; +using GitHubExtension.DeveloperId.LoginUI; using Microsoft.Windows.DevHome.SDK; -using Windows.ApplicationModel.Activation; using Windows.Foundation; using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; @@ -14,7 +13,6 @@ namespace GitHubExtension.DeveloperId; internal class LoginUIController : IExtensionAdaptiveCardSession { private IExtensionAdaptiveCard? _loginUI; - private static readonly LoginUITemplate _loginUITemplate = new (); private Uri? _hostAddress; public LoginUIController() @@ -31,8 +29,7 @@ public ProviderOperationResult Initialize(IExtensionAdaptiveCard extensionUI) { Log.Logger()?.ReportDebug($"Initialize"); _loginUI = extensionUI; - var operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginPage), null, LoginUIState.LoginPage); - return operationResult; + return new LoginPage().UpdateExtensionAdaptiveCard(_loginUI); } public IAsyncOperation OnAction(string action, string inputs) @@ -48,10 +45,11 @@ public IAsyncOperation OnAction(string action, string i ProviderOperationResult operationResult; Log.Logger()?.ReportInfo($"OnAction() called with state:{_loginUI.State}"); Log.Logger()?.ReportDebug($"action: {action}"); + var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); switch (_loginUI.State) { - case LoginUIState.LoginPage: + case nameof(LoginUIState.LoginPage): { try { @@ -59,13 +57,13 @@ public IAsyncOperation OnAction(string action, string i /*if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) { Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); break; }*/ // Inputs are validated at this point. - var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid action"); @@ -75,49 +73,38 @@ public IAsyncOperation OnAction(string action, string i Log.Logger()?.ReportInfo($"Show Enterprise Page"); // Update UI with Enterprise Server page and return. - var pageData = new EnterpriseServerPageData() - { - EnterpriseServerInputValue = string.Empty, - EnterpriseServerPageErrorValue = string.Empty, - EnterpriseServerPageErrorVisible = false, - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + operationResult = new EnterpriseServerPage(hostAddress: string.Empty, errorText: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); break; } // Display Waiting page before Browser launch in LoginNewDeveloperIdAsync() - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.WaitingPage), null, LoginUIState.WaitingPage); + new WaitingPage().UpdateExtensionAdaptiveCard(_loginUI); var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); if (devId != null) { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - var pageData = new LoginSucceededPageData - { - Message = $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage), JsonSerializer.Serialize(pageData), LoginUIState.LoginSucceededPage); + operationResult = new LoginSucceededPage(devId).UpdateExtensionAdaptiveCard(_loginUI); } else { Log.Logger()?.ReportError($"Unable to create DeveloperId"); + new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Developer Id could not be created", "Developer Id could not be created"); - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); } } catch (Exception ex) { Log.Logger()?.ReportError($"Error: {ex}"); + new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); - _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); } break; } - case LoginUIState.EnterpriseServerPage: + case nameof(LoginUIState.EnterpriseServerPage): { // Check if the user clicked on Cancel button. - var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid action"); @@ -125,13 +112,13 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePageActionPayload?.Id == "Cancel") { Log.Logger()?.ReportInfo($"Cancel clicked"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginPage), null, LoginUIState.LoginPage); + operationResult = new LoginPage().UpdateExtensionAdaptiveCard(_loginUI); break; } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid inputs"); @@ -140,14 +127,7 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePageInputPayload?.EnterpriseServer == null) { Log.Logger()?.ReportError($"EnterpriseServer is null"); - var pageData = new EnterpriseServerPageData() - { - EnterpriseServerInputValue = string.Empty, - EnterpriseServerPageErrorValue = "EnterpriseServer is null", - EnterpriseServerPageErrorVisible = true, - }; - - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + operationResult = new EnterpriseServerPage(hostAddress: string.Empty, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_NullErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -157,64 +137,37 @@ public IAsyncOperation OnAction(string action, string i _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); if (!Validation.IsReachableGitHubEnterpriseServerURL(_hostAddress)) { - var pageData = new EnterpriseServerPageData() - { - EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, - EnterpriseServerPageErrorValue = "Enterprise Server is not reachable", - EnterpriseServerPageErrorVisible = true, - }; - - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_UnreachableErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } } catch (UriFormatException ufe) { Log.Logger()?.ReportError($"Error: {ufe}"); - var pageData = new EnterpriseServerPageData() - { - EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, - EnterpriseServerPageErrorValue = "Enterprise Server URL is invalid", - EnterpriseServerPageErrorVisible = true, - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_UriErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } catch (Exception ex) { Log.Logger()?.ReportError($"Error: {ex}"); - var pageData = new EnterpriseServerPageData() - { - EnterpriseServerInputValue = enterprisePageInputPayload.EnterpriseServer, - EnterpriseServerPageErrorValue = $"Somthing went wrong: {ex}", - EnterpriseServerPageErrorVisible = true, - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPage); + operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_GenericErrorText")} : {ex}").UpdateExtensionAdaptiveCard(_loginUI); break; } - var pageData1 = new EnterpriseServerPATPageData() - { - EnterpriseServerPATPageErrorValue = string.Empty, - EnterpriseServerPATPageErrorVisible = false, - EnterpriseServerPATPageInputValue = string.Empty, - EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", - EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, - }; try { - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData1), LoginUIState.EnterpriseServerPATPage); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: string.Empty, inputPAT: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); } catch (Exception ex) { Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + operationResult = new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); } break; } - case LoginUIState.EnterpriseServerPATPage: + case nameof(LoginUIState.EnterpriseServerPATPage): { if (_hostAddress == null) { @@ -225,7 +178,7 @@ public IAsyncOperation OnAction(string action, string i } // Check if the user clicked on Cancel button. - var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions + var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid action"); @@ -233,13 +186,55 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePATPageActionPayload?.Id == "Cancel") { Log.Logger()?.ReportInfo($"Cancel clicked"); + operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); + break; + } - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPage), JsonSerializer.Serialize(new EnterpriseServerPageData()), LoginUIState.EnterpriseServerPage); + if (enterprisePATPageActionPayload?.Type == "Action.OpenUrl") + { + Log.Logger()?.ReportInfo($"Create PAT Link clicked"); + + try + { + Uri uri = new Uri(enterprisePATPageActionPayload?.URL ?? string.Empty); + + var browserLaunch = false; + + _ = Task.Run(async () => + { + // Launch GitHub login page on Browser. + browserLaunch = await Windows.System.Launcher.LaunchUriAsync(uri); + + if (browserLaunch) + { + Log.Logger()?.ReportInfo($"Uri Launched to {uri.AbsoluteUri} - Check browser"); + } + else + { + Log.Logger()?.ReportError($"Uri Launch failed to {uri.AbsoluteUri}"); + } + }); + } + catch (UriFormatException ufe) + { + Log.Logger()?.ReportError($"Error: {ufe}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error: {ufe}", $"Error: {ufe}"); + break; + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error: {ex}"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error: {ex}", $"Error: {ex}"); + break; + } + + operationResult = new ProviderOperationResult(ProviderOperationStatus.Success, null, string.Empty, string.Empty); break; } + // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions + var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions { PropertyNameCaseInsensitive = true, }) ?? throw new InvalidOperationException("Invalid inputs"); @@ -248,16 +243,7 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePATPageInputPayload?.PAT == null) { Log.Logger()?.ReportError($"PAT is null"); - var pageData = new EnterpriseServerPATPageData - { - EnterpriseServerPATPageInputValue = enterprisePATPageInputPayload?.PAT, - EnterpriseServerPATPageErrorValue = $"Please enter the PAT", - EnterpriseServerPATPageErrorVisible = true, - EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", - EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPATPage); - + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -269,48 +255,42 @@ public IAsyncOperation OnAction(string action, string i if (devId != null) { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - var pageData = new LoginSucceededPageData() - { - Message = $"{devId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginSucceededPage), JsonSerializer.Serialize(pageData), LoginUIState.LoginSucceededPage); + operationResult = new LoginSucceededPage(devId).UpdateExtensionAdaptiveCard(_loginUI); break; } else { Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}"); - var pageData = new EnterpriseServerPATPageData - { - EnterpriseServerPATPageInputValue = enterprisePATPageInputPayload?.PAT, - EnterpriseServerPATPageErrorValue = $"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}", - EnterpriseServerPATPageErrorVisible = true, - EnterpriseServerPATPageCreatePATUrlValue = _hostAddress.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", - EnterpriseServerPATPageServerUrlValue = _hostAddress.OriginalString, - }; - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.EnterpriseServerPATPage), JsonSerializer.Serialize(pageData), LoginUIState.EnterpriseServerPATPage); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_NullErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } catch (Exception ex) { + if (ex.Message.Contains("Bad credentials")) + { + Log.Logger()?.ReportError($"Unauthorized Error: {ex}"); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_BadCredentialsErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + break; + } + Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = _loginUI.Update(_loginUITemplate.GetLoginUITemplate(LoginUIState.LoginFailedPage), null, LoginUIState.LoginFailedPage); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_GenericErrorText")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } // These pages only have close actions. - case LoginUIState.LoginSucceededPage: - case LoginUIState.LoginFailedPage: + case nameof(LoginUIState.LoginSucceededPage): + case nameof(LoginUIState.LoginFailedPage): { Log.Logger()?.ReportInfo($"State:{_loginUI.State}"); - operationResult = _loginUI.Update(null, null, LoginUIState.End); + operationResult = new EndPage().UpdateExtensionAdaptiveCard(_loginUI); break; } // These pages do not have any actions. We should never be here. - case LoginUIState.WaitingPage: + case nameof(LoginUIState.WaitingPage): default: { Log.Logger()?.ReportError($"Unexpected state:{_loginUI.State}"); @@ -322,631 +302,4 @@ public IAsyncOperation OnAction(string action, string i return operationResult; }).AsAsyncOperation(); } - - // Adaptive Card Templates for LoginUI. - private class LoginUITemplate - { - internal string GetLoginUITemplate(string loginUIState) - { - var loader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - - var loginPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""ColumnSet"", - ""spacing"": ""Large"", - ""columns"": [ - { - ""type"": ""Column"", - ""items"": [ - { - ""type"": ""Image"", - ""style"": ""Person"", - ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", - ""size"": ""small"", - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"" - }, - { - ""type"": ""TextBlock"", - ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Heading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""Small"", - ""size"": ""large"" - }, - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Subheading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"", - ""size"": ""small"" - }, - { - ""type"": ""TextBlock"", - ""text"": """", - ""wrap"": true, - ""spacing"": ""Large"", - ""horizontalAlignment"": ""Center"", - ""isSubtle"": true - } - ], - ""width"": ""stretch"", - ""separator"": true, - ""spacing"": ""Medium"" - } - ] - }, - { - ""type"": ""Table"", - ""columns"": [ - { - ""width"": 1 - } - ], - ""rows"": [ - { - ""type"": ""TableRow"", - ""cells"": [ - { - ""type"": ""TableCell"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1Text")}" + @""", - ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1ToolTip")}" + @""", - ""style"": ""positive"", - ""isEnabled"": true, - ""id"": ""Personal"" - } - ], - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"" - } - ], - ""verticalContentAlignment"": ""Center"", - ""height"": ""stretch"", - ""spacing"": ""None"", - ""horizontalAlignment"": ""Center"" - } - ], - ""horizontalAlignment"": ""Center"", - ""height"": ""stretch"", - ""horizontalCellContentAlignment"": ""Center"", - ""verticalCellContentAlignment"": ""Center"", - ""spacing"": ""None"" - }, - { - ""type"": ""TableRow"", - ""cells"": [ - { - ""type"": ""TableCell"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Text")}" + @""", - ""isEnabled"": true, - ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2ToolTip")}" + @""", - ""id"": ""Enterprise"" - } - ], - ""spacing"": ""None"", - ""horizontalAlignment"": ""Center"" - } - ], - ""verticalContentAlignment"": ""Center"", - ""spacing"": ""None"", - ""horizontalAlignment"": ""Center"" - } - ], - ""spacing"": ""None"", - ""horizontalAlignment"": ""Center"", - ""horizontalCellContentAlignment"": ""Center"", - ""verticalCellContentAlignment"": ""Center"" - } - ], - ""firstRowAsHeaders"": false, - ""spacing"": ""Medium"", - ""horizontalAlignment"": ""Center"", - ""horizontalCellContentAlignment"": ""Center"", - ""verticalCellContentAlignment"": ""Center"", - ""showGridLines"": false - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""350px"", - ""verticalContentAlignment"": ""Top"", - ""rtl"": false -} -"; - - var enterpriseServerPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""ColumnSet"", - ""spacing"": ""Large"", - ""columns"": [ - { - ""type"": ""Column"", - ""items"": [ - { - ""type"": ""Image"", - ""style"": ""Person"", - ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", - ""size"": ""Small"", - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"" - }, - { - ""type"": ""TextBlock"", - ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""Small"", - ""size"": ""Large"" - }, - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"", - ""size"": ""Small"" - }, - { - ""type"": ""TextBlock"", - ""text"": """", - ""wrap"": true, - ""spacing"": ""Large"", - ""horizontalAlignment"": ""Center"", - ""isSubtle"": true - } - ], - ""width"": ""stretch"", - ""separator"": true, - ""spacing"": ""Medium"" - } - ] - }, - { - ""type"": ""Input.Text"", - ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePage_InputText_PlaceHolder")}" + @""", - ""id"": ""EnterpriseServer"", - ""style"": ""Url"", - ""spacing"": ""ExtraLarge"", - ""value"": ""${EnterpriseServerInputValue}"" - }, - { - ""type"": ""TextBlock"", - ""text"": ""${EnterpriseServerPageErrorValue}"", - ""wrap"": true, - ""horizontalAlignment"": ""Left"", - ""spacing"": ""small"", - ""size"": ""small"", - ""color"": ""attention"", - ""isVisible"": ""${EnterpriseServerPageErrorVisible}"" - }, - { - ""type"": ""ColumnSet"", - ""horizontalAlignment"": ""Center"", - ""height"": ""stretch"", - ""columns"": [ - { - ""type"": ""Column"", - ""width"": ""stretch"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Cancel")}" + @""", - ""id"": ""Cancel"", - ""role"": ""Button"" - } - ] - } - ] - }, - { - ""type"": ""Column"", - ""width"": ""stretch"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Next")}" + @""", - ""id"": ""Next"", - ""style"": ""positive"", - ""role"": ""Button"" - } - ] - } - ] - } - ], - ""spacing"": ""Small"" - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""350px"", - ""verticalContentAlignment"": ""Top"", - ""rtl"": false -} -"; - - var enterpriseServerPATPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""ColumnSet"", - ""spacing"": ""Large"", - ""columns"": [ - { - ""type"": ""Column"", - ""items"": [ - { - ""type"": ""Image"", - ""style"": ""Person"", - ""url"": ""https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"", - ""size"": ""Small"", - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"" - }, - { - ""type"": ""TextBlock"", - ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""Small"", - ""size"": ""Large"" - }, - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""None"", - ""size"": ""Small"" - } - ], - ""width"": ""stretch"", - ""separator"": true, - ""spacing"": ""Medium"" - } - ] - }, - { - ""type"": ""RichTextBlock"", - ""inlines"": [ - { - ""type"": ""TextRun"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Text")}" + @""" - }, - { - ""type"": ""TextRun"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_HighlightedText")}" + @""", - ""selectAction"": { - ""type"": ""Action.OpenUrl"", - ""url"": ""${EnterpriseServerPATPageCreatePATUrlValue}"" - } - } - ] - }, - { - ""type"": ""Input.Text"", - ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_InputText_PlaceHolder")}" + @""", - ""id"": ""PAT"", - ""spacing"": ""Large"", - ""value"": ""${EnterpriseServerPATPageInputValue}"" - }, - { - ""type"": ""TextBlock"", - ""text"": ""${EnterpriseServerPATPageErrorValue}"", - ""wrap"": true, - ""horizontalAlignment"": ""Left"", - ""spacing"": ""small"", - ""size"": ""small"", - ""color"": ""attention"", - ""isVisible"": ""${EnterpriseServerPATPageErrorVisible}"" - }, - { - ""type"": ""ColumnSet"", - ""horizontalAlignment"": ""Center"", - ""height"": ""stretch"", - ""columns"": [ - { - ""type"": ""Column"", - ""width"": ""stretch"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Cancel")}" + @""", - ""id"": ""Cancel"", - ""role"": ""Button"" - } - ] - } - ] - }, - { - ""type"": ""Column"", - ""width"": ""stretch"", - ""items"": [ - { - ""type"": ""ActionSet"", - ""actions"": [ - { - ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Connect")}" + @""", - ""id"": ""Connect"", - ""style"": ""positive"", - ""role"": ""Button"" - } - ] - } - ] - } - ], - ""spacing"": ""Small"" - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""350px"", - ""verticalContentAlignment"": ""Top"", - ""rtl"": false -} -"; - - var waitingPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_WaitingPage_Text")}" + @""", - ""isSubtle"": false, - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""ExtraLarge"", - ""size"": ""Large"", - ""weight"": ""Lighter"", - ""height"": ""stretch"", - ""style"": ""heading"" - }, - { - ""type"" : ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_WaitingPageBrowserLaunch_Text")}" + @""", - ""isSubtle"": false, - ""horizontalAlignment"": ""Center"", - ""weight"": ""Lighter"" - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""100px"" -} -"; - - var loginSucceededPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""TextBlock"", - ""text"": ""${message}"", - ""isSubtle"": false, - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""ExtraLarge"", - ""size"": ""Large"", - ""weight"": ""Lighter"", - ""style"": ""heading"" - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""200px"" -} -"; - - var loginFailedPage = @" -{ - ""type"": ""AdaptiveCard"", - ""body"": [ - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text1")}" + @""", - ""isSubtle"": false, - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""spacing"": ""ExtraLarge"", - ""size"": ""Large"", - ""weight"": ""Lighter"", - ""style"": ""heading"" - }, - { - ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text2")}" + @""", - ""isSubtle"": true, - ""wrap"": true, - ""horizontalAlignment"": ""Center"", - ""size"": ""medium"", - ""weight"": ""Lighter"" - } - ], - ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"", - ""version"": ""1.5"", - ""minHeight"": ""200px"" -} -"; - - switch (loginUIState) - { - case LoginUIState.LoginPage: - { - return loginPage; - } - - case LoginUIState.EnterpriseServerPage: - { - return enterpriseServerPage; - } - - case LoginUIState.EnterpriseServerPATPage: - { - return enterpriseServerPATPage; - } - - case LoginUIState.WaitingPage: - { - return waitingPage; - } - - case LoginUIState.LoginFailedPage: - { - return loginFailedPage; - } - - case LoginUIState.LoginSucceededPage: - { - return loginSucceededPage; - } - - default: - { - throw new InvalidOperationException(); - } - } - } - } - - private class ButtonClickActionPayload - { - public string? Id - { - get; set; - } - - public string? Style - { - get; set; - } - - public string? ToolTip - { - get; set; - } - - public string? Title - { - get; set; - } - - public string? Type - { - get; set; - } - } - - private class LoginPageActionPayload : ButtonClickActionPayload - { - } - - private class EnterprisePageActionPayload : ButtonClickActionPayload - { - } - - private class EnterprisePATPageActionPayload : ButtonClickActionPayload - { - } - - private class EnterprisePageInputPayload - { - public string? EnterpriseServer - { - get; set; - } - } - - private class EnterprisePATPageInputPayload - { - public string? PAT - { - get; set; - } - } - - private class PageData - { - } - - private class LoginSucceededPageData : PageData - { - public string? Message { get; set; } = string.Empty; - } - - private class EnterpriseServerPageData : PageData - { - public string EnterpriseServerInputValue { get; set; } = string.Empty; - - // Default is false - public bool EnterpriseServerPageErrorVisible - { - get; set; - } - - public string EnterpriseServerPageErrorValue { get; set; } = string.Empty; - } - - private class EnterpriseServerPATPageData : PageData - { - public string? EnterpriseServerPATPageInputValue { get; set; } = string.Empty; - - public bool? EnterpriseServerPATPageErrorVisible - { - get; set; - } - - public string? EnterpriseServerPATPageErrorValue { get; set; } = string.Empty; - - public string? EnterpriseServerPATPageCreatePATUrlValue { get; set; } = "https://github.com/"; - - public string? EnterpriseServerPATPageServerUrlValue { get; set; } = "https://github.com/"; - } - - // This class cannot be an enum, since we are passing this to the core app as State parameter. - private class LoginUIState - { - internal const string LoginPage = "LoginPage"; - internal const string EnterpriseServerPage = "EnterpriseServerPage"; - internal const string EnterpriseServerPATPage = "EnterpriseServerPATPage"; - internal const string WaitingPage = "WaitingPage"; - internal const string LoginFailedPage = "LoginFailedPage"; - internal const string LoginSucceededPage = "LoginSucceededPage"; - internal const string End = "End"; - } } diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index a560429a..8794dfa5 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -435,7 +435,7 @@ Next LoginUI Button text - + Enterprise Server is not valid LoginUI Error text @@ -459,9 +459,13 @@ Connect LoginUI Button text - - Unable to connect to the Enterprise Server - LoginUI Button text + + URL is invalid + LoginUI Error text + + + Enterprise Server is not reachable + LoginUI Error text click here. @@ -475,4 +479,20 @@ Enter your Personal Access Token (PAT) to connect to ${EnterpriseServerPATPageServerUrlValue}. To create a new PAT, LoginUI Instructional text + + Something went wrong + LoginUI Error text + + + PAT doesn't work for GHES endpoint + LoginUI Error text + + + Error: + LoginUI Error text + + + Please enter the PAT + LoginUI Error text + \ No newline at end of file From febc384b81309bdefeed052e8e798d2a0dab4324 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sat, 25 Nov 2023 14:48:47 -0800 Subject: [PATCH 12/27] Added CredentialVault Tests --- .../DeveloperId/CredentialVault.cs | 73 ++++++++---- .../DeveloperId/DeveloperId.cs | 5 +- .../DeveloperId/DeveloperIdProvider.cs | 16 +-- .../DeveloperId/ICredentialVault.cs | 20 ++++ .../DeveloperId/CredentialVaultTests.cs | 106 ++++++++++++++++++ .../DeveloperId/DeveloperIdTestsSetup.cs | 40 +++++++ 6 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 src/GitHubExtension/DeveloperId/ICredentialVault.cs create mode 100644 test/GitHubExtension/DeveloperId/CredentialVaultTests.cs create mode 100644 test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index 2258bb1d..e892144e 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -10,14 +10,29 @@ using static GitHubExtension.DeveloperId.CredentialManager; namespace GitHubExtension.DeveloperId; -internal static class CredentialVault +public class CredentialVault : ICredentialVault { + private static readonly object CredentialVaultLock = new (); + + // CredentialVault uses singleton pattern. + private static CredentialVault? singletonCredentialVault; + + public static CredentialVault GetInstance() + { + lock (CredentialVaultLock) + { + singletonCredentialVault ??= new CredentialVault(); + } + + return singletonCredentialVault; + } + private static class CredentialVaultConfiguration { public const string CredResourceName = "GitHubDevHomeExtension"; } - internal static void SaveAccessTokenToVault(string loginId, SecureString? accessToken) + public void SaveCredentials(string loginId, SecureString? accessToken) { // Initialize a credential object. var credential = new CREDENTIAL @@ -61,18 +76,7 @@ internal static void SaveAccessTokenToVault(string loginId, SecureString? access } } - internal static void RemoveAccessTokenFromVault(string loginId) - { - var targetCredentialToDelete = CredentialVaultConfiguration.CredResourceName + ": " + loginId; - var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0); - if (!isCredentialDeleted) - { - Log.Logger()?.ReportInfo($"Deleting credentials from Credential Manager has failed"); - throw new Win32Exception(Marshal.GetLastWin32Error()); - } - } - - internal static PasswordCredential GetCredentialFromLocker(string loginId) + public PasswordCredential? GetCredentials(string loginId) { var credentialNameToRetrieve = CredentialVaultConfiguration.CredResourceName + ": " + loginId; var ptrToCredential = IntPtr.Zero; @@ -83,7 +87,7 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId) if (!isCredentialRetrieved) { Log.Logger()?.ReportInfo($"Retrieving credentials from Credential Manager has failed"); - throw new Win32Exception(Marshal.GetLastWin32Error()); + return null; } CREDENTIAL credentialObject; @@ -97,26 +101,29 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId) else { Log.Logger()?.ReportInfo("No credentials found for this DeveloperId"); - throw new ArgumentOutOfRangeException(loginId); + return null; } var accessTokenInChars = new char[credentialObject.CredentialBlobSize / 2]; Marshal.Copy(credentialObject.CredentialBlob, accessTokenInChars, 0, accessTokenInChars.Length); - var accessToken = new SecureString(); + // convert accessTokenInChars to string + string accessTokenString = new (accessTokenInChars); + for (var i = 0; i < accessTokenInChars.Length; i++) { - accessToken.AppendChar(accessTokenInChars[i]); - // Zero out characters after they are copied over from an unmanaged to managed type. accessTokenInChars[i] = '\0'; } - accessToken.MakeReadOnly(); - - var credential = new PasswordCredential(CredentialVaultConfiguration.CredResourceName, loginId, new NetworkCredential(string.Empty, accessToken).Password); + var credential = new PasswordCredential(CredentialVaultConfiguration.CredResourceName, loginId, accessTokenString); return credential; } + catch (Exception) + { + Log.Logger()?.ReportInfo($"Retrieving credentials from Credential Manager has failed unexpectedly"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } finally { if (ptrToCredential != IntPtr.Zero) @@ -126,7 +133,18 @@ internal static PasswordCredential GetCredentialFromLocker(string loginId) } } - public static IEnumerable GetAllSavedLoginIdsOrUrls() + public void RemoveCredentials(string loginId) + { + var targetCredentialToDelete = CredentialVaultConfiguration.CredResourceName + ": " + loginId; + var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0); + if (!isCredentialDeleted) + { + Log.Logger()?.ReportInfo($"Deleting credentials from Credential Manager has failed"); + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + public IEnumerable GetAllCredentials() { var ptrToCredential = IntPtr.Zero; @@ -179,4 +197,13 @@ public static IEnumerable GetAllSavedLoginIdsOrUrls() } } } + + public void RemoveAllCredentials() + { + var allCredentials = GetAllCredentials(); + foreach (var credential in allCredentials) + { + RemoveCredentials(credential); + } + } } diff --git a/src/GitHubExtension/DeveloperId/DeveloperId.cs b/src/GitHubExtension/DeveloperId/DeveloperId.cs index 0b13852e..b6af8f8d 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperId.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperId.cs @@ -55,7 +55,8 @@ public Windows.Security.Credentials.PasswordCredential GetCredential(bool refres return RefreshDeveloperId(); } - return CredentialVault.GetCredentialFromLocker(Url); + var credential = CredentialVault.GetInstance().GetCredentials(Url) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); + return credential; } public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() @@ -63,7 +64,7 @@ public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() // Setting to MaxValue, since GitHub doesn't forcibly expire tokens currently. CredentialExpiryTime = DateTime.MaxValue; DeveloperIdProvider.GetInstance().RefreshDeveloperId(this); - var credential = CredentialVault.GetCredentialFromLocker(Url); + var credential = CredentialVault.GetInstance().GetCredentials(Url) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); GitHubClient.Credentials = new (credential.Password); return credential; } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index c70c6eaa..31eb7c95 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -56,7 +56,7 @@ private DeveloperIdProvider() try { // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(CredentialVault.GetAllSavedLoginIdsOrUrls()); + RestoreDeveloperIds(CredentialVault.GetInstance().GetAllCredentials()); } catch (Exception error) { @@ -166,7 +166,7 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) return new ProviderOperationResult(ProviderOperationStatus.Failure, new ArgumentNullException(nameof(developerId)), "The developer account to log out does not exist", "Unable to find DeveloperId to logout"); } - CredentialVault.RemoveAccessTokenFromVault(developerIdToLogout.Url); + CredentialVault.GetInstance().RemoveCredentials(developerIdToLogout.Url); DeveloperIds?.Remove(developerIdToLogout); } @@ -248,7 +248,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString try { // Save the credential to Credential Vault. - CredentialVault.SaveAccessTokenToVault(duplicateDeveloperIds.Single().Url, accessToken); + CredentialVault.GetInstance().SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); try { @@ -272,7 +272,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString DeveloperIds.Add(newDeveloperId); } - CredentialVault.SaveAccessTokenToVault(newDeveloperId.Url, accessToken); + CredentialVault.GetInstance().SaveCredentials(newDeveloperId.Url, accessToken); try { @@ -314,7 +314,7 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new (CredentialVault.GetCredentialFromLocker(loginIdOrUrl).Password), + Credentials = new (CredentialVault.GetInstance().GetCredentials(loginIdOrUrl)?.Password), }; var user = gitHubClient.User.Current().Result; @@ -333,10 +333,10 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) { try { - CredentialVault.SaveAccessTokenToVault( + CredentialVault.GetInstance().SaveCredentials( user.Url, - new NetworkCredential(string.Empty, CredentialVault.GetCredentialFromLocker(loginIdOrUrl).Password).SecurePassword); - CredentialVault.RemoveAccessTokenFromVault(loginIdOrUrl); + new NetworkCredential(string.Empty, CredentialVault.GetInstance().GetCredentials(loginIdOrUrl)?.Password).SecurePassword); + CredentialVault.GetInstance().RemoveCredentials(loginIdOrUrl); Log.Logger()?.ReportInfo($"Replaced {loginIdOrUrl} with {user.Url} in CredentialManager"); } catch (Exception error) diff --git a/src/GitHubExtension/DeveloperId/ICredentialVault.cs b/src/GitHubExtension/DeveloperId/ICredentialVault.cs new file mode 100644 index 00000000..dd6a187b --- /dev/null +++ b/src/GitHubExtension/DeveloperId/ICredentialVault.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Security; +using Windows.Security.Credentials; + +namespace GitHubExtension.DeveloperId; + +internal interface ICredentialVault +{ + PasswordCredential? GetCredentials(string loginId); + + void RemoveCredentials(string loginId); + + void SaveCredentials(string loginId, SecureString? accessToken); + + IEnumerable GetAllCredentials(); + + void RemoveAllCredentials(); +} diff --git a/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs new file mode 100644 index 00000000..4fa85683 --- /dev/null +++ b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Net; +using GitHubExtension.DeveloperId; + +namespace GitHubExtension.Test; + +// Unit Tests for CredentialVault +public partial class DeveloperIdTests +{ + [TestMethod] + [TestCategory("Unit")] + public void CredentialVault_CreateSingleton() + { + var credentialVault1 = CredentialVault.GetInstance(); + Assert.IsNotNull(credentialVault1); + + var credentialVault2 = CredentialVault.GetInstance(); + Assert.IsNotNull(credentialVault2); + + Assert.AreEqual(credentialVault1, credentialVault2); + + credentialVault1.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault1.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + [DataRow("testuser1")] + [DataRow("https://github.com/testuser2")] + [DataRow("https://RandomWebServer.example/testuser3")] + public void CredentialVault_SaveAndRetrieveCredential(string loginId) + { + var credentialVault = CredentialVault.GetInstance(); + Assert.IsNotNull(credentialVault); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + + var nullPassword = credentialVault.GetCredentials(loginId); + Assert.IsNull(nullPassword); + + var testPassword = "testpassword"; + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(loginId, password); + + var retrievedPassword = credentialVault.GetCredentials(loginId); + Assert.IsNotNull(retrievedPassword); + Assert.AreEqual(testPassword, retrievedPassword.Password); + + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + [DataRow("testuser1")] + [DataRow("https://github.com/testuser2")] + [DataRow("https://RandomWebServer.example/testuser3")] + public void CredentialVault_RemoveAndRetrieveCredential(string loginId) + { + var credentialVault = CredentialVault.GetInstance(); + Assert.IsNotNull(credentialVault); + + var testPassword = "testpassword"; + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(loginId, password); + + var retrievedPassword = credentialVault.GetCredentials(loginId); + Assert.IsNotNull(retrievedPassword); + Assert.AreEqual(testPassword, retrievedPassword.Password); + + credentialVault.RemoveCredentials(loginId); + + var nullPassword = credentialVault.GetCredentials(loginId); + Assert.IsNull(nullPassword); + + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + public void CredentialVault_GetAllCredentials() + { + var credentialVault = CredentialVault.GetInstance(); + Assert.IsNotNull(credentialVault); + + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + + var testLoginId = "testuser1"; + var testPassword = "testpassword"; + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(testLoginId, password); + + Assert.AreEqual(1, credentialVault.GetAllCredentials().Count()); + + credentialVault.RemoveCredentials(testLoginId); + + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + + var nullPassword = credentialVault.GetCredentials(testLoginId); + Assert.IsNull(nullPassword); + } +} diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs new file mode 100644 index 00000000..118da2cd --- /dev/null +++ b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +namespace GitHubExtension.Test; + +[TestClass] +public partial class DeveloperIdTests +{ + public TestContext? TestContext + { + get; + set; + } + + private TestOptions testOptions = new (); + + private TestOptions TestOptions + { + get => testOptions; + set => testOptions = value; + } + + [TestInitialize] + public void TestInitialize() + { + using var log = new DevHome.Logging.Logger("TestStore", TestOptions.LogOptions); + var testListener = new TestListener("TestListener", TestContext!); + log.AddListener(testListener); + DataModel.Log.Attach(log); + TestOptions = TestHelpers.SetupTempTestOptions(TestContext!); + + TestContext?.WriteLine("DeveloperIdTests use the same credential store as Dev Home Github Extension"); + } + + [TestCleanup] + public void Cleanup() + { + TestHelpers.CleanupTempTestOptions(TestOptions, TestContext!); + } +} From b2639d617a92653092fa7b77971d7ded5cd0f91a Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sat, 25 Nov 2023 22:43:12 -0800 Subject: [PATCH 13/27] Added LoginUI tests --- .../DeveloperId/LoginUI/LoginPage.cs | 4 + .../DeveloperId/LoginUI/LoginSucceededPage.cs | 4 +- .../DeveloperId/LoginUI/LoginUIPage.cs | 63 ++++---- .../DeveloperId/LoginUIController.cs | 76 ++++----- .../DeveloperId/LoginUITests.cs | 146 ++++++++++++++++++ 5 files changed, 225 insertions(+), 68 deletions(-) create mode 100644 test/GitHubExtension/DeveloperId/LoginUITests.cs diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs index 11ed48b6..3ca58be8 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs @@ -31,5 +31,9 @@ public string GetJson() internal class ActionPayload : SubmitActionPayload { + public bool IsEnterprise() + { + return this.Id == "Enterprise"; + } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs index ab36bdc4..a9af75cc 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; @@ -13,10 +14,9 @@ internal class LoginSucceededPage : LoginUIPage public LoginSucceededPage(IDeveloperId developerId) : base(LoginUIState.LoginSucceededPage) { - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); Data = new LoginSucceededPageData() { - Message = $"{developerId.LoginId} {resourceLoader.GetString("LoginUI_LoginSucceededPage_text")}", + Message = $"{developerId.LoginId} {Resources.GetResource("LoginUI_LoginSucceededPage_text")}", }; } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index 89a68886..fe83fa7f 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; -using GitHubExtension.DeveloperId.LoginUI; +using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; @@ -39,14 +37,11 @@ public ProviderOperationResult UpdateExtensionAdaptiveCard(IExtensionAdaptiveCar throw new ArgumentNullException(nameof(adaptiveCard)); } - var x = _data?.GetJson(); - return adaptiveCard.Update(_template, x, Enum.GetName(typeof(LoginUIState), _state)); + return adaptiveCard.Update(_template, _data?.GetJson(), Enum.GetName(typeof(LoginUIState), _state)); } private string GetTemplate(LoginUIState loginUIState) { - var loader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); - var loginPage = @" { ""type"": ""AdaptiveCard"", @@ -69,7 +64,7 @@ private string GetTemplate(LoginUIState loginUIState) { ""type"": ""TextBlock"", ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Heading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Heading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""Small"", @@ -77,7 +72,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginPage_Subheading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Subheading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""None"", @@ -117,8 +112,8 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1Text")}" + @""", - ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button1ToolTip")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Button1Text")}" + @""", + ""tooltip"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Button1ToolTip")}" + @""", ""style"": ""positive"", ""isEnabled"": true, ""id"": ""Personal"" @@ -151,9 +146,9 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2Text")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Button2Text")}" + @""", ""isEnabled"": true, - ""tooltip"": """ + $"{loader.GetString("LoginUI_LoginPage_Button2ToolTip")}" + @""", + ""tooltip"": """ + $"{Resources.GetResource("LoginUI_LoginPage_Button2ToolTip")}" + @""", ""id"": ""Enterprise"" } ], @@ -210,7 +205,7 @@ private string GetTemplate(LoginUIState loginUIState) { ""type"": ""TextBlock"", ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Heading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""Small"", @@ -218,7 +213,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Subheading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""None"", @@ -241,7 +236,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""Input.Text"", - ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePage_InputText_PlaceHolder")}" + @""", + ""placeholder"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_InputText_PlaceHolder")}" + @""", ""id"": ""EnterpriseServer"", ""style"": ""Url"", ""spacing"": ""ExtraLarge"", @@ -271,7 +266,7 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Cancel")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Button_Cancel")}" + @""", ""id"": ""Cancel"", ""role"": ""Button"" } @@ -288,7 +283,7 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Button_Next")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Button_Next")}" + @""", ""id"": ""Next"", ""style"": ""positive"", ""role"": ""Button"" @@ -331,7 +326,7 @@ private string GetTemplate(LoginUIState loginUIState) { ""type"": ""TextBlock"", ""weight"": ""Bolder"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Heading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Heading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""Small"", @@ -339,7 +334,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePage_Subheading")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePage_Subheading")}" + @""", ""wrap"": true, ""horizontalAlignment"": ""Center"", ""spacing"": ""None"", @@ -357,11 +352,11 @@ private string GetTemplate(LoginUIState loginUIState) ""inlines"": [ { ""type"": ""TextRun"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Text")} " + @""" + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Text")} " + @""" }, { ""type"": ""TextRun"", - ""text"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_HighlightedText")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_HighlightedText")}" + @""", ""selectAction"": { ""type"": ""Action.OpenUrl"", ""url"": ""${EnterpriseServerPATPageCreatePATUrlValue}"" @@ -371,7 +366,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""Input.Text"", - ""placeholder"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_InputText_PlaceHolder")}" + @""", + ""placeholder"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_InputText_PlaceHolder")}" + @""", ""id"": ""PAT"", ""spacing"": ""Large"", ""value"": ""${EnterpriseServerPATPageInputValue}"" @@ -400,7 +395,7 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Cancel")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Button_Cancel")}" + @""", ""id"": ""Cancel"", ""role"": ""Button"" } @@ -417,7 +412,7 @@ private string GetTemplate(LoginUIState loginUIState) ""actions"": [ { ""type"": ""Action.Submit"", - ""title"": """ + $"{loader.GetString("LoginUI_EnterprisePATPage_Button_Connect")}" + @""", + ""title"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Button_Connect")}" + @""", ""id"": ""Connect"", ""style"": ""positive"", ""role"": ""Button"" @@ -444,7 +439,7 @@ private string GetTemplate(LoginUIState loginUIState) ""body"": [ { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_WaitingPage_Text")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_WaitingPage_Text")}" + @""", ""isSubtle"": false, ""wrap"": true, ""horizontalAlignment"": ""Center"", @@ -456,7 +451,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"" : ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_WaitingPageBrowserLaunch_Text")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_WaitingPageBrowserLaunch_Text")}" + @""", ""isSubtle"": false, ""horizontalAlignment"": ""Center"", ""weight"": ""Lighter"" @@ -496,7 +491,7 @@ private string GetTemplate(LoginUIState loginUIState) ""body"": [ { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text1")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_LoginFailedPage_text1")}" + @""", ""isSubtle"": false, ""wrap"": true, ""horizontalAlignment"": ""Center"", @@ -507,7 +502,7 @@ private string GetTemplate(LoginUIState loginUIState) }, { ""type"": ""TextBlock"", - ""text"": """ + $"{loader.GetString("LoginUI_LoginFailedPage_text2")}" + @""", + ""text"": """ + $"{Resources.GetResource("LoginUI_LoginFailedPage_text2")}" + @""", ""isSubtle"": true, ""wrap"": true, ""horizontalAlignment"": ""Center"", @@ -586,5 +581,15 @@ public string? Type { get; set; } + + public bool IsCancelAction() + { + return this.Id == "Cancel"; + } + + public bool IsUrlAction() + { + return this.Type == "Action.OpenUrl"; + } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index 052a4712..ca0b7870 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,20 +1,29 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Linq; using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; using GitHubExtension.Client; using GitHubExtension.DeveloperId.LoginUI; +using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; namespace GitHubExtension.DeveloperId; -internal class LoginUIController : IExtensionAdaptiveCardSession +public class LoginUIController : IExtensionAdaptiveCardSession { private IExtensionAdaptiveCard? _loginUI; private Uri? _hostAddress; + public Uri HostAddress + { + get => _hostAddress ?? throw new InvalidOperationException("HostAddress is null"); + set => _hostAddress = value; + } + public LoginUIController() { } @@ -45,7 +54,6 @@ public IAsyncOperation OnAction(string action, string i ProviderOperationResult operationResult; Log.Logger()?.ReportInfo($"OnAction() called with state:{_loginUI.State}"); Log.Logger()?.ReportDebug($"action: {action}"); - var resourceLoader = new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), "GitHubExtension/Resources"); switch (_loginUI.State) { @@ -63,16 +71,11 @@ public IAsyncOperation OnAction(string action, string i }*/ // Inputs are validated at this point. - var loginPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid action"); + var loginPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); - if (loginPageActionPayload?.Id == "Enterprise") + if (loginPageActionPayload.IsEnterprise()) { Log.Logger()?.ReportInfo($"Show Enterprise Page"); - - // Update UI with Enterprise Server page and return. operationResult = new EnterpriseServerPage(hostAddress: string.Empty, errorText: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -104,12 +107,10 @@ public IAsyncOperation OnAction(string action, string i case nameof(LoginUIState.EnterpriseServerPage): { // Check if the user clicked on Cancel button. - var enterprisePageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid action"); + var enterprisePageActionPayload = CreateFromJson(action) + ?? throw new InvalidOperationException("Invalid action"); - if (enterprisePageActionPayload?.Id == "Cancel") + if (enterprisePageActionPayload.IsCancelAction()) { Log.Logger()?.ReportInfo($"Cancel clicked"); operationResult = new LoginPage().UpdateExtensionAdaptiveCard(_loginUI); @@ -118,16 +119,13 @@ public IAsyncOperation OnAction(string action, string i // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid inputs"); + var enterprisePageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); if (enterprisePageInputPayload?.EnterpriseServer == null) { Log.Logger()?.ReportError($"EnterpriseServer is null"); - operationResult = new EnterpriseServerPage(hostAddress: string.Empty, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_NullErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPage(hostAddress: string.Empty, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_NullErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -137,20 +135,20 @@ public IAsyncOperation OnAction(string action, string i _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); if (!Validation.IsReachableGitHubEnterpriseServerURL(_hostAddress)) { - operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_UnreachableErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_UnreachableErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } } catch (UriFormatException ufe) { Log.Logger()?.ReportError($"Error: {ufe}"); - operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_UriErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_UriErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } catch (Exception ex) { Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePage_GenericErrorText")} : {ex}").UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_GenericErrorText")} : {ex}").UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -178,19 +176,16 @@ public IAsyncOperation OnAction(string action, string i } // Check if the user clicked on Cancel button. - var enterprisePATPageActionPayload = JsonSerializer.Deserialize(action, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid action"); + var enterprisePATPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); - if (enterprisePATPageActionPayload?.Id == "Cancel") + if (enterprisePATPageActionPayload.IsCancelAction()) { Log.Logger()?.ReportInfo($"Cancel clicked"); operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); break; } - if (enterprisePATPageActionPayload?.Type == "Action.OpenUrl") + if (enterprisePATPageActionPayload.IsUrlAction()) { Log.Logger()?.ReportInfo($"Create PAT Link clicked"); @@ -234,16 +229,13 @@ public IAsyncOperation OnAction(string action, string i // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. Log.Logger()?.ReportDebug($"inputs: {inputs}"); - var enterprisePATPageInputPayload = JsonSerializer.Deserialize(inputs, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }) ?? throw new InvalidOperationException("Invalid inputs"); + var enterprisePATPageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"PAT Received"); if (enterprisePATPageInputPayload?.PAT == null) { Log.Logger()?.ReportError($"PAT is null"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -261,21 +253,21 @@ public IAsyncOperation OnAction(string action, string i else { Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_NullErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } catch (Exception ex) { - if (ex.Message.Contains("Bad credentials")) + if (ex.Message.Contains("Bad credentials") || ex.Message.Contains("Not Found")) { Log.Logger()?.ReportError($"Unauthorized Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_BadCredentialsErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{resourceLoader.GetString("LoginUI_EnterprisePATPage_GenericErrorText")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorText")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } @@ -302,4 +294,14 @@ public IAsyncOperation OnAction(string action, string i return operationResult; }).AsAsyncOperation(); } + + private T? CreateFromJson(string json) + { + return JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } } diff --git a/test/GitHubExtension/DeveloperId/LoginUITests.cs b/test/GitHubExtension/DeveloperId/LoginUITests.cs new file mode 100644 index 00000000..f21a9a53 --- /dev/null +++ b/test/GitHubExtension/DeveloperId/LoginUITests.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using GitHubExtension.DeveloperId; +using Microsoft.Windows.DevHome.SDK; + +namespace GitHubExtension.Test; + +// Unit Tests for LoginUIController and LoginUI +public partial class DeveloperIdTests +{ + public const string EMPTYJSON = "{}"; + + private struct LoginUITestData + { + public const string GithubButtonAction = "{\"id\":\"Personal\",\"style\":\"positive\",\"title\":\"Sign in to github.com\",\"tooltip\":\"Opens the browser to log you into GitHub\",\"type\":\"Action.Submit\"}"; + public const string GithubButtonInput = EMPTYJSON; + + public const string GithubEnterpriseButtonAction = "{\"id\":\"Enterprise\",\"title\":\"Sign in to GitHub Enterprise Server\",\"tooltip\":\"Lets you enter the host address of your GitHub Enterprise Server\",\"type\":\"Action.Submit\"}"; + public const string GithubEnterpriseButtonInput = EMPTYJSON; + + public const string CancelButtonAction = "{\"id\":\"Cancel\",\"title\":\"Cancel\",\"type\":\"Action.Submit\"}"; + public const string CancelButtonInput = EMPTYJSON; + + public const string NextButtonAction = "{\"id\":\"Next\",\"style\":\"positive\",\"title\":\"Next\",\"type\":\"Action.Submit\"}"; + public const string BadUrlEnterpriseServerInput = "{\"EnterpriseServer\":\"badUrlEnterpriseServer\"}"; + public const string UnreachableUrlEnterpriseServerInput = "{\"EnterpriseServer\":\"https://www.bing.com\"}"; + public static readonly string GoodUrlEnterpriseServerInput = "{\"EnterpriseServer\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER") + "\"}"; + public const string GithubUrlEnterpriseServerInput = "{\"EnterpriseServer\":\"https://www.github.com\"}"; + + public const string ClickHereUrlAction = "{\"role\":\"Link\",\"type\":\"Action.OpenUrl\",\"url\":\"https://www.bing.com\"}"; + public const string ClickHereUrlInput = EMPTYJSON; + + public const string ConnectButtonAction = "{\"id\":\"Connect\",\"style\":\"positive\",\"title\":\"Connect\",\"type\":\"Action.Submit\"}"; + public const string NullPATInput = "{\"PAT\":\"\"}"; + public const string BadPATInput = "{\"PAT\":\"Enterprise\"}"; + public static readonly string GoodPATEnterpriseServerInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT") + "\"}"; + public static readonly string GoodPATGithubComInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_PAT") + "\"}"; + } + + public class TestExtensionAdaptiveCard : IExtensionAdaptiveCard + { + private int updateCount; + + public int UpdateCount + { + get => updateCount; + set => updateCount = value; + } + + public TestExtensionAdaptiveCard(string templateJson, string dataJson, string state) + { + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + } + + public string DataJson + { + get; set; + } + + public string State + { + get; set; + } + + public string TemplateJson + { + get; set; + } + + public ProviderOperationResult Update(string templateJson, string dataJson, string state) + { + UpdateCount++; + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); + } + } + + [TestMethod] + [TestCategory("Unit")] + public void LoginUIControllerInitializeTest() + { + var testExtensionAdaptiveCard = new TestExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); + + // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. + var controller = new LoginUIController(); + controller.Initialize(testExtensionAdaptiveCard); + + // Verify that the initial state is the login page. + Assert.IsTrue(testExtensionAdaptiveCard.State == Enum.GetName(typeof(LoginUIState), LoginUIState.LoginPage)); + Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); + + controller.Dispose(); + } + + [TestMethod] + [TestCategory("Unit")] + [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] + [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] + [DataRow("EnterpriseServerPage", LoginUITestData.NextButtonAction, LoginUITestData.BadUrlEnterpriseServerInput, "EnterpriseServerPage")] + [DataRow("EnterpriseServerPage", LoginUITestData.NextButtonAction, LoginUITestData.UnreachableUrlEnterpriseServerInput, "EnterpriseServerPage")] + [DataRow("EnterpriseServerPage", LoginUITestData.CancelButtonAction, LoginUITestData.CancelButtonInput, "LoginPage")] + [DataRow("EnterpriseServerPATPage", LoginUITestData.CancelButtonAction, LoginUITestData.CancelButtonInput, "EnterpriseServerPage")] + [DataRow("EnterpriseServerPATPage", LoginUITestData.ClickHereUrlAction, LoginUITestData.ClickHereUrlInput, "EnterpriseServerPATPage", 1)] + [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.NullPATInput, "EnterpriseServerPATPage")] + [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.BadPATInput, "EnterpriseServerPATPage")] + + // Cannot test with LoginPAge since that launches browser + // [DataRow("LoginPage", LoginUITestData.GithubButtonAction, LoginUITestData.GithubButtonInput, "WaitingPage", 2)] + public async Task LoginUIControllerTest( + string initialState, string actions, string inputs, string finalState, int numOfFinalUpdates = 2) + { + var testExtensionAdaptiveCard = new TestExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); + + // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. + var controller = new LoginUIController(); + controller.Initialize(testExtensionAdaptiveCard); + Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); + + // Set the initial state. + testExtensionAdaptiveCard.State = initialState; + Assert.AreEqual(initialState, testExtensionAdaptiveCard.State); + + // Set HostAddress for EnterpriseServerPATPage to make this a valid state + if (initialState == "EnterpriseServerPATPage") + { + controller.HostAddress = new Uri("https://www.github.com"); + Assert.AreEqual("https://www.github.com", controller.HostAddress.OriginalString); + } + + // Call OnAction() with the actions and inputs. + await controller.OnAction(actions, inputs); + + // Verify the final state + Assert.AreEqual(finalState, testExtensionAdaptiveCard.State); + Assert.AreEqual(numOfFinalUpdates, testExtensionAdaptiveCard.UpdateCount); + + controller.Dispose(); + } +} From 04360df2062a8a2905715861e03fc327c55d74cf Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sun, 26 Nov 2023 15:32:02 -0800 Subject: [PATCH 14/27] Added some tests --- .../DataModel/DataObjects/Issue.cs | 1 - .../DeveloperId/CredentialVault.cs | 30 ++-- .../DeveloperId/DeveloperId.cs | 4 +- .../DeveloperId/DeveloperIdProvider.cs | 32 +++-- .../IDeveloperIdProviderInternal.cs | 16 +++ .../DeveloperId/LoginUI/LoginSucceededPage.cs | 1 - .../DeveloperId/LoginUI/LoginUIPage.cs | 1 - .../DeveloperId/LoginUIController.cs | 16 +-- .../DeveloperId/OAuthRequest.cs | 1 - .../Widgets/GitHubReviewWidget.cs | 2 - src/Logging/listeners/ListenerBase.cs | 2 - src/Telemetry/ILogger.cs | 4 - src/Telemetry/LogLevel.cs | 6 - src/Telemetry/Logger.cs | 5 - src/Telemetry/LoggerFactory.cs | 6 - test/Console/Program.cs | 1 - .../DeveloperId/CredentialVaultTests.cs | 12 +- .../DeveloperId/DeveloperIdTestsSetup.cs | 135 ++++++++++++++++++ .../DeveloperId/LoginUITests.cs | 122 +++++++++------- test/UITest/GitHubExtensionSession.cs | 1 - 20 files changed, 268 insertions(+), 130 deletions(-) create mode 100644 src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs diff --git a/src/GitHubExtension/DataModel/DataObjects/Issue.cs b/src/GitHubExtension/DataModel/DataObjects/Issue.cs index 8b58185e..e735794a 100644 --- a/src/GitHubExtension/DataModel/DataObjects/Issue.cs +++ b/src/GitHubExtension/DataModel/DataObjects/Issue.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Globalization; using Dapper; using Dapper.Contrib.Extensions; using GitHubExtension.Helpers; diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index e892144e..a8f06a72 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -2,34 +2,24 @@ // Licensed under the MIT license. using System.ComponentModel; -using System.Net; using System.Runtime.InteropServices; using System.Security; -using Octokit; using Windows.Security.Credentials; using static GitHubExtension.DeveloperId.CredentialManager; namespace GitHubExtension.DeveloperId; public class CredentialVault : ICredentialVault { - private static readonly object CredentialVaultLock = new (); + private readonly string credentialResourceName; - // CredentialVault uses singleton pattern. - private static CredentialVault? singletonCredentialVault; - - public static CredentialVault GetInstance() + private static class CredentialVaultConfiguration { - lock (CredentialVaultLock) - { - singletonCredentialVault ??= new CredentialVault(); - } - - return singletonCredentialVault; + public const string CredResourceName = "GitHubDevHomeExtension"; } - private static class CredentialVaultConfiguration + public CredentialVault(string applicationName = "") { - public const string CredResourceName = "GitHubDevHomeExtension"; + credentialResourceName = string.IsNullOrEmpty(applicationName) ? CredentialVaultConfiguration.CredResourceName : applicationName; } public void SaveCredentials(string loginId, SecureString? accessToken) @@ -38,7 +28,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) var credential = new CREDENTIAL { Type = CRED_TYPE.GENERIC, - TargetName = CredentialVaultConfiguration.CredResourceName + ": " + loginId, + TargetName = credentialResourceName + ": " + loginId, UserName = loginId, Persist = (int)CRED_PERSIST.LocalMachine, AttributeCount = 0, @@ -78,7 +68,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) public PasswordCredential? GetCredentials(string loginId) { - var credentialNameToRetrieve = CredentialVaultConfiguration.CredResourceName + ": " + loginId; + var credentialNameToRetrieve = credentialResourceName + ": " + loginId; var ptrToCredential = IntPtr.Zero; try @@ -116,7 +106,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) accessTokenInChars[i] = '\0'; } - var credential = new PasswordCredential(CredentialVaultConfiguration.CredResourceName, loginId, accessTokenString); + var credential = new PasswordCredential(credentialResourceName, loginId, accessTokenString); return credential; } catch (Exception) @@ -135,7 +125,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) public void RemoveCredentials(string loginId) { - var targetCredentialToDelete = CredentialVaultConfiguration.CredResourceName + ": " + loginId; + var targetCredentialToDelete = credentialResourceName + ": " + loginId; var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0); if (!isCredentialDeleted) { @@ -153,7 +143,7 @@ public IEnumerable GetAllCredentials() IntPtr[] allCredentials; uint count; - if (CredEnumerate(CredentialVaultConfiguration.CredResourceName + "*", 0, out count, out ptrToCredential) != false) + if (CredEnumerate(credentialResourceName + "*", 0, out count, out ptrToCredential) != false) { allCredentials = new IntPtr[count]; Marshal.Copy(ptrToCredential, allCredentials, 0, (int)count); diff --git a/src/GitHubExtension/DeveloperId/DeveloperId.cs b/src/GitHubExtension/DeveloperId/DeveloperId.cs index b6af8f8d..f2a821d3 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperId.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperId.cs @@ -55,7 +55,7 @@ public Windows.Security.Credentials.PasswordCredential GetCredential(bool refres return RefreshDeveloperId(); } - var credential = CredentialVault.GetInstance().GetCredentials(Url) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); + var credential = DeveloperIdProvider.GetInstance().GetCredentials(this) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); return credential; } @@ -64,7 +64,7 @@ public Windows.Security.Credentials.PasswordCredential RefreshDeveloperId() // Setting to MaxValue, since GitHub doesn't forcibly expire tokens currently. CredentialExpiryTime = DateTime.MaxValue; DeveloperIdProvider.GetInstance().RefreshDeveloperId(this); - var credential = CredentialVault.GetInstance().GetCredentials(Url) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); + var credential = DeveloperIdProvider.GetInstance().GetCredentials(this) ?? throw new InvalidOperationException("Invalid credential present for valid DeveloperId"); GitHubClient.Credentials = new (credential.Password); return credential; } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 31eb7c95..7f7913bd 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -7,16 +7,19 @@ using Microsoft.Windows.DevHome.SDK; using Octokit; using Windows.Foundation; +using Windows.Security.Credentials; namespace GitHubExtension.DeveloperId; -public class DeveloperIdProvider : IDeveloperIdProvider +public class DeveloperIdProvider : IDeveloperIdProviderInternal { // Locks to control access to Singleton class members. private static readonly object DeveloperIdsLock = new (); private static readonly object OAuthRequestsLock = new (); + private static readonly object CredentialVaultLock = new (); + private static readonly object AuthenticationProviderLock = new (); // DeveloperId list containing all Logged in Ids. @@ -31,6 +34,8 @@ private List OAuthRequests get; set; } + private readonly CredentialVault credentialVault; + // DeveloperIdProvider uses singleton pattern. private static DeveloperIdProvider? singletonDeveloperIdProvider; @@ -45,6 +50,11 @@ private DeveloperIdProvider() { Log.Logger()?.ReportInfo($"Creating DeveloperIdProvider singleton instance"); + lock (CredentialVaultLock) + { + credentialVault ??= new CredentialVault(); + } + lock (OAuthRequestsLock) { OAuthRequests ??= new List(); @@ -56,7 +66,7 @@ private DeveloperIdProvider() try { // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(CredentialVault.GetInstance().GetAllCredentials()); + RestoreDeveloperIds(credentialVault.GetAllCredentials()); } catch (Exception error) { @@ -166,7 +176,7 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) return new ProviderOperationResult(ProviderOperationStatus.Failure, new ArgumentNullException(nameof(developerId)), "The developer account to log out does not exist", "Unable to find DeveloperId to logout"); } - CredentialVault.GetInstance().RemoveCredentials(developerIdToLogout.Url); + credentialVault.RemoveCredentials(developerIdToLogout.Url); DeveloperIds?.Remove(developerIdToLogout); } @@ -248,7 +258,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString try { // Save the credential to Credential Vault. - CredentialVault.GetInstance().SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); + credentialVault.SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); try { @@ -272,7 +282,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString DeveloperIds.Add(newDeveloperId); } - CredentialVault.GetInstance().SaveCredentials(newDeveloperId.Url, accessToken); + credentialVault.SaveCredentials(newDeveloperId.Url, accessToken); try { @@ -314,7 +324,7 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new (CredentialVault.GetInstance().GetCredentials(loginIdOrUrl)?.Password), + Credentials = new (credentialVault.GetCredentials(loginIdOrUrl)?.Password), }; var user = gitHubClient.User.Current().Result; @@ -333,10 +343,10 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) { try { - CredentialVault.GetInstance().SaveCredentials( + credentialVault.SaveCredentials( user.Url, - new NetworkCredential(string.Empty, CredentialVault.GetInstance().GetCredentials(loginIdOrUrl)?.Password).SecurePassword); - CredentialVault.GetInstance().RemoveCredentials(loginIdOrUrl); + new NetworkCredential(string.Empty, credentialVault.GetCredentials(loginIdOrUrl)?.Password).SecurePassword); + credentialVault.RemoveCredentials(loginIdOrUrl); Log.Logger()?.ReportInfo($"Replaced {loginIdOrUrl} with {user.Url} in CredentialManager"); } catch (Exception error) @@ -362,7 +372,7 @@ public AuthenticationExperienceKind GetAuthenticationExperienceKind() public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() { Log.Logger()?.ReportInfo($"GetAdaptiveCardController"); - return new AdaptiveCardSessionResult(new LoginUIController()); + return new AdaptiveCardSessionResult(new LoginUIController(this)); } public void Dispose() @@ -388,4 +398,6 @@ public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) } } } + + internal PasswordCredential? GetCredentials(IDeveloperId developerId) => credentialVault.GetCredentials(developerId.Url); } diff --git a/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs b/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs new file mode 100644 index 00000000..56e96b71 --- /dev/null +++ b/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Security; +using Microsoft.Windows.DevHome.SDK; +using Windows.Foundation; + +namespace GitHubExtension.DeveloperId; +public interface IDeveloperIdProviderInternal : IDeveloperIdProvider +{ + public IAsyncOperation LoginNewDeveloperIdAsync(); + + public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken); + + public IEnumerable GetLoggedInDeveloperIdsInternal(); +} diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs index a9af75cc..0c46f5f4 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -5,7 +5,6 @@ using System.Text.Json.Serialization; using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; -using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; namespace GitHubExtension.DeveloperId.LoginUI; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index fe83fa7f..0fe3588b 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -3,7 +3,6 @@ using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; -using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; namespace GitHubExtension.DeveloperId; diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index ca0b7870..d43bf15e 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Linq; using System.Net; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,11 +9,11 @@ using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; using Windows.Foundation; -using ResourceLoader = Microsoft.Windows.ApplicationModel.Resources.ResourceLoader; namespace GitHubExtension.DeveloperId; public class LoginUIController : IExtensionAdaptiveCardSession { + private readonly IDeveloperIdProviderInternal _developerIdProvider; private IExtensionAdaptiveCard? _loginUI; private Uri? _hostAddress; @@ -24,8 +23,9 @@ public Uri HostAddress set => _hostAddress = value; } - public LoginUIController() + public LoginUIController(IDeveloperIdProviderInternal developerIdProvider) { + _developerIdProvider = developerIdProvider; } public void Dispose() @@ -62,9 +62,9 @@ public IAsyncOperation OnAction(string action, string i try { // If there is already a developer id, we should block another login. - /*if (DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().Any()) + /*if (_developerIdProvider.GetLoggedInDeveloperIdsInternal().Any()) { - Log.Logger()?.ReportInfo($"DeveloperId {DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); + Log.Logger()?.ReportInfo($"DeveloperId {_developerIdProvider.GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); break; @@ -82,7 +82,7 @@ public IAsyncOperation OnAction(string action, string i // Display Waiting page before Browser launch in LoginNewDeveloperIdAsync() new WaitingPage().UpdateExtensionAdaptiveCard(_loginUI); - var devId = await DeveloperIdProvider.GetInstance().LoginNewDeveloperIdAsync(); + var devId = await _developerIdProvider.LoginNewDeveloperIdAsync(); if (devId != null) { operationResult = new LoginSucceededPage(devId).UpdateExtensionAdaptiveCard(_loginUI); @@ -118,7 +118,6 @@ public IAsyncOperation OnAction(string action, string i } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. - Log.Logger()?.ReportDebug($"inputs: {inputs}"); var enterprisePageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); @@ -228,7 +227,6 @@ public IAsyncOperation OnAction(string action, string i } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. - Log.Logger()?.ReportDebug($"inputs: {inputs}"); var enterprisePATPageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"PAT Received"); @@ -243,7 +241,7 @@ public IAsyncOperation OnAction(string action, string i try { - var devId = DeveloperIdProvider.GetInstance().LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); + var devId = _developerIdProvider.LoginNewDeveloperIdWithPAT(_hostAddress, securePAT); if (devId != null) { diff --git a/src/GitHubExtension/DeveloperId/OAuthRequest.cs b/src/GitHubExtension/DeveloperId/OAuthRequest.cs index f5464389..b6d08435 100644 --- a/src/GitHubExtension/DeveloperId/OAuthRequest.cs +++ b/src/GitHubExtension/DeveloperId/OAuthRequest.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Collections.Specialized; using System.Net; using System.Security; using System.Security.Cryptography; diff --git a/src/GitHubExtension/Widgets/GitHubReviewWidget.cs b/src/GitHubExtension/Widgets/GitHubReviewWidget.cs index 1c311580..86cd6281 100644 --- a/src/GitHubExtension/Widgets/GitHubReviewWidget.cs +++ b/src/GitHubExtension/Widgets/GitHubReviewWidget.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using GitHubExtension.DataManager; using GitHubExtension.Helpers; using GitHubExtension.Widgets.Enums; diff --git a/src/Logging/listeners/ListenerBase.cs b/src/Logging/listeners/ListenerBase.cs index b264ff1f..8b9c6be8 100644 --- a/src/Logging/listeners/ListenerBase.cs +++ b/src/Logging/listeners/ListenerBase.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using DevHome.Logging; - namespace DevHome.Logging.Listeners; public abstract class ListenerBase : IListener diff --git a/src/Telemetry/ILogger.cs b/src/Telemetry/ILogger.cs index 805b2ecb..2a14d53d 100644 --- a/src/Telemetry/ILogger.cs +++ b/src/Telemetry/ILogger.cs @@ -2,11 +2,7 @@ // Licensed under the MIT license. using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace GitHubExtension.Telemetry; diff --git a/src/Telemetry/LogLevel.cs b/src/Telemetry/LogLevel.cs index b40afc17..0917717e 100644 --- a/src/Telemetry/LogLevel.cs +++ b/src/Telemetry/LogLevel.cs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace GitHubExtension.Telemetry; /// diff --git a/src/Telemetry/Logger.cs b/src/Telemetry/Logger.cs index e1fd833f..fb223746 100644 --- a/src/Telemetry/Logger.cs +++ b/src/Telemetry/Logger.cs @@ -3,17 +3,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Tracing; using System.IO; using System.Linq; -using System.Reflection; using System.Text; -using System.Threading.Tasks; -using System.Windows.Input; using Microsoft.Diagnostics.Telemetry; -using Microsoft.Win32; namespace GitHubExtension.Telemetry; diff --git a/src/Telemetry/LoggerFactory.cs b/src/Telemetry/LoggerFactory.cs index 7627c1d9..28a4abb1 100644 --- a/src/Telemetry/LoggerFactory.cs +++ b/src/Telemetry/LoggerFactory.cs @@ -1,12 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - namespace GitHubExtension.Telemetry; /// diff --git a/test/Console/Program.cs b/test/Console/Program.cs index d414e358..6f99f5e1 100644 --- a/test/Console/Program.cs +++ b/test/Console/Program.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using GitHubExtension; -using GitHubExtension.DataModel; internal class Program { diff --git a/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs index 4fa85683..ccf5d900 100644 --- a/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs +++ b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs @@ -13,13 +13,13 @@ public partial class DeveloperIdTests [TestCategory("Unit")] public void CredentialVault_CreateSingleton() { - var credentialVault1 = CredentialVault.GetInstance(); + var credentialVault1 = new CredentialVault("DevHomeGitHubExtensionTest"); Assert.IsNotNull(credentialVault1); - var credentialVault2 = CredentialVault.GetInstance(); + var credentialVault2 = new CredentialVault("DevHomeGitHubExtensionTest"); Assert.IsNotNull(credentialVault2); - Assert.AreEqual(credentialVault1, credentialVault2); + Assert.AreNotEqual(credentialVault1, credentialVault2); credentialVault1.RemoveAllCredentials(); Assert.AreEqual(0, credentialVault1.GetAllCredentials().Count()); @@ -32,7 +32,7 @@ public void CredentialVault_CreateSingleton() [DataRow("https://RandomWebServer.example/testuser3")] public void CredentialVault_SaveAndRetrieveCredential(string loginId) { - var credentialVault = CredentialVault.GetInstance(); + var credentialVault = new CredentialVault("DevHomeGitHubExtensionTest"); Assert.IsNotNull(credentialVault); Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); @@ -59,7 +59,7 @@ public void CredentialVault_SaveAndRetrieveCredential(string loginId) [DataRow("https://RandomWebServer.example/testuser3")] public void CredentialVault_RemoveAndRetrieveCredential(string loginId) { - var credentialVault = CredentialVault.GetInstance(); + var credentialVault = new CredentialVault("DevHomeGitHubExtensionTest"); Assert.IsNotNull(credentialVault); var testPassword = "testpassword"; @@ -83,7 +83,7 @@ public void CredentialVault_RemoveAndRetrieveCredential(string loginId) [TestCategory("Unit")] public void CredentialVault_GetAllCredentials() { - var credentialVault = CredentialVault.GetInstance(); + var credentialVault = new CredentialVault("DevHomeGitHubExtensionTest"); Assert.IsNotNull(credentialVault); Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs index 118da2cd..11d82599 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs @@ -1,11 +1,142 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Security; +using GitHubExtension.DeveloperId; +using Microsoft.UI; +using Microsoft.Windows.DevHome.SDK; +using Octokit; +using Windows.Foundation; + namespace GitHubExtension.Test; [TestClass] public partial class DeveloperIdTests { + public class RuntimeDataRow + { + public string? InitialState + { + get; set; + } + + public string? Actions + { + get; set; + } + + public string? Inputs + { + get; set; + } + + public string? FinalState + { + get; set; + } + + public string? HostAddress + { + get; set; + } + } + + public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard + { + private int updateCount; + + public int UpdateCount + { + get => updateCount; + set => updateCount = value; + } + + public MockExtensionAdaptiveCard(string templateJson, string dataJson, string state) + { + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + } + + public string DataJson + { + get; set; + } + + public string State + { + get; set; + } + + public string TemplateJson + { + get; set; + } + + public ProviderOperationResult Update(string templateJson, string dataJson, string state) + { + UpdateCount++; + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); + } + } + + public class MockDeveloperIdProvider : IDeveloperIdProviderInternal + { + private static MockDeveloperIdProvider? instance; + + public string DisplayName => throw new NotImplementedException(); + + public event TypedEventHandler Changed; + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public AuthenticationExperienceKind GetAuthenticationExperienceKind() => throw new NotImplementedException(); + + public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) => throw new NotImplementedException(); + + public DeveloperIdsResult GetLoggedInDeveloperIds() => throw new NotImplementedException(); + + public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() => throw new NotImplementedException(); + + public Windows.Foundation.IAsyncOperation LoginNewDeveloperIdAsync() + { + return Task.Run(() => + { + return (IDeveloperId)new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); + }).AsAsyncOperation(); + } + + public DeveloperId.DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken) + { + // This is a mock method, so we don't need to do anything here. Using Changed to avoid build warning. + _ = Changed.GetInvocationList(); + return new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); + } + + public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) => throw new NotImplementedException(); + + public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); + + private MockDeveloperIdProvider() + { + Changed += (IDeveloperIdProvider sender, IDeveloperId args) => { }; + } + + public static MockDeveloperIdProvider GetInstance() + { + instance ??= new MockDeveloperIdProvider(); + return instance; + } + + public IEnumerable GetLoggedInDeveloperIdsInternal() => throw new NotImplementedException(); + } + public TestContext? TestContext { get; @@ -30,6 +161,10 @@ public void TestInitialize() TestOptions = TestHelpers.SetupTempTestOptions(TestContext!); TestContext?.WriteLine("DeveloperIdTests use the same credential store as Dev Home Github Extension"); + + // Remove any existing credentials + var credentialVault = new CredentialVault(); + credentialVault.RemoveAllCredentials(); } [TestCleanup] diff --git a/test/GitHubExtension/DeveloperId/LoginUITests.cs b/test/GitHubExtension/DeveloperId/LoginUITests.cs index f21a9a53..71b4d8bb 100644 --- a/test/GitHubExtension/DeveloperId/LoginUITests.cs +++ b/test/GitHubExtension/DeveloperId/LoginUITests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using GitHubExtension.DeveloperId; -using Microsoft.Windows.DevHome.SDK; namespace GitHubExtension.Test; @@ -34,61 +33,19 @@ private struct LoginUITestData public const string ConnectButtonAction = "{\"id\":\"Connect\",\"style\":\"positive\",\"title\":\"Connect\",\"type\":\"Action.Submit\"}"; public const string NullPATInput = "{\"PAT\":\"\"}"; public const string BadPATInput = "{\"PAT\":\"Enterprise\"}"; - public static readonly string GoodPATEnterpriseServerInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT") + "\"}"; - public static readonly string GoodPATGithubComInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_PAT") + "\"}"; - } - - public class TestExtensionAdaptiveCard : IExtensionAdaptiveCard - { - private int updateCount; - - public int UpdateCount - { - get => updateCount; - set => updateCount = value; - } - - public TestExtensionAdaptiveCard(string templateJson, string dataJson, string state) - { - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - } - - public string DataJson - { - get; set; - } - - public string State - { - get; set; - } - - public string TemplateJson - { - get; set; - } - - public ProviderOperationResult Update(string templateJson, string dataJson, string state) - { - UpdateCount++; - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); - } + public static readonly string GoodPATEnterpriseServerPATInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT") + "\"}"; + public static readonly string GoodPATGithubComPATInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_PAT") + "\"}"; } [TestMethod] [TestCategory("Unit")] public void LoginUIControllerInitializeTest() { - var testExtensionAdaptiveCard = new TestExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. - var controller = new LoginUIController(); + var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); controller.Initialize(testExtensionAdaptiveCard); // Verify that the initial state is the login page. @@ -100,6 +57,7 @@ public void LoginUIControllerInitializeTest() [TestMethod] [TestCategory("Unit")] + [DataRow("LoginPage", LoginUITestData.GithubButtonAction, LoginUITestData.GithubButtonInput, "LoginSucceededPage", 3)] [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] [DataRow("EnterpriseServerPage", LoginUITestData.NextButtonAction, LoginUITestData.BadUrlEnterpriseServerInput, "EnterpriseServerPage")] @@ -109,17 +67,14 @@ public void LoginUIControllerInitializeTest() [DataRow("EnterpriseServerPATPage", LoginUITestData.ClickHereUrlAction, LoginUITestData.ClickHereUrlInput, "EnterpriseServerPATPage", 1)] [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.NullPATInput, "EnterpriseServerPATPage")] [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.BadPATInput, "EnterpriseServerPATPage")] - - // Cannot test with LoginPAge since that launches browser - // [DataRow("LoginPage", LoginUITestData.GithubButtonAction, LoginUITestData.GithubButtonInput, "WaitingPage", 2)] public async Task LoginUIControllerTest( string initialState, string actions, string inputs, string finalState, int numOfFinalUpdates = 2) { - var testExtensionAdaptiveCard = new TestExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. - var controller = new LoginUIController(); + var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); controller.Initialize(testExtensionAdaptiveCard); Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); @@ -143,4 +98,67 @@ public async Task LoginUIControllerTest( controller.Dispose(); } + + /* This test requires the following environment variables to be set: + * DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER : The host address of the GitHub Enterprise Server to test against + * DEV_HOME_TEST_GITHUB_COM_PAT : A valid Personal Access Token for GitHub.com (with at least repo_public permissions) + * DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT : A valid Personal Access Token for the GitHub Enterprise Server set in DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER (with at least repo_public permissions) + */ + [TestMethod] + [TestCategory("Unit")] + public async Task LoginUI_PATLoginTest_Success() + { + // Create DataRows during Runtime since these need Env vars + RuntimeDataRow[] dataRows = + { + new RuntimeDataRow() + { + InitialState = "EnterpriseServerPATPage", + Actions = LoginUITestData.ConnectButtonAction, + Inputs = LoginUITestData.GoodPATEnterpriseServerPATInput, + FinalState = "LoginSucceededPage", + HostAddress = Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER"), + }, + new RuntimeDataRow() + { + InitialState = "EnterpriseServerPATPage", + Actions = LoginUITestData.ConnectButtonAction, + Inputs = LoginUITestData.GoodPATGithubComPATInput, + FinalState = "LoginSucceededPage", + HostAddress = "https://api.github.com", + }, + } + ; + + foreach (RuntimeDataRow dataRow in dataRows) + { + var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); + + // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. + var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); + controller.Initialize(testExtensionAdaptiveCard); + Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); + + // Set the initial state. + testExtensionAdaptiveCard.State = dataRow.InitialState ?? string.Empty; + Assert.AreEqual(dataRow.InitialState, testExtensionAdaptiveCard.State); + + // Set HostAddress for EnterpriseServerPATPage to make this a valid state + if (dataRow.InitialState == "EnterpriseServerPATPage") + { + controller.HostAddress = new Uri(dataRow.HostAddress ?? string.Empty); + Assert.AreEqual(dataRow.HostAddress, controller.HostAddress.OriginalString); + } + + // Call OnAction() with the actions and inputs. + await controller.OnAction(dataRow.Actions ?? string.Empty, dataRow.Inputs ?? string.Empty); + + // Verify the final state + Assert.AreEqual(dataRow.FinalState, testExtensionAdaptiveCard.State); + Assert.AreEqual(2, testExtensionAdaptiveCard.UpdateCount); + + controller.Dispose(); + } + } } diff --git a/test/UITest/GitHubExtensionSession.cs b/test/UITest/GitHubExtensionSession.cs index f098a6b2..6205732f 100644 --- a/test/UITest/GitHubExtensionSession.cs +++ b/test/UITest/GitHubExtensionSession.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System; using Microsoft.VisualStudio.TestTools.UnitTesting; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Appium.Windows; From d4dd71894026b998046710e08bac60656bd11b0b Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sun, 26 Nov 2023 15:42:58 -0800 Subject: [PATCH 15/27] Reverted Widget updates --- .../DataManager/GitHubSearchManger.cs | 38 +--------------- .../DataManager/IGitHubSearchManager.cs | 4 +- .../Widgets/GitHubMentionedInWidget.cs | 44 ++++++++++++------- 3 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/GitHubExtension/DataManager/GitHubSearchManger.cs b/src/GitHubExtension/DataManager/GitHubSearchManger.cs index b026bcc9..f4eef37a 100644 --- a/src/GitHubExtension/DataManager/GitHubSearchManger.cs +++ b/src/GitHubExtension/DataManager/GitHubSearchManger.cs @@ -3,8 +3,7 @@ using GitHubExtension.Client; using GitHubExtension.DataManager; -using GitHubExtension.DataModel; -using Microsoft.Windows.DevHome.SDK; +using GitHubExtension.DataModel; namespace GitHubExtension; @@ -33,41 +32,6 @@ public GitHubSearchManager() return null; } } - - public async Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, IDeveloperId developerId, RequestOptions? options = null) - { - Log.Logger()?.ReportInfo(Name, $"Searching for issues or pull requests for widget {initiator}"); - request.State = Octokit.ItemState.Open; - request.Archived = false; - request.PerPage = 10; - request.SortField = Octokit.IssueSearchSort.Updated; - request.Order = Octokit.SortDirection.Descending; - - var client = GitHubClientProvider.Instance.GetClient(developerId.Url) ?? throw new InvalidOperationException($"Client does not exist for {developerId.Url}"); - - // Set is: parameter according to the search category. - // For the case we are searching for both we don't have to set the parameter - if (category.Equals(SearchCategory.Issues)) - { - request.Is = new List() { Octokit.IssueIsQualifier.Issue }; - } - else if (category.Equals(SearchCategory.PullRequests)) - { - request.Is = new List() { Octokit.IssueIsQualifier.PullRequest }; - } - - var octokitResult = await client.Search.SearchIssues(request); - if (octokitResult == null) - { - Log.Logger()?.ReportDebug($"No issues or PRs found."); - SendResultsAvailable(new List(), initiator); - } - else - { - Log.Logger()?.ReportDebug(Name, $"Results contain {octokitResult.Items.Count} items."); - SendResultsAvailable(octokitResult.Items, initiator); - } - } public async Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null) { diff --git a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs index dad5a644..963e4636 100644 --- a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs +++ b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs @@ -8,7 +8,5 @@ namespace GitHubExtension; public interface IGitHubSearchManager : IDisposable { - Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null); - - Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, IDeveloperId developerId, RequestOptions? options = null); + Task SearchForGitHubIssuesOrPRs(Octokit.SearchIssuesRequest request, string initiator, SearchCategory category, RequestOptions? options = null); } diff --git a/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs b/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs index edfcecc8..98cb7848 100644 --- a/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs +++ b/src/GitHubExtension/Widgets/GitHubMentionedInWidget.cs @@ -7,7 +7,6 @@ using GitHubExtension.DataManager; using GitHubExtension.Helpers; using GitHubExtension.Widgets.Enums; -using Microsoft.Windows.DevHome.SDK; using Microsoft.Windows.Widgets.Providers; using Octokit; @@ -46,8 +45,6 @@ internal class GitHubMentionedInWidget : GitHubWidget private static Dictionary Templates { get; set; } = new (); protected static readonly new string Name = nameof(GitHubMentionedInWidget); - - private readonly IDeveloperId? mentionedDeveloperId = DeveloperId.DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIds().DeveloperIds.Last(); private SearchCategory ShowCategory { @@ -56,9 +53,23 @@ private SearchCategory ShowCategory set => SetState(EnumHelper.SearchCategoryToString(value)); } - private SearchCategory? savedShowCategory; + private SearchCategory? savedShowCategory; + + private string mentionedName = string.Empty; - private string MentionedName => (mentionedDeveloperId != null) ? mentionedDeveloperId.LoginId : string.Empty; + private string MentionedName + { + get + { + if (string.IsNullOrEmpty(mentionedName)) + { + GetMentionedName(); + } + + return mentionedName; + } + set => mentionedName = value; + } public GitHubMentionedInWidget() : base() @@ -70,7 +81,16 @@ public GitHubMentionedInWidget() ~GitHubMentionedInWidget() { GitHubSearchManager.OnResultsAvailable -= SearchManagerResultsAvailableHandler; - } + } + + private void GetMentionedName() + { + var devIds = DeveloperId.DeveloperIdProvider.GetInstance().GetLoggedInDeveloperIdsInternal(); + if ((devIds != null) && devIds.Any()) + { + mentionedName = devIds.First().LoginId; + } + } public override void DeleteWidget(string widgetId, string customState) { @@ -154,13 +174,7 @@ public override void RequestContentData() try { - if (mentionedDeveloperId == null) - { - Log.Logger()?.ReportError($"MentionedDeveloperId is null"); - return; - } - - Log.Logger()?.ReportInfo(Name, ShortId, $"Requesting search for mentioned user {mentionedDeveloperId.LoginId}"); + Log.Logger()?.ReportInfo(Name, ShortId, $"Requesting search for mentioned user {mentionedName}"); var requestOptions = new RequestOptions { ApiOptions = new ApiOptions @@ -178,8 +192,8 @@ public override void RequestContentData() }; var searchManager = GitHubSearchManager.CreateInstance(); - searchManager?.SearchForGitHubIssuesOrPRs(request, Name, ShowCategory, mentionedDeveloperId, requestOptions); - Log.Logger()?.ReportInfo(Name, ShortId, $"Requested search for {mentionedDeveloperId?.LoginId}"); + searchManager?.SearchForGitHubIssuesOrPRs(request, Name, ShowCategory, requestOptions); + Log.Logger()?.ReportInfo(Name, ShortId, $"Requested search for {mentionedName}"); DataState = WidgetDataState.Requested; } catch (Exception ex) From ccda4930018d780ea191dcb72e1c3b01b6f3a48b Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Sun, 26 Nov 2023 23:14:27 -0800 Subject: [PATCH 16/27] Fixed tests --- .../DataManager/IGitHubSearchManager.cs | 1 - .../DeveloperId/LoginUI/LoginUIPage.cs | 5 + .../DeveloperId/LoginUIController.cs | 30 +- .../DeveloperId/DeveloperIdProviderTests.cs | 268 ++++++++++++++++++ .../DeveloperId/DeveloperIdTestsSetup.cs | 104 +------ .../DeveloperId/FunctionalTests.cs | 77 +++++ .../DeveloperId/LoginUITests.cs | 63 +++- .../Mocks/DeveloperIdProvider.cs | 113 ++++++++ 8 files changed, 543 insertions(+), 118 deletions(-) create mode 100644 test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs create mode 100644 test/GitHubExtension/DeveloperId/FunctionalTests.cs create mode 100644 test/GitHubExtension/Mocks/DeveloperIdProvider.cs diff --git a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs index 963e4636..a0a41eff 100644 --- a/src/GitHubExtension/DataManager/IGitHubSearchManager.cs +++ b/src/GitHubExtension/DataManager/IGitHubSearchManager.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using GitHubExtension.DataManager; -using Microsoft.Windows.DevHome.SDK; namespace GitHubExtension; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index 0fe3588b..2696215a 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -590,5 +590,10 @@ public bool IsUrlAction() { return this.Type == "Action.OpenUrl"; } + + public bool IsSubmitAction() + { + return this.Type == "Action.Submit"; + } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index d43bf15e..d31b64fd 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -70,9 +70,15 @@ public IAsyncOperation OnAction(string action, string i break; }*/ - // Inputs are validated at this point. var loginPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); + if (!loginPageActionPayload.IsSubmitAction()) + { + Log.Logger()?.ReportError($"Invalid action"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + break; + } + if (loginPageActionPayload.IsEnterprise()) { Log.Logger()?.ReportInfo($"Show Enterprise Page"); @@ -117,6 +123,13 @@ public IAsyncOperation OnAction(string action, string i break; } + if (!enterprisePageActionPayload.IsSubmitAction()) + { + Log.Logger()?.ReportError($"Invalid action"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + break; + } + // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. var enterprisePageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); @@ -226,17 +239,24 @@ public IAsyncOperation OnAction(string action, string i break; } + if (!enterprisePATPageActionPayload.IsSubmitAction()) + { + Log.Logger()?.ReportError($"Invalid action"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + break; + } + // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. var enterprisePATPageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); - Log.Logger()?.ReportInfo($"PAT Received"); - if (enterprisePATPageInputPayload?.PAT == null) + if (string.IsNullOrEmpty(enterprisePATPageInputPayload?.PAT)) { Log.Logger()?.ReportError($"PAT is null"); operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } + Log.Logger()?.ReportInfo($"PAT Received"); var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; try @@ -251,7 +271,7 @@ public IAsyncOperation OnAction(string action, string i else { Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } @@ -260,7 +280,7 @@ public IAsyncOperation OnAction(string action, string i if (ex.Message.Contains("Bad credentials") || ex.Message.Contains("Not Found")) { Log.Logger()?.ReportError($"Unauthorized Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs new file mode 100644 index 00000000..d4276947 --- /dev/null +++ b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs @@ -0,0 +1,268 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Net; +using GitHubExtension.DeveloperId; +using Microsoft.Windows.DevHome.SDK; + +namespace GitHubExtension.Test; +public partial class DeveloperIdTests +{ + private CredentialVault SetupCleanCredentialVaultClean() + { + var credentialVault = new CredentialVault(); + Assert.IsNotNull(credentialVault); + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + return credentialVault; + } + + /* Tests depend on the following environment variables: + * DEV_HOME_TEST_GITHUB_COM_USER : Url for a test user on github.com + * DEV_HOME_TEST_GITHUB_COM_PAT : Personal Access Token for the test user on github.com + */ + private CredentialVault SetupCredentialVaultWithTestUser() + { + var credentialVault = SetupCleanCredentialVaultClean(); + + var testLoginId = Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_USER") ?? string.Empty; + var testPassword = Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_PAT"); + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(testLoginId, password); + + Assert.AreEqual(1, credentialVault.GetAllCredentials().Count()); + return credentialVault; + } + + /* Tests depend on the following environment variables: + * DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER : Url for a test user on GHES + * DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT : Personal Access Token for the test user on the GHES + */ + private CredentialVault SetupCredentialVaultWithGHESTestUser() + { + var credentialVault = SetupCleanCredentialVaultClean(); + + var testLoginId = Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER") ?? string.Empty; + var testPassword = Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT"); + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(testLoginId, password); + + Assert.AreEqual(1, credentialVault.GetAllCredentials().Count()); + return credentialVault; + } + + private CredentialVault SetupCredentialVaultWithInvalidTestUser() + { + var credentialVault = SetupCleanCredentialVaultClean(); + + var testLoginId = "dummytestuser1"; + var testPassword = "invalidPAT"; + + var password = new NetworkCredential(null, testPassword).SecurePassword; + credentialVault.SaveCredentials(testLoginId, password); + + Assert.AreEqual(1, credentialVault.GetAllCredentials().Count()); + return credentialVault; + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_SingletonTest() + { + // Ensure that the DeveloperIdProvider is a singleton + var devIdProvider1 = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider1); + Assert.IsInstanceOfType(devIdProvider1, typeof(DeveloperIdProvider)); + + var devIdProvider2 = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider2); + Assert.IsInstanceOfType(devIdProvider2, typeof(DeveloperIdProvider)); + + Assert.AreSame(devIdProvider1, devIdProvider2); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_GetDeveloperIds_Empty() + { + // Remove any existing credentials + var credentialVault = SetupCleanCredentialVaultClean(); + + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + + var result = devIdProvider.GetLoggedInDeveloperIds(); + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.AreEqual(0, result.DeveloperIds.Count()); + + // Cleanup + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_RestoreAndGetDeveloperIds() + { + // Setup CredentialVault with a dummy testuser and valid PAT for Github.com + var credentialVault = SetupCredentialVaultWithTestUser(); + + // Test whether the DeveloperIdProvider can restore the saved credentials + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + var result = devIdProvider.GetLoggedInDeveloperIds(); + + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.AreEqual(1, result.DeveloperIds.Count()); + Assert.AreNotEqual("dummytestuser1", result.DeveloperIds.First().LoginId); + Assert.IsNotNull(new Uri(result.DeveloperIds.First().Url)); + + // Cleanup + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_GetDeveloperIds_InvalidPAT() + { + // Setup CredentialVault with a dummy testuser and invalid PAT for Github.com + var credentialVault = SetupCredentialVaultWithInvalidTestUser(); + + // Test whether the DeveloperIdProvider can restore the saved credentials + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + var result = devIdProvider.GetLoggedInDeveloperIds(); + + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.AreEqual(0, result.DeveloperIds.Count()); + + // Cleanup + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_AuthenticationExperienceKind() + { + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + Assert.AreEqual(AuthenticationExperienceKind.CardSession, devIdProvider.GetAuthenticationExperienceKind()); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_GetLoginAdaptiveCardSession() + { + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + var result = devIdProvider.GetLoginAdaptiveCardSession(); + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.IsNotNull(result.AdaptiveCardSession); + Assert.IsInstanceOfType(result.AdaptiveCardSession, typeof(IExtensionAdaptiveCardSession)); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_ShowLogonSession_NeverImplemented() + { + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + DeveloperIdResult? result = null; + try + { + result = devIdProvider.ShowLogonSession(default(Microsoft.UI.WindowId)).GetResults(); + } + catch (Exception e) + { + Assert.IsInstanceOfType(e, typeof(NotImplementedException)); + } + + Assert.IsNull(result); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_GetDeveloperIdState_NeverImplemented() + { + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + + var result = devIdProvider.GetDeveloperIdState(new DeveloperId.DeveloperId()); + Assert.IsNotNull(result); + Assert.AreEqual(AuthenticationState.LoggedOut, result); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_LogoutDeveloperId_InvalidDeveloperId() + { + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + + var result = devIdProvider.LogoutDeveloperId(new DeveloperId.DeveloperId()); + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Failure, result.Status); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_LogoutDeveloperId_Success() + { + // Setup CredentialVault with a dummy testuser and valid PAT for Github.com + var credentialVault = SetupCredentialVaultWithTestUser(); + + // Test whether the DeveloperIdProvider can restore the saved credentials + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + var resultGetLoggedInDeveloperIds = devIdProvider.GetLoggedInDeveloperIds(); + + Assert.IsNotNull(resultGetLoggedInDeveloperIds); + Assert.AreEqual(ProviderOperationStatus.Success, resultGetLoggedInDeveloperIds.Result.Status); + Assert.AreEqual(1, resultGetLoggedInDeveloperIds.DeveloperIds.Count()); + var devId = resultGetLoggedInDeveloperIds.DeveloperIds.First(); + Assert.IsNotNull(devId); + + var resultLogoutDeveloperId = devIdProvider.LogoutDeveloperId(devId); + Assert.IsNotNull(resultLogoutDeveloperId); + Assert.AreEqual(ProviderOperationStatus.Success, resultLogoutDeveloperId.Status); + Assert.AreEqual(AuthenticationState.LoggedOut, devIdProvider.GetDeveloperIdState(devId)); + + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } + + [TestMethod] + [TestCategory("Unit")] + public void DeveloperIdProvider_LogoutDeveloperId_GHES_Success() + { + // Setup CredentialVault with a dummy testuser and valid PAT for Github Enterprise Server + var credentialVault = SetupCredentialVaultWithGHESTestUser(); + + // Test whether the DeveloperIdProvider can restore the saved credentials + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + var resultGetLoggedInDeveloperIds = devIdProvider.GetLoggedInDeveloperIds(); + + Assert.IsNotNull(resultGetLoggedInDeveloperIds); + Assert.AreEqual(ProviderOperationStatus.Success, resultGetLoggedInDeveloperIds.Result.Status); + Assert.AreEqual(1, resultGetLoggedInDeveloperIds.DeveloperIds.Count()); + var devId = resultGetLoggedInDeveloperIds.DeveloperIds.First(); + Assert.IsNotNull(devId); + + var resultLogoutDeveloperId = devIdProvider.LogoutDeveloperId(devId); + Assert.IsNotNull(resultLogoutDeveloperId); + Assert.AreEqual(ProviderOperationStatus.Success, resultLogoutDeveloperId.Status); + Assert.AreEqual(AuthenticationState.LoggedOut, devIdProvider.GetDeveloperIdState(devId)); + + credentialVault.RemoveAllCredentials(); + Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + } +} diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs index 11d82599..6a6c000a 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdTestsSetup.cs @@ -1,12 +1,7 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Security; using GitHubExtension.DeveloperId; -using Microsoft.UI; -using Microsoft.Windows.DevHome.SDK; -using Octokit; -using Windows.Foundation; namespace GitHubExtension.Test; @@ -41,102 +36,6 @@ public string? HostAddress } } - public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard - { - private int updateCount; - - public int UpdateCount - { - get => updateCount; - set => updateCount = value; - } - - public MockExtensionAdaptiveCard(string templateJson, string dataJson, string state) - { - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - } - - public string DataJson - { - get; set; - } - - public string State - { - get; set; - } - - public string TemplateJson - { - get; set; - } - - public ProviderOperationResult Update(string templateJson, string dataJson, string state) - { - UpdateCount++; - TemplateJson = templateJson; - DataJson = dataJson; - State = state; - return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); - } - } - - public class MockDeveloperIdProvider : IDeveloperIdProviderInternal - { - private static MockDeveloperIdProvider? instance; - - public string DisplayName => throw new NotImplementedException(); - - public event TypedEventHandler Changed; - - public void Dispose() - { - GC.SuppressFinalize(this); - } - - public AuthenticationExperienceKind GetAuthenticationExperienceKind() => throw new NotImplementedException(); - - public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) => throw new NotImplementedException(); - - public DeveloperIdsResult GetLoggedInDeveloperIds() => throw new NotImplementedException(); - - public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() => throw new NotImplementedException(); - - public Windows.Foundation.IAsyncOperation LoginNewDeveloperIdAsync() - { - return Task.Run(() => - { - return (IDeveloperId)new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); - }).AsAsyncOperation(); - } - - public DeveloperId.DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken) - { - // This is a mock method, so we don't need to do anything here. Using Changed to avoid build warning. - _ = Changed.GetInvocationList(); - return new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); - } - - public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) => throw new NotImplementedException(); - - public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); - - private MockDeveloperIdProvider() - { - Changed += (IDeveloperIdProvider sender, IDeveloperId args) => { }; - } - - public static MockDeveloperIdProvider GetInstance() - { - instance ??= new MockDeveloperIdProvider(); - return instance; - } - - public IEnumerable GetLoggedInDeveloperIdsInternal() => throw new NotImplementedException(); - } - public TestContext? TestContext { get; @@ -158,9 +57,10 @@ public void TestInitialize() var testListener = new TestListener("TestListener", TestContext!); log.AddListener(testListener); DataModel.Log.Attach(log); + DeveloperId.Log.Attach(log); TestOptions = TestHelpers.SetupTempTestOptions(TestContext!); - TestContext?.WriteLine("DeveloperIdTests use the same credential store as Dev Home Github Extension"); + TestContext?.WriteLine("DeveloperIdTests may use the same credential store as Dev Home Github Extension"); // Remove any existing credentials var credentialVault = new CredentialVault(); diff --git a/test/GitHubExtension/DeveloperId/FunctionalTests.cs b/test/GitHubExtension/DeveloperId/FunctionalTests.cs new file mode 100644 index 00000000..c641bef5 --- /dev/null +++ b/test/GitHubExtension/DeveloperId/FunctionalTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using GitHubExtension.DeveloperId; +using Microsoft.Windows.DevHome.SDK; + +namespace GitHubExtension.Test; +public partial class DeveloperIdTests +{ + [TestMethod] + [TestCategory("Functional")] + public async Task FunctionalTest_RestoreAndRetrieveRepositoriesAsync() + { + var credentialVault = SetupCredentialVaultWithTestUser(); + + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + + var devIds = devIdProvider.GetLoggedInDeveloperIds().DeveloperIds; + Assert.IsNotNull(devIds); + Assert.AreEqual(1, devIds.Count()); + + var devId = devIds.First(); + Assert.IsNotNull(devId); + + // Get Repository Provider + var manualResetEvent = new ManualResetEvent(false); + var githubExtension = new GitHubExtension(manualResetEvent); + var repositoryObject = githubExtension.GetProvider(ProviderType.Repository); + Assert.IsNotNull(repositoryObject); + + var repositoryProvider = repositoryObject as IRepositoryProvider; + Assert.IsNotNull(repositoryProvider); + + var result = await repositoryProvider.GetRepositoriesAsync(devId); + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.IsNotNull(result.Repositories); + Assert.IsTrue(result.Repositories.Count() > 1); + + credentialVault.RemoveAllCredentials(); + } + + [TestMethod] + [TestCategory("Functional")] + public async Task FunctionalTest_GHES_RestoreAndRetrieveRepositoriesAsync() + { + var credentialVault = SetupCredentialVaultWithGHESTestUser(); + + var devIdProvider = DeveloperIdProvider.GetInstance(); + Assert.IsNotNull(devIdProvider); + + var devIds = devIdProvider.GetLoggedInDeveloperIds().DeveloperIds; + Assert.IsNotNull(devIds); + Assert.AreEqual(1, devIds.Count()); + + var devId = devIds.First(); + Assert.IsNotNull(devId); + + // Get Repository Provider + var manualResetEvent = new ManualResetEvent(false); + var githubExtension = new GitHubExtension(manualResetEvent); + var repositoryObject = githubExtension.GetProvider(ProviderType.Repository); + Assert.IsNotNull(repositoryObject); + + var repositoryProvider = repositoryObject as IRepositoryProvider; + Assert.IsNotNull(repositoryProvider); + + var result = await repositoryProvider.GetRepositoriesAsync(devId); + Assert.IsNotNull(result); + Assert.AreEqual(ProviderOperationStatus.Success, result.Result.Status); + Assert.IsNotNull(result.Repositories); + Assert.IsTrue(result.Repositories.Count() > 1); + + credentialVault.RemoveAllCredentials(); + } +} diff --git a/test/GitHubExtension/DeveloperId/LoginUITests.cs b/test/GitHubExtension/DeveloperId/LoginUITests.cs index 71b4d8bb..868dc9ec 100644 --- a/test/GitHubExtension/DeveloperId/LoginUITests.cs +++ b/test/GitHubExtension/DeveloperId/LoginUITests.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. using GitHubExtension.DeveloperId; +using GitHubExtension.Test.Mocks; +using Microsoft.Windows.DevHome.SDK; namespace GitHubExtension.Test; @@ -10,7 +12,7 @@ public partial class DeveloperIdTests { public const string EMPTYJSON = "{}"; - private struct LoginUITestData + public struct LoginUITestData { public const string GithubButtonAction = "{\"id\":\"Personal\",\"style\":\"positive\",\"title\":\"Sign in to github.com\",\"tooltip\":\"Opens the browser to log you into GitHub\",\"type\":\"Action.Submit\"}"; public const string GithubButtonInput = EMPTYJSON; @@ -32,21 +34,22 @@ private struct LoginUITestData public const string ConnectButtonAction = "{\"id\":\"Connect\",\"style\":\"positive\",\"title\":\"Connect\",\"type\":\"Action.Submit\"}"; public const string NullPATInput = "{\"PAT\":\"\"}"; - public const string BadPATInput = "{\"PAT\":\"Enterprise\"}"; + public const string BadPAT = "BadPAT"; + public const string BadPATInput = "{\"PAT\":\"" + BadPAT + "\"}"; public static readonly string GoodPATEnterpriseServerPATInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT") + "\"}"; public static readonly string GoodPATGithubComPATInput = "{\"PAT\":\"" + Environment.GetEnvironmentVariable("DEV_HOME_TEST_GITHUB_COM_PAT") + "\"}"; } [TestMethod] [TestCategory("Unit")] - public void LoginUIControllerInitializeTest() + public void LoginUI_ControllerInitializeTest() { var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); - controller.Initialize(testExtensionAdaptiveCard); + Assert.AreEqual(ProviderOperationStatus.Success, controller.Initialize(testExtensionAdaptiveCard).Status); // Verify that the initial state is the login page. Assert.IsTrue(testExtensionAdaptiveCard.State == Enum.GetName(typeof(LoginUIState), LoginUIState.LoginPage)); @@ -67,7 +70,8 @@ public void LoginUIControllerInitializeTest() [DataRow("EnterpriseServerPATPage", LoginUITestData.ClickHereUrlAction, LoginUITestData.ClickHereUrlInput, "EnterpriseServerPATPage", 1)] [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.NullPATInput, "EnterpriseServerPATPage")] [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, LoginUITestData.BadPATInput, "EnterpriseServerPATPage")] - public async Task LoginUIControllerTest( + [DataRow("EnterpriseServerPATPage", LoginUITestData.ConnectButtonAction, EMPTYJSON, "EnterpriseServerPATPage")] + public async Task LoginUI_ControllerTestSuccess( string initialState, string actions, string inputs, string finalState, int numOfFinalUpdates = 2) { var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); @@ -75,7 +79,7 @@ public async Task LoginUIControllerTest( // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); - controller.Initialize(testExtensionAdaptiveCard); + Assert.AreEqual(ProviderOperationStatus.Success, controller.Initialize(testExtensionAdaptiveCard).Status); Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); // Set the initial state. @@ -90,7 +94,46 @@ public async Task LoginUIControllerTest( } // Call OnAction() with the actions and inputs. - await controller.OnAction(actions, inputs); + Assert.AreEqual(ProviderOperationStatus.Success, (await controller.OnAction(actions, inputs)).Status); + + // Verify the final state + Assert.AreEqual(finalState, testExtensionAdaptiveCard.State); + Assert.AreEqual(numOfFinalUpdates, testExtensionAdaptiveCard.UpdateCount); + + controller.Dispose(); + } + + [TestMethod] + [TestCategory("Unit")] + [DataRow("WaitingPage", EMPTYJSON, EMPTYJSON, "WaitingPage", 1)] + [DataRow("PageDoesn'tExist", EMPTYJSON, EMPTYJSON, "PageDoesn'tExist", 1)] + [DataRow("LoginPage", EMPTYJSON, EMPTYJSON, "LoginPage", 1)] + [DataRow("EnterpriseServerPATPage", EMPTYJSON, EMPTYJSON, "EnterpriseServerPATPage", 1)] + [DataRow("EnterpriseServerPage", EMPTYJSON, EMPTYJSON, "EnterpriseServerPage", 1)] + public async Task LoginUI_ControllerTestFailure( + string initialState, string actions, string inputs, string finalState, int numOfFinalUpdates = 2) + { + var testExtensionAdaptiveCard = new MockExtensionAdaptiveCard(string.Empty, string.Empty, string.Empty); + Assert.AreEqual(0, testExtensionAdaptiveCard.UpdateCount); + + // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. + var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); + Assert.AreEqual(ProviderOperationStatus.Success, controller.Initialize(testExtensionAdaptiveCard).Status); + Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); + + // Set the initial state. + testExtensionAdaptiveCard.State = initialState; + Assert.AreEqual(initialState, testExtensionAdaptiveCard.State); + + // Set HostAddress for EnterpriseServerPATPage to make this a valid state + if (initialState == "EnterpriseServerPATPage") + { + controller.HostAddress = new Uri("https://www.github.com"); + Assert.AreEqual("https://www.github.com", controller.HostAddress.OriginalString); + } + + // Call OnAction() with the actions and inputs. + Assert.AreEqual(ProviderOperationStatus.Failure, (await controller.OnAction(actions, inputs)).Status); // Verify the final state Assert.AreEqual(finalState, testExtensionAdaptiveCard.State); @@ -106,7 +149,7 @@ public async Task LoginUIControllerTest( */ [TestMethod] [TestCategory("Unit")] - public async Task LoginUI_PATLoginTest_Success() + public async Task LoginUI_ControllerPATLoginTest_Success() { // Create DataRows during Runtime since these need Env vars RuntimeDataRow[] dataRows = @@ -137,7 +180,7 @@ public async Task LoginUI_PATLoginTest_Success() // Create a LoginUIController and initialize it with the testExtensionAdaptiveCard. var controller = new LoginUIController(MockDeveloperIdProvider.GetInstance()); - controller.Initialize(testExtensionAdaptiveCard); + Assert.AreEqual(ProviderOperationStatus.Success, controller.Initialize(testExtensionAdaptiveCard).Status); Assert.AreEqual(1, testExtensionAdaptiveCard.UpdateCount); // Set the initial state. @@ -152,7 +195,7 @@ public async Task LoginUI_PATLoginTest_Success() } // Call OnAction() with the actions and inputs. - await controller.OnAction(dataRow.Actions ?? string.Empty, dataRow.Inputs ?? string.Empty); + Assert.AreEqual(ProviderOperationStatus.Success, (await controller.OnAction(dataRow.Actions ?? string.Empty, dataRow.Inputs ?? string.Empty)).Status); // Verify the final state Assert.AreEqual(dataRow.FinalState, testExtensionAdaptiveCard.State); diff --git a/test/GitHubExtension/Mocks/DeveloperIdProvider.cs b/test/GitHubExtension/Mocks/DeveloperIdProvider.cs new file mode 100644 index 00000000..2919cb7a --- /dev/null +++ b/test/GitHubExtension/Mocks/DeveloperIdProvider.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation and Contributors +// Licensed under the MIT license. + +using System.Security; +using GitHubExtension.DeveloperId; +using Microsoft.UI; +using Microsoft.Windows.DevHome.SDK; +using Octokit; +using Windows.Foundation; + +namespace GitHubExtension.Test.Mocks; + +public class MockDeveloperIdProvider : IDeveloperIdProviderInternal +{ + private static MockDeveloperIdProvider? instance; + + public string DisplayName => throw new NotImplementedException(); + + public event TypedEventHandler Changed; + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public AuthenticationExperienceKind GetAuthenticationExperienceKind() => throw new NotImplementedException(); + + public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) => throw new NotImplementedException(); + + public DeveloperIdsResult GetLoggedInDeveloperIds() => throw new NotImplementedException(); + + public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() => throw new NotImplementedException(); + + public IAsyncOperation LoginNewDeveloperIdAsync() + { + return Task.Run(() => + { + return (IDeveloperId)new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); + }).AsAsyncOperation(); + } + + public DeveloperId.DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken) + { + var pat = new System.Net.NetworkCredential(string.Empty, personalAccessToken).Password; + if (pat == DeveloperIdTests.LoginUITestData.BadPAT) + { + throw new InvalidOperationException("Invalid PAT"); + } + + // This is a mock method, so we don't need to do anything here. Using Changed to avoid build warning. + _ = Changed.GetInvocationList(); + return new DeveloperId.DeveloperId(string.Empty, string.Empty, string.Empty, string.Empty, new GitHubClient(new ProductHeaderValue("Test"))); + } + + public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) => throw new NotImplementedException(); + + public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); + + private MockDeveloperIdProvider() + { + Changed += (sender, args) => { }; + } + + public static MockDeveloperIdProvider GetInstance() + { + instance ??= new MockDeveloperIdProvider(); + return instance; + } + + public IEnumerable GetLoggedInDeveloperIdsInternal() => throw new NotImplementedException(); +} + +public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard +{ + private int updateCount; + + public int UpdateCount + { + get => updateCount; + set => updateCount = value; + } + + public MockExtensionAdaptiveCard(string templateJson, string dataJson, string state) + { + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + } + + public string DataJson + { + get; set; + } + + public string State + { + get; set; + } + + public string TemplateJson + { + get; set; + } + + public ProviderOperationResult Update(string templateJson, string dataJson, string state) + { + UpdateCount++; + TemplateJson = templateJson; + DataJson = dataJson; + State = state; + return new ProviderOperationResult(ProviderOperationStatus.Success, null, "Update() succeeded", "Update() succeeded"); + } +} From c5f40eee9a2bc8c936b7fdd4b331e3aed7868790 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Mon, 27 Nov 2023 11:52:29 -0800 Subject: [PATCH 17/27] Revert changes to allow multi-user for tests --- src/GitHubExtension/DeveloperId/LoginUIController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index d31b64fd..ed68e66c 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -62,13 +62,13 @@ public IAsyncOperation OnAction(string action, string i try { // If there is already a developer id, we should block another login. - /*if (_developerIdProvider.GetLoggedInDeveloperIdsInternal().Any()) + if (_developerIdProvider.GetLoggedInDeveloperIdsInternal().Any()) { Log.Logger()?.ReportInfo($"DeveloperId {_developerIdProvider.GetLoggedInDeveloperIdsInternal().First().LoginId} already exists. Blocking login."); new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Only one DeveloperId can be logged in at a time", "One DeveloperId already exists"); break; - }*/ + } var loginPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); From 4f1159c1079096995ec14c8e0e0dca036e93bbc4 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Mon, 27 Nov 2023 19:58:31 -0600 Subject: [PATCH 18/27] Update GitHubExt-CI.yml Add env vars to use saved secrets --- .github/workflows/GitHubExt-CI.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/GitHubExt-CI.yml b/.github/workflows/GitHubExt-CI.yml index a4420bf6..6508cd95 100644 --- a/.github/workflows/GitHubExt-CI.yml +++ b/.github/workflows/GitHubExt-CI.yml @@ -70,3 +70,7 @@ jobs: - name: DevHome UnitTests if: ${{ matrix.platform != 'arm64' }} run: cmd /c "$env:VSDevTestCmd" /Platform:${{ matrix.platform }} /TestCaseFilter:"TestCategory!=LiveData" BuildOutput\\${{ matrix.configuration }}\\${{ matrix.platform }}\\GitHubExtension.Test\\GitHubExtension.Test.dll + env: + DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER }} + DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT }} + DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER }} From 28ae79cca9f7d3a433ac329f276e0a63175b4aba Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Tue, 28 Nov 2023 12:34:59 -0600 Subject: [PATCH 19/27] Update GitHubExt-CI.yml Revert adding secrets --- .github/workflows/GitHubExt-CI.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/GitHubExt-CI.yml b/.github/workflows/GitHubExt-CI.yml index 6508cd95..a4420bf6 100644 --- a/.github/workflows/GitHubExt-CI.yml +++ b/.github/workflows/GitHubExt-CI.yml @@ -70,7 +70,3 @@ jobs: - name: DevHome UnitTests if: ${{ matrix.platform != 'arm64' }} run: cmd /c "$env:VSDevTestCmd" /Platform:${{ matrix.platform }} /TestCaseFilter:"TestCategory!=LiveData" BuildOutput\\${{ matrix.configuration }}\\${{ matrix.platform }}\\GitHubExtension.Test\\GitHubExtension.Test.dll - env: - DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER }} - DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT }} - DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER: ${{ secrets.DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_USER }} From 9c5189e43f6c45aff6df170d9712d13a1d7a69fa Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Mon, 11 Dec 2023 07:53:33 -0800 Subject: [PATCH 20/27] PR Comments 1 --- .../DeveloperId/DeveloperIdProvider.cs | 8 +++++ .../LoginUI/EnterpriseServerPATPage.cs | 10 ++---- .../LoginUI/EnterpriseServerPage.cs | 10 ++---- .../DeveloperId/LoginUI/LoginFailedPage.cs | 10 ++---- .../DeveloperId/LoginUI/LoginPage.cs | 10 ++---- .../DeveloperId/LoginUI/LoginSucceededPage.cs | 9 +---- .../DeveloperId/LoginUI/LoginUIPage.cs | 4 ++- .../DeveloperId/LoginUI/WaitingPage.cs | 10 ++---- .../DeveloperId/LoginUIController.cs | 21 ++++-------- src/GitHubExtension/Helpers/Json.cs | 34 +++++++++++++++++++ .../Providers/RepositoryProvider.cs | 18 +++++++--- .../DeveloperId/DeveloperIdProviderTests.cs | 9 +++-- .../DeveloperId/FunctionalTests.cs | 4 +-- .../DeveloperId/LoginUITests.cs | 3 +- .../Mocks/DeveloperIdProvider.cs | 4 +-- 15 files changed, 86 insertions(+), 78 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 7f7913bd..28ea2526 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -315,6 +315,10 @@ private DeveloperId CreateOrUpdateDeveloperIdFromOauthRequest(OAuthRequest oauth private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) { + // We take loginIds or Urls here because in older versions of DevHome, we used loginIds to save credentials. + // In newer versions, we use Urls to save credentials. + // So, we need to check if loginId is currently used to save credential, and if so, replace it with URL. + // This is a temporary fix, and we should replace this logic once we are sure that most users have updated to newer versions of DevHome. foreach (var loginIdOrUrl in loginIdsAndUrls) { var isUrl = loginIdOrUrl.Contains('/'); @@ -378,6 +382,10 @@ public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() public void Dispose() { GC.SuppressFinalize(this); + lock (AuthenticationProviderLock) + { + singletonDeveloperIdProvider = null; + } } public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs index a878cab5..3b72234b 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; @@ -35,14 +36,7 @@ internal class PageData : ILoginUIPageData public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs index d75304cc..408cec3f 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; @@ -41,14 +42,7 @@ internal class PageData : ILoginUIPageData public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs index 93566fe8..d4d7deeb 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; @@ -18,14 +19,7 @@ internal class LoginFailedPageData : ILoginUIPageData { public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs index 3ca58be8..41d9d3c2 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; @@ -18,14 +19,7 @@ internal class PageData : ILoginUIPageData { public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs index 0c46f5f4..1c9f7d8e 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -25,14 +25,7 @@ internal class LoginSucceededPageData : ILoginUIPageData public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index 2696215a..7606a00e 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Text.Json; +using System.Text.Json.Serialization; using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; @@ -39,7 +41,7 @@ public ProviderOperationResult UpdateExtensionAdaptiveCard(IExtensionAdaptiveCar return adaptiveCard.Update(_template, _data?.GetJson(), Enum.GetName(typeof(LoginUIState), _state)); } - private string GetTemplate(LoginUIState loginUIState) + private static string GetTemplate(LoginUIState loginUIState) { var loginPage = @" { diff --git a/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs index 2f6c298e..7da82276 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; @@ -18,14 +19,7 @@ internal class WaitingPageData : ILoginUIPageData { public string GetJson() { - return JsonSerializer.Serialize( - this, - new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); + return Json.Stringify(this); } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index ed68e66c..56ff5cdf 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -17,6 +17,7 @@ public class LoginUIController : IExtensionAdaptiveCardSession private IExtensionAdaptiveCard? _loginUI; private Uri? _hostAddress; + // This variable is used to store the host address from EnterpriseServerPage. It is used in EnterpriseServerPATPage. public Uri HostAddress { get => _hostAddress ?? throw new InvalidOperationException("HostAddress is null"); @@ -70,7 +71,7 @@ public IAsyncOperation OnAction(string action, string i break; } - var loginPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); + var loginPageActionPayload = Json.ToObject(action) ?? throw new InvalidOperationException("Invalid action"); if (!loginPageActionPayload.IsSubmitAction()) { @@ -113,7 +114,7 @@ public IAsyncOperation OnAction(string action, string i case nameof(LoginUIState.EnterpriseServerPage): { // Check if the user clicked on Cancel button. - var enterprisePageActionPayload = CreateFromJson(action) + var enterprisePageActionPayload = Json.ToObject(action) ?? throw new InvalidOperationException("Invalid action"); if (enterprisePageActionPayload.IsCancelAction()) @@ -131,7 +132,7 @@ public IAsyncOperation OnAction(string action, string i } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. - var enterprisePageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); + var enterprisePageInputPayload = Json.ToObject(inputs) ?? throw new InvalidOperationException("Invalid inputs"); Log.Logger()?.ReportInfo($"EnterpriseServer: {enterprisePageInputPayload?.EnterpriseServer}"); if (enterprisePageInputPayload?.EnterpriseServer == null) @@ -188,7 +189,7 @@ public IAsyncOperation OnAction(string action, string i } // Check if the user clicked on Cancel button. - var enterprisePATPageActionPayload = CreateFromJson(action) ?? throw new InvalidOperationException("Invalid action"); + var enterprisePATPageActionPayload = Json.ToObject(action) ?? throw new InvalidOperationException("Invalid action"); if (enterprisePATPageActionPayload.IsCancelAction()) { @@ -247,7 +248,7 @@ public IAsyncOperation OnAction(string action, string i } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. - var enterprisePATPageInputPayload = CreateFromJson(inputs) ?? throw new InvalidOperationException("Invalid inputs"); + var enterprisePATPageInputPayload = Json.ToObject(inputs) ?? throw new InvalidOperationException("Invalid inputs"); if (string.IsNullOrEmpty(enterprisePATPageInputPayload?.PAT)) { @@ -312,14 +313,4 @@ public IAsyncOperation OnAction(string action, string i return operationResult; }).AsAsyncOperation(); } - - private T? CreateFromJson(string json) - { - return JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - DefaultIgnoreCondition = JsonIgnoreCondition.Never, - IncludeFields = true, - }); - } } diff --git a/src/GitHubExtension/Helpers/Json.cs b/src/GitHubExtension/Helpers/Json.cs index 8e2fa183..ff3c48bd 100644 --- a/src/GitHubExtension/Helpers/Json.cs +++ b/src/GitHubExtension/Helpers/Json.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. +using System.Text.Json; +using System.Text.Json.Serialization; using Newtonsoft.Json; namespace GitHubExtension.Helpers; @@ -32,4 +34,36 @@ public static async Task StringifyAsync(T value) return JsonConvert.SerializeObject(value); }); } + + public static string Stringify(T value) + { + if (typeof(T) == typeof(bool)) + { + return value!.ToString()!.ToLowerInvariant(); + } + + return System.Text.Json.JsonSerializer.Serialize( + value, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } + + public static T? ToObject(string json) + { + if (typeof(T) == typeof(bool)) + { + return (T)(object)bool.Parse(json); + } + + return System.Text.Json.JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.Never, + IncludeFields = true, + }); + } } diff --git a/src/GitHubExtension/Providers/RepositoryProvider.cs b/src/GitHubExtension/Providers/RepositoryProvider.cs index a89e735f..fd066708 100644 --- a/src/GitHubExtension/Providers/RepositoryProvider.cs +++ b/src/GitHubExtension/Providers/RepositoryProvider.cs @@ -205,12 +205,20 @@ public IAsyncOperation CloneRepositoryAsync(IRepository { var loggedInDeveloperId = DeveloperId.DeveloperIdProvider.GetInstance().GetDeveloperIdInternal(developerId); - cloneOptions.CredentialsProvider = (url, user, cred) => new LibGit2Sharp.UsernamePasswordCredentials + try { - // Password is a PAT unique to GitHub. - Username = loggedInDeveloperId.GetCredential().Password, - Password = string.Empty, - }; + cloneOptions.CredentialsProvider = (url, user, cred) => new LibGit2Sharp.UsernamePasswordCredentials + { + // Password is a PAT unique to GitHub. + Username = loggedInDeveloperId.GetCredential().Password, + Password = string.Empty, + }; + } + catch (Exception e) + { + Log.Logger()?.ReportError("DevHomeRepository", "Could not get credentials.", e); + return new ProviderOperationResult(ProviderOperationStatus.Failure, e, "Could not get credentials.", e.Message); + } } try diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs index d4276947..74115d9c 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs @@ -14,6 +14,9 @@ private CredentialVault SetupCleanCredentialVaultClean() Assert.IsNotNull(credentialVault); credentialVault.RemoveAllCredentials(); Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); + + // Ensure that the DeveloperIdProvider is deleted and recreated on the next GetInstance() call + DeveloperIdProvider.GetInstance().Dispose(); return credentialVault; } @@ -104,7 +107,7 @@ public void DeveloperIdProvider_GetDeveloperIds_Empty() } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public void DeveloperIdProvider_RestoreAndGetDeveloperIds() { // Setup CredentialVault with a dummy testuser and valid PAT for Github.com @@ -213,7 +216,7 @@ public void DeveloperIdProvider_LogoutDeveloperId_InvalidDeveloperId() } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public void DeveloperIdProvider_LogoutDeveloperId_Success() { // Setup CredentialVault with a dummy testuser and valid PAT for Github.com @@ -240,7 +243,7 @@ public void DeveloperIdProvider_LogoutDeveloperId_Success() } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public void DeveloperIdProvider_LogoutDeveloperId_GHES_Success() { // Setup CredentialVault with a dummy testuser and valid PAT for Github Enterprise Server diff --git a/test/GitHubExtension/DeveloperId/FunctionalTests.cs b/test/GitHubExtension/DeveloperId/FunctionalTests.cs index c641bef5..5f6fa780 100644 --- a/test/GitHubExtension/DeveloperId/FunctionalTests.cs +++ b/test/GitHubExtension/DeveloperId/FunctionalTests.cs @@ -8,7 +8,7 @@ namespace GitHubExtension.Test; public partial class DeveloperIdTests { [TestMethod] - [TestCategory("Functional")] + [TestCategory("LiveData")] public async Task FunctionalTest_RestoreAndRetrieveRepositoriesAsync() { var credentialVault = SetupCredentialVaultWithTestUser(); @@ -42,7 +42,7 @@ public async Task FunctionalTest_RestoreAndRetrieveRepositoriesAsync() } [TestMethod] - [TestCategory("Functional")] + [TestCategory("LiveData")] public async Task FunctionalTest_GHES_RestoreAndRetrieveRepositoriesAsync() { var credentialVault = SetupCredentialVaultWithGHESTestUser(); diff --git a/test/GitHubExtension/DeveloperId/LoginUITests.cs b/test/GitHubExtension/DeveloperId/LoginUITests.cs index 868dc9ec..8ff7e854 100644 --- a/test/GitHubExtension/DeveloperId/LoginUITests.cs +++ b/test/GitHubExtension/DeveloperId/LoginUITests.cs @@ -62,7 +62,6 @@ public void LoginUI_ControllerInitializeTest() [TestCategory("Unit")] [DataRow("LoginPage", LoginUITestData.GithubButtonAction, LoginUITestData.GithubButtonInput, "LoginSucceededPage", 3)] [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] - [DataRow("LoginPage", LoginUITestData.GithubEnterpriseButtonAction, LoginUITestData.GithubEnterpriseButtonInput, "EnterpriseServerPage")] [DataRow("EnterpriseServerPage", LoginUITestData.NextButtonAction, LoginUITestData.BadUrlEnterpriseServerInput, "EnterpriseServerPage")] [DataRow("EnterpriseServerPage", LoginUITestData.NextButtonAction, LoginUITestData.UnreachableUrlEnterpriseServerInput, "EnterpriseServerPage")] [DataRow("EnterpriseServerPage", LoginUITestData.CancelButtonAction, LoginUITestData.CancelButtonInput, "LoginPage")] @@ -148,7 +147,7 @@ public async Task LoginUI_ControllerTestFailure( * DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER_PAT : A valid Personal Access Token for the GitHub Enterprise Server set in DEV_HOME_TEST_GITHUB_ENTERPRISE_SERVER (with at least repo_public permissions) */ [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public async Task LoginUI_ControllerPATLoginTest_Success() { // Create DataRows during Runtime since these need Env vars diff --git a/test/GitHubExtension/Mocks/DeveloperIdProvider.cs b/test/GitHubExtension/Mocks/DeveloperIdProvider.cs index 2919cb7a..cdca1a29 100644 --- a/test/GitHubExtension/Mocks/DeveloperIdProvider.cs +++ b/test/GitHubExtension/Mocks/DeveloperIdProvider.cs @@ -27,7 +27,7 @@ public void Dispose() public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) => throw new NotImplementedException(); - public DeveloperIdsResult GetLoggedInDeveloperIds() => throw new NotImplementedException(); + public DeveloperIdsResult GetLoggedInDeveloperIds() => new (new List()); public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() => throw new NotImplementedException(); @@ -67,7 +67,7 @@ public static MockDeveloperIdProvider GetInstance() return instance; } - public IEnumerable GetLoggedInDeveloperIdsInternal() => throw new NotImplementedException(); + public IEnumerable GetLoggedInDeveloperIdsInternal() => new List(); } public class MockExtensionAdaptiveCard : IExtensionAdaptiveCard From 1cfa81afc8e0cb23a8023de4c2ed949727225bf0 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Mon, 11 Dec 2023 07:54:55 -0800 Subject: [PATCH 21/27] PR comments 2 --- src/GitHubExtension/Client/Validation.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/GitHubExtension/Client/Validation.cs b/src/GitHubExtension/Client/Validation.cs index fb1f088d..50034c92 100644 --- a/src/GitHubExtension/Client/Validation.cs +++ b/src/GitHubExtension/Client/Validation.cs @@ -223,6 +223,7 @@ public static bool IsReachableGitHubEnterpriseServerURL(Uri server) { if (new EnterpriseProbe(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)).Probe(server).Result != EnterpriseProbeResult.Ok) { + Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} is not reachable"); return false; } From 0799379a6afb07e4647dd1a281f533999600fa6d Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Wed, 27 Dec 2023 13:01:40 -0800 Subject: [PATCH 22/27] Ignore some tests in pipeline --- test/GitHubExtension/DeveloperId/CredentialVaultTests.cs | 6 +++--- .../GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs index ccf5d900..2be88ece 100644 --- a/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs +++ b/test/GitHubExtension/DeveloperId/CredentialVaultTests.cs @@ -26,7 +26,7 @@ public void CredentialVault_CreateSingleton() } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] [DataRow("testuser1")] [DataRow("https://github.com/testuser2")] [DataRow("https://RandomWebServer.example/testuser3")] @@ -53,7 +53,7 @@ public void CredentialVault_SaveAndRetrieveCredential(string loginId) } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] [DataRow("testuser1")] [DataRow("https://github.com/testuser2")] [DataRow("https://RandomWebServer.example/testuser3")] @@ -80,7 +80,7 @@ public void CredentialVault_RemoveAndRetrieveCredential(string loginId) } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public void CredentialVault_GetAllCredentials() { var credentialVault = new CredentialVault("DevHomeGitHubExtensionTest"); diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs index 74115d9c..f85e058d 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs @@ -130,7 +130,7 @@ public void DeveloperIdProvider_RestoreAndGetDeveloperIds() } [TestMethod] - [TestCategory("Unit")] + [TestCategory("LiveData")] public void DeveloperIdProvider_GetDeveloperIds_InvalidPAT() { // Setup CredentialVault with a dummy testuser and invalid PAT for Github.com From 54fcfedc278a0d0407d1d33660546beb825db0d1 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 29 Dec 2023 12:24:11 -0800 Subject: [PATCH 23/27] PR Comments 3 --- .../DeveloperId/CredentialVault.cs | 23 ++++++++++++------- .../LoginUI/EnterpriseServerPATPage.cs | 2 +- .../DeveloperId/LoginUIController.cs | 8 +++---- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index a8f06a72..be682d27 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -76,8 +76,8 @@ public void SaveCredentials(string loginId, SecureString? accessToken) var isCredentialRetrieved = CredRead(credentialNameToRetrieve, CRED_TYPE.GENERIC, 0, out ptrToCredential); if (!isCredentialRetrieved) { - Log.Logger()?.ReportInfo($"Retrieving credentials from Credential Manager has failed"); - return null; + Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed for {loginId}"); + throw new Win32Exception(Marshal.GetLastWin32Error()); } CREDENTIAL credentialObject; @@ -90,7 +90,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) } else { - Log.Logger()?.ReportInfo("No credentials found for this DeveloperId"); + Log.Logger()?.ReportError($"No credentials found for this DeveloperId : {loginId}"); return null; } @@ -109,10 +109,10 @@ public void SaveCredentials(string loginId, SecureString? accessToken) var credential = new PasswordCredential(credentialResourceName, loginId, accessTokenString); return credential; } - catch (Exception) + catch (Exception ex) { - Log.Logger()?.ReportInfo($"Retrieving credentials from Credential Manager has failed unexpectedly"); - throw new Win32Exception(Marshal.GetLastWin32Error()); + Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed unexpectedly: {loginId} : {ex.Message}"); + throw; } finally { @@ -129,7 +129,7 @@ public void RemoveCredentials(string loginId) var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0); if (!isCredentialDeleted) { - Log.Logger()?.ReportInfo($"Deleting credentials from Credential Manager has failed"); + Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed for {loginId}"); throw new Win32Exception(Marshal.GetLastWin32Error()); } } @@ -193,7 +193,14 @@ public void RemoveAllCredentials() var allCredentials = GetAllCredentials(); foreach (var credential in allCredentials) { - RemoveCredentials(credential); + try + { + RemoveCredentials(credential); + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed unexpectedly: {credential} : {ex.Message}"); + } } } } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs index 3b72234b..f96ff9cc 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs @@ -17,7 +17,7 @@ public EnterpriseServerPATPage(Uri hostAddress, string errorText, string? inputP EnterpriseServerPATPageInputValue = inputPAT ?? string.Empty, EnterpriseServerPATPageErrorValue = errorText ?? string.Empty, EnterpriseServerPATPageErrorVisible = !string.IsNullOrEmpty(errorText), - EnterpriseServerPATPageCreatePATUrlValue = hostAddress?.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomePAT", + EnterpriseServerPATPageCreatePATUrlValue = hostAddress?.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomeGitHubExtension", EnterpriseServerPATPageServerUrlValue = hostAddress?.OriginalString ?? string.Empty, }; } diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index 56ff5cdf..cdb06f3d 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -48,7 +48,7 @@ public IAsyncOperation OnAction(string action, string i { if (_loginUI == null) { - Log.Logger()?.ReportError($"_loginUI is null"); + Log.Logger()?.ReportError($"OnAction() called with invalid state of LoginUI"); return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "_loginUI is null", "_loginUI is null"); } @@ -75,7 +75,7 @@ public IAsyncOperation OnAction(string action, string i if (!loginPageActionPayload.IsSubmitAction()) { - Log.Logger()?.ReportError($"Invalid action"); + Log.Logger()?.ReportError($"Invalid action performed on LoginUI: {loginPageActionPayload.Id}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); break; } @@ -119,14 +119,14 @@ public IAsyncOperation OnAction(string action, string i if (enterprisePageActionPayload.IsCancelAction()) { - Log.Logger()?.ReportInfo($"Cancel clicked"); + Log.Logger()?.ReportInfo($"Cancel clicked on EnterpriseServerPage"); operationResult = new LoginPage().UpdateExtensionAdaptiveCard(_loginUI); break; } if (!enterprisePageActionPayload.IsSubmitAction()) { - Log.Logger()?.ReportError($"Invalid action"); + Log.Logger()?.ReportError($"Invalid action performed on LoginUI: {enterprisePageActionPayload.Id}"); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); break; } From cc1faf56c2a769bfa893ec530096061ee98b536f Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 5 Jan 2024 12:53:34 -0800 Subject: [PATCH 24/27] PR Comments 4 --- .../DeveloperId/LoginUI/LoginSucceededPage.cs | 2 +- .../DeveloperId/LoginUI/LoginUIPage.cs | 6 ++- .../DeveloperId/LoginUIController.cs | 2 +- .../Strings/en-US/Resources.resw | 54 ++++++++++--------- 4 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs index 1c9f7d8e..f36284b5 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -15,7 +15,7 @@ public LoginSucceededPage(IDeveloperId developerId) { Data = new LoginSucceededPageData() { - Message = $"{developerId.LoginId} {Resources.GetResource("LoginUI_LoginSucceededPage_text")}", + Message = $"{Resources.GetResource("LoginUI_LoginSucceededPage_text").Replace("{User}", developerId.LoginId)}", }; } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index 7606a00e..faae5ff2 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -353,7 +353,7 @@ private static string GetTemplate(LoginUIState loginUIState) ""inlines"": [ { ""type"": ""TextRun"", - ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Text")} " + @""" + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Text1")} " + @""" }, { ""type"": ""TextRun"", @@ -362,6 +362,10 @@ private static string GetTemplate(LoginUIState loginUIState) ""type"": ""Action.OpenUrl"", ""url"": ""${EnterpriseServerPATPageCreatePATUrlValue}"" } + }, + { + ""type"": ""TextRun"", + ""text"": """ + $"{Resources.GetResource("LoginUI_EnterprisePATPage_Text2")} " + @""" } ] }, diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index cdb06f3d..4550935e 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -286,7 +286,7 @@ public IAsyncOperation OnAction(string action, string i } Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorText")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorPrefix")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); break; } } diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index 8794dfa5..9d3c3990 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -225,21 +225,23 @@ Go - Please enter a valid GitHub enterprise server URL + Please enter a valid GitHub Enterprise Server URL + "GitHub Enterprise Server" should not be localized Server URL - <Enter the GitHub Server link> + <Enter the GitHub Enterprise Server link> + "GitHub Enterprise Server" should not be localized Sign in to GitHub Enterprise Server - Button name to login to GHES + Button name to login to GitHub Enterprise Server Lets you enter the host address of your GitHub Enterprise Server - Tooltip text on mouse hover over GHES button + Tooltip text on mouse hover over GitHub Enterprise Server button GitHub @@ -250,8 +252,8 @@ Page subtitle - has logged in successfully! You may close this window. - Partial text for when a user signs in successfully + {User} has logged in successfully! You may close this window. + Text for when a user signs in successfully Waiting for browser... @@ -437,7 +439,7 @@ Enterprise Server is not valid - LoginUI Error text + LoginUI Error text if input is not valid GitHub @@ -445,7 +447,7 @@ Enter server address here - LoginUI placeholder text + LoginUI Input placeholder text Enterprise Server @@ -461,38 +463,42 @@ URL is invalid - LoginUI Error text + LoginUI Error text if input is not a valid URL Enterprise Server is not reachable - LoginUI Error text - - - click here. - LoginUI Instructional text with hyperlink + LoginUI Error text if input is not reachable - Enter personal access token + Enter Personal Access Token LoginUI placeholder text - - Enter your Personal Access Token (PAT) to connect to ${EnterpriseServerPATPageServerUrlValue}. To create a new PAT, - LoginUI Instructional text + + Enter your Personal Access Token (PAT) to connect to ${EnterpriseServerPATPageServerUrlValue}. To create a new PAT, + LoginUI Instructional text before highlighted text. This will be displayed as "LoginUI_EnterprisePATPage_Text1+LoginUI_EnterprisePATPage_HighlightedText+LoginUI_EnterprisePATPage_Text2". + + + click here + LoginUI Instructional text with hyperlink. This will be displayed as "LoginUI_EnterprisePATPage_Text1+LoginUI_EnterprisePATPage_HighlightedText+LoginUI_EnterprisePATPage_Text2". + + + . + LoginUI Instructional text after highlighted text. In some languages, there might be some text after the highlighted text with link. This will be displayed as "LoginUI_EnterprisePATPage_Text1+LoginUI_EnterprisePATPage_HighlightedText+LoginUI_EnterprisePATPage_Text2". Something went wrong - LoginUI Error text + LoginUI Error text for generic case - PAT doesn't work for GHES endpoint - LoginUI Error text + PAT doesn't work for this GitHub Enterprise Server endpoint + LoginUI Error text if input Personal Access Token does not work; "GitHub Enterprise Server" should not be localized - + Error: - LoginUI Error text + LoginUI Error Prefix Please enter the PAT - LoginUI Error text + LoginUI Error text if input is null \ No newline at end of file From dec0a5c7f1a8c24d120454063bb1882f82595248 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Fri, 5 Jan 2024 12:57:53 -0800 Subject: [PATCH 25/27] Minor update --- src/GitHubExtension/Strings/en-US/Resources.resw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GitHubExtension/Strings/en-US/Resources.resw b/src/GitHubExtension/Strings/en-US/Resources.resw index 9d3c3990..9f74e894 100644 --- a/src/GitHubExtension/Strings/en-US/Resources.resw +++ b/src/GitHubExtension/Strings/en-US/Resources.resw @@ -215,7 +215,7 @@ Sign in to github.com - Button name to login to GitHub.com + Button name to login to GitHub.com. "github.com" should not be localized. Opens the browser to log you into GitHub From 1374d114d9f6f5bd6469e47a7b7b4e36c5517915 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Wed, 10 Jan 2024 10:31:06 -0800 Subject: [PATCH 26/27] PR comments 6 --- .../DeveloperId/CredentialVault.cs | 12 +++- .../DeveloperId/DeveloperIdProvider.cs | 56 ++++++++----------- .../LoginUI/EnterpriseServerPATPage.cs | 9 ++- .../DeveloperId/LoginUIController.cs | 26 +++++---- .../DeveloperId/DeveloperIdProviderTests.cs | 2 +- 5 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index be682d27..40916b8f 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -76,8 +76,16 @@ public void SaveCredentials(string loginId, SecureString? accessToken) var isCredentialRetrieved = CredRead(credentialNameToRetrieve, CRED_TYPE.GENERIC, 0, out ptrToCredential); if (!isCredentialRetrieved) { - Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed for {loginId}"); - throw new Win32Exception(Marshal.GetLastWin32Error()); + var error = Marshal.GetLastWin32Error(); + Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed for {loginId} with {error}"); + + // NotFound is expected and can be ignored. + if (error == 1168) + { + return null; + } + + throw new Win32Exception(error); } CREDENTIAL credentialObject; diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index 28ea2526..c927d5c3 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -18,10 +18,6 @@ public class DeveloperIdProvider : IDeveloperIdProviderInternal private static readonly object OAuthRequestsLock = new (); - private static readonly object CredentialVaultLock = new (); - - private static readonly object AuthenticationProviderLock = new (); - // DeveloperId list containing all Logged in Ids. private List DeveloperIds { @@ -34,10 +30,15 @@ private List OAuthRequests get; set; } - private readonly CredentialVault credentialVault; + private readonly Lazy credentialVault; // DeveloperIdProvider uses singleton pattern. - private static DeveloperIdProvider? singletonDeveloperIdProvider; + private static Lazy singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); + + public static DeveloperIdProvider GetInstance() + { + return singletonDeveloperIdProvider.Value; + } public event TypedEventHandler? Changed; @@ -50,10 +51,7 @@ private DeveloperIdProvider() { Log.Logger()?.ReportInfo($"Creating DeveloperIdProvider singleton instance"); - lock (CredentialVaultLock) - { - credentialVault ??= new CredentialVault(); - } + credentialVault = new (() => new CredentialVault()); lock (OAuthRequestsLock) { @@ -66,7 +64,7 @@ private DeveloperIdProvider() try { // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(credentialVault.GetAllCredentials()); + RestoreDeveloperIds(credentialVault.Value.GetAllCredentials()); } catch (Exception error) { @@ -75,16 +73,6 @@ private DeveloperIdProvider() } } - public static DeveloperIdProvider GetInstance() - { - lock (AuthenticationProviderLock) - { - singletonDeveloperIdProvider ??= new DeveloperIdProvider(); - } - - return singletonDeveloperIdProvider; - } - public DeveloperIdsResult GetLoggedInDeveloperIds() { List iDeveloperIds = new (); @@ -176,7 +164,7 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) return new ProviderOperationResult(ProviderOperationStatus.Failure, new ArgumentNullException(nameof(developerId)), "The developer account to log out does not exist", "Unable to find DeveloperId to logout"); } - credentialVault.RemoveCredentials(developerIdToLogout.Url); + credentialVault.Value.RemoveCredentials(developerIdToLogout.Url); DeveloperIds?.Remove(developerIdToLogout); } @@ -258,7 +246,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString try { // Save the credential to Credential Vault. - credentialVault.SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); + credentialVault.Value.SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); try { @@ -282,7 +270,7 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString DeveloperIds.Add(newDeveloperId); } - credentialVault.SaveCredentials(newDeveloperId.Url, accessToken); + credentialVault.Value.SaveCredentials(newDeveloperId.Url, accessToken); try { @@ -328,7 +316,7 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new (credentialVault.GetCredentials(loginIdOrUrl)?.Password), + Credentials = new (credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password), }; var user = gitHubClient.User.Current().Result; @@ -347,10 +335,10 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) { try { - credentialVault.SaveCredentials( + credentialVault.Value.SaveCredentials( user.Url, - new NetworkCredential(string.Empty, credentialVault.GetCredentials(loginIdOrUrl)?.Password).SecurePassword); - credentialVault.RemoveCredentials(loginIdOrUrl); + new NetworkCredential(string.Empty, credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password).SecurePassword); + credentialVault.Value.RemoveCredentials(loginIdOrUrl); Log.Logger()?.ReportInfo($"Replaced {loginIdOrUrl} with {user.Url} in CredentialManager"); } catch (Exception error) @@ -382,10 +370,12 @@ public AdaptiveCardSessionResult GetLoginAdaptiveCardSession() public void Dispose() { GC.SuppressFinalize(this); - lock (AuthenticationProviderLock) - { - singletonDeveloperIdProvider = null; - } + } + + // This function is to be used for testing purposes only. + public static void ResetInstanceForTests() + { + singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); } public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); @@ -407,5 +397,5 @@ public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) } } - internal PasswordCredential? GetCredentials(IDeveloperId developerId) => credentialVault.GetCredentials(developerId.Url); + internal PasswordCredential? GetCredentials(IDeveloperId developerId) => credentialVault.Value.GetCredentials(developerId.Url); } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs index f96ff9cc..de7bc318 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPATPage.cs @@ -1,24 +1,23 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Security; using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; internal class EnterpriseServerPATPage : LoginUIPage { - public EnterpriseServerPATPage(Uri hostAddress, string errorText, string? inputPAT) + public EnterpriseServerPATPage(Uri hostAddress, string errorText, SecureString inputPAT) : base(LoginUIState.EnterpriseServerPATPage) { Data = new PageData() { - EnterpriseServerPATPageInputValue = inputPAT ?? string.Empty, + EnterpriseServerPATPageInputValue = new System.Net.NetworkCredential(string.Empty, inputPAT).Password ?? string.Empty, EnterpriseServerPATPageErrorValue = errorText ?? string.Empty, EnterpriseServerPATPageErrorVisible = !string.IsNullOrEmpty(errorText), EnterpriseServerPATPageCreatePATUrlValue = hostAddress?.GetComponents(UriComponents.SchemeAndServer, UriFormat.UriEscaped) + $"/settings/tokens/new?scopes=read:user,notifications,repo,read:org&description=DevHomeGitHubExtension", - EnterpriseServerPATPageServerUrlValue = hostAddress?.OriginalString ?? string.Empty, + EnterpriseServerPATPageServerUrlValue = hostAddress?.Host ?? string.Empty, }; } diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index 4550935e..d2d58f9f 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -49,7 +49,7 @@ public IAsyncOperation OnAction(string action, string i if (_loginUI == null) { Log.Logger()?.ReportError($"OnAction() called with invalid state of LoginUI"); - return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "_loginUI is null", "_loginUI is null"); + return new ProviderOperationResult(ProviderOperationStatus.Failure, null, "OnAction() called with invalid state of LoginUI", "_loginUI is null"); } ProviderOperationResult operationResult; @@ -76,7 +76,7 @@ public IAsyncOperation OnAction(string action, string i if (!loginPageActionPayload.IsSubmitAction()) { Log.Logger()?.ReportError($"Invalid action performed on LoginUI: {loginPageActionPayload.Id}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action performed on LoginUI", "Invalid action performed on LoginUI"); break; } @@ -127,7 +127,7 @@ public IAsyncOperation OnAction(string action, string i if (!enterprisePageActionPayload.IsSubmitAction()) { Log.Logger()?.ReportError($"Invalid action performed on LoginUI: {enterprisePageActionPayload.Id}"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action performed on LoginUI", "Invalid action"); break; } @@ -167,7 +167,7 @@ public IAsyncOperation OnAction(string action, string i try { - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: string.Empty, inputPAT: string.Empty).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: string.Empty, inputPAT: new NetworkCredential(null, string.Empty).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); } catch (Exception ex) { @@ -242,23 +242,25 @@ public IAsyncOperation OnAction(string action, string i if (!enterprisePATPageActionPayload.IsSubmitAction()) { - Log.Logger()?.ReportError($"Invalid action"); - operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action", "Invalid action"); + Log.Logger()?.ReportError($"Invalid action performed on LoginUI"); + operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, "Invalid action performed on LoginUI", "Invalid action"); break; } // Otherwise user clicked on Next button. We should validate the inputs and update the UI with PAT page. var enterprisePATPageInputPayload = Json.ToObject(inputs) ?? throw new InvalidOperationException("Invalid inputs"); - if (string.IsNullOrEmpty(enterprisePATPageInputPayload?.PAT)) + if (string.IsNullOrEmpty(enterprisePATPageInputPayload.PAT)) { Log.Logger()?.ReportError($"PAT is null"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_NullErrorText")}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; } Log.Logger()?.ReportInfo($"PAT Received"); - var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword; + var securePAT = new NetworkCredential(null, enterprisePATPageInputPayload.PAT).SecurePassword; + enterprisePATPageInputPayload.PAT = string.Empty; + enterprisePATPageInputPayload = null; try { @@ -272,7 +274,7 @@ public IAsyncOperation OnAction(string action, string i else { Log.Logger()?.ReportError($"PAT doesn't work for GHES endpoint {_hostAddress.OriginalString}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; } } @@ -281,12 +283,12 @@ public IAsyncOperation OnAction(string action, string i if (ex.Message.Contains("Bad credentials") || ex.Message.Contains("Not Found")) { Log.Logger()?.ReportError($"Unauthorized Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; } Log.Logger()?.ReportError($"Error: {ex}"); - operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorPrefix")} {ex}", inputPAT: enterprisePATPageInputPayload?.PAT).UpdateExtensionAdaptiveCard(_loginUI); + operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorPrefix")} {ex}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; } } diff --git a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs index f85e058d..b419d8d5 100644 --- a/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs +++ b/test/GitHubExtension/DeveloperId/DeveloperIdProviderTests.cs @@ -16,7 +16,7 @@ private CredentialVault SetupCleanCredentialVaultClean() Assert.AreEqual(0, credentialVault.GetAllCredentials().Count()); // Ensure that the DeveloperIdProvider is deleted and recreated on the next GetInstance() call - DeveloperIdProvider.GetInstance().Dispose(); + DeveloperIdProvider.ResetInstanceForTests(); return credentialVault; } From dda9219a7798121d9409b2b8d719a19c932b9ff7 Mon Sep 17 00:00:00 2001 From: Vineeth Thomas Alex Date: Thu, 11 Jan 2024 13:46:50 -0800 Subject: [PATCH 27/27] PR Comments 7 --- .../Client/GithubClientProvider.cs | 2 +- src/GitHubExtension/Client/Validation.cs | 15 +- .../DeveloperId/CredentialVault.cs | 28 ++-- .../DeveloperId/DeveloperIdProvider.cs | 155 ++++++++++-------- .../IDeveloperIdProviderInternal.cs | 8 + .../LoginUI/EnterpriseServerPage.cs | 2 - .../DeveloperId/LoginUI/LoginFailedPage.cs | 2 - .../DeveloperId/LoginUI/LoginPage.cs | 2 - .../DeveloperId/LoginUI/LoginSucceededPage.cs | 2 - .../DeveloperId/LoginUI/LoginUIPage.cs | 2 - .../DeveloperId/LoginUI/WaitingPage.cs | 2 - .../DeveloperId/LoginUIController.cs | 20 +-- 12 files changed, 134 insertions(+), 106 deletions(-) diff --git a/src/GitHubExtension/Client/GithubClientProvider.cs b/src/GitHubExtension/Client/GithubClientProvider.cs index f8af81e9..02cd9109 100644 --- a/src/GitHubExtension/Client/GithubClientProvider.cs +++ b/src/GitHubExtension/Client/GithubClientProvider.cs @@ -90,7 +90,7 @@ public async Task GetClientForLoggedInDeveloper(bool logRateLimit } catch (Exception ex) { - Log.Logger()?.ReportInfo($"Rate limiting not enabled for server: {ex.Message}"); + Log.Logger()?.ReportError($"Rate limiting not enabled for server.", ex); } } diff --git a/src/GitHubExtension/Client/Validation.cs b/src/GitHubExtension/Client/Validation.cs index 50034c92..d38bde43 100644 --- a/src/GitHubExtension/Client/Validation.cs +++ b/src/GitHubExtension/Client/Validation.cs @@ -219,11 +219,20 @@ private static string AddProtocolToString(string s) return n; } - public static bool IsReachableGitHubEnterpriseServerURL(Uri server) + public static async Task IsReachableGitHubEnterpriseServerURL(Uri server) { - if (new EnterpriseProbe(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)).Probe(server).Result != EnterpriseProbeResult.Ok) + try { - Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} is not reachable"); + var probeResult = await new EnterpriseProbe(new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME)).Probe(server); + if (probeResult != EnterpriseProbeResult.Ok) + { + Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} is not reachable"); + return false; + } + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"EnterpriseServer {server.AbsoluteUri} could not be probed.", ex); return false; } diff --git a/src/GitHubExtension/DeveloperId/CredentialVault.cs b/src/GitHubExtension/DeveloperId/CredentialVault.cs index 40916b8f..3c3abcf1 100644 --- a/src/GitHubExtension/DeveloperId/CredentialVault.cs +++ b/src/GitHubExtension/DeveloperId/CredentialVault.cs @@ -10,25 +10,30 @@ namespace GitHubExtension.DeveloperId; public class CredentialVault : ICredentialVault { - private readonly string credentialResourceName; + private readonly string _credentialResourceName; private static class CredentialVaultConfiguration { public const string CredResourceName = "GitHubDevHomeExtension"; } + // Win32 Error codes + public const int Win32ErrorNotFound = 1168; + public CredentialVault(string applicationName = "") { - credentialResourceName = string.IsNullOrEmpty(applicationName) ? CredentialVaultConfiguration.CredResourceName : applicationName; + _credentialResourceName = string.IsNullOrEmpty(applicationName) ? CredentialVaultConfiguration.CredResourceName : applicationName; } + private string AddCredentialResourceNamePrefix(string loginId) => _credentialResourceName + ": " + loginId; + public void SaveCredentials(string loginId, SecureString? accessToken) { // Initialize a credential object. var credential = new CREDENTIAL { Type = CRED_TYPE.GENERIC, - TargetName = credentialResourceName + ": " + loginId, + TargetName = AddCredentialResourceNamePrefix(loginId), UserName = loginId, Persist = (int)CRED_PERSIST.LocalMachine, AttributeCount = 0, @@ -68,7 +73,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) public PasswordCredential? GetCredentials(string loginId) { - var credentialNameToRetrieve = credentialResourceName + ": " + loginId; + var credentialNameToRetrieve = AddCredentialResourceNamePrefix(loginId); var ptrToCredential = IntPtr.Zero; try @@ -80,7 +85,7 @@ public void SaveCredentials(string loginId, SecureString? accessToken) Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed for {loginId} with {error}"); // NotFound is expected and can be ignored. - if (error == 1168) + if (error == Win32ErrorNotFound) { return null; } @@ -114,12 +119,12 @@ public void SaveCredentials(string loginId, SecureString? accessToken) accessTokenInChars[i] = '\0'; } - var credential = new PasswordCredential(credentialResourceName, loginId, accessTokenString); + var credential = new PasswordCredential(_credentialResourceName, loginId, accessTokenString); return credential; } catch (Exception ex) { - Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed unexpectedly: {loginId} : {ex.Message}"); + Log.Logger()?.ReportError($"Retrieving credentials from Credential Manager has failed unexpectedly: {loginId} : ", ex); throw; } finally @@ -133,12 +138,11 @@ public void SaveCredentials(string loginId, SecureString? accessToken) public void RemoveCredentials(string loginId) { - var targetCredentialToDelete = credentialResourceName + ": " + loginId; + var targetCredentialToDelete = AddCredentialResourceNamePrefix(loginId); var isCredentialDeleted = CredDelete(targetCredentialToDelete, CRED_TYPE.GENERIC, 0); if (!isCredentialDeleted) { Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed for {loginId}"); - throw new Win32Exception(Marshal.GetLastWin32Error()); } } @@ -151,7 +155,7 @@ public IEnumerable GetAllCredentials() IntPtr[] allCredentials; uint count; - if (CredEnumerate(credentialResourceName + "*", 0, out count, out ptrToCredential) != false) + if (CredEnumerate(_credentialResourceName + "*", 0, out count, out ptrToCredential) != false) { allCredentials = new IntPtr[count]; Marshal.Copy(ptrToCredential, allCredentials, 0, (int)count); @@ -161,7 +165,7 @@ public IEnumerable GetAllCredentials() var error = Marshal.GetLastWin32Error(); // NotFound is expected and can be ignored. - if (error == 1168) + if (error == Win32ErrorNotFound) { return Enumerable.Empty(); } @@ -207,7 +211,7 @@ public void RemoveAllCredentials() } catch (Exception ex) { - Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed unexpectedly: {credential} : {ex.Message}"); + Log.Logger()?.ReportError($"Deleting credentials from Credential Manager has failed unexpectedly: {credential} : ", ex); } } } diff --git a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs index c927d5c3..895ff03b 100644 --- a/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs +++ b/src/GitHubExtension/DeveloperId/DeveloperIdProvider.cs @@ -14,9 +14,9 @@ namespace GitHubExtension.DeveloperId; public class DeveloperIdProvider : IDeveloperIdProviderInternal { // Locks to control access to Singleton class members. - private static readonly object DeveloperIdsLock = new (); + private static readonly object _developerIdsLock = new (); - private static readonly object OAuthRequestsLock = new (); + private static readonly object _oAuthRequestsLock = new (); // DeveloperId list containing all Logged in Ids. private List DeveloperIds @@ -30,15 +30,7 @@ private List OAuthRequests get; set; } - private readonly Lazy credentialVault; - - // DeveloperIdProvider uses singleton pattern. - private static Lazy singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); - - public static DeveloperIdProvider GetInstance() - { - return singletonDeveloperIdProvider.Value; - } + private readonly Lazy _credentialVault; public event TypedEventHandler? Changed; @@ -46,37 +38,46 @@ public static DeveloperIdProvider GetInstance() public string DisplayName => "GitHub"; + // DeveloperIdProvider uses singleton pattern. + private static Lazy _singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); + + public static DeveloperIdProvider GetInstance() + { + return _singletonDeveloperIdProvider.Value; + } + // Private constructor for Singleton class. private DeveloperIdProvider() { Log.Logger()?.ReportInfo($"Creating DeveloperIdProvider singleton instance"); - credentialVault = new (() => new CredentialVault()); + _credentialVault = new (() => new CredentialVault()); - lock (OAuthRequestsLock) + lock (_oAuthRequestsLock) { OAuthRequests ??= new List(); } - lock (DeveloperIdsLock) + lock (_developerIdsLock) { DeveloperIds ??= new List(); - try - { - // Retrieve and populate Logged in DeveloperIds from previous launch. - RestoreDeveloperIds(credentialVault.Value.GetAllCredentials()); - } - catch (Exception error) - { - Log.Logger()?.ReportError($"Error while restoring DeveloperIds: {error.Message}. Proceeding without restoring."); - } + } + + try + { + // Retrieve and populate Logged in DeveloperIds from previous launch. + RestoreDeveloperIds(_credentialVault.Value.GetAllCredentials()); + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error while restoring DeveloperIds: {ex.Message}. Proceeding without restoring.", ex); } } public DeveloperIdsResult GetLoggedInDeveloperIds() { List iDeveloperIds = new (); - lock (DeveloperIdsLock) + lock (_developerIdsLock) { iDeveloperIds.AddRange(DeveloperIds); } @@ -125,7 +126,7 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers } catch (Exception ex) { - Log.Logger()?.ReportError($"Error while logging in with PAT to {hostAddress.AbsoluteUri} : {ex.Message}"); + Log.Logger()?.ReportError($"Error while logging in with PAT to {hostAddress.AbsoluteUri} : ", ex); throw; } } @@ -134,7 +135,7 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers { OAuthRequest oauthRequest = new (); - lock (OAuthRequestsLock) + lock (_oAuthRequestsLock) { OAuthRequests.Add(oauthRequest); try @@ -142,10 +143,10 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers oauthRequest.BeginOAuthRequest(); return oauthRequest; } - catch (Exception error) + catch (Exception ex) { OAuthRequests.Remove(oauthRequest); - Log.Logger()?.ReportError($"Unable to complete OAuth request: {error.Message}"); + Log.Logger()?.ReportError($"Unable to complete OAuth request: ", ex); } } @@ -155,7 +156,7 @@ public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString pers public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) { DeveloperId? developerIdToLogout; - lock (DeveloperIdsLock) + lock (_developerIdsLock) { developerIdToLogout = DeveloperIds?.Find(e => e.LoginId == developerId.LoginId); if (developerIdToLogout == null) @@ -164,7 +165,7 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) return new ProviderOperationResult(ProviderOperationStatus.Failure, new ArgumentNullException(nameof(developerId)), "The developer account to log out does not exist", "Unable to find DeveloperId to logout"); } - credentialVault.Value.RemoveCredentials(developerIdToLogout.Url); + _credentialVault.Value.RemoveCredentials(developerIdToLogout.Url); DeveloperIds?.Remove(developerIdToLogout); } @@ -172,9 +173,9 @@ public ProviderOperationResult LogoutDeveloperId(IDeveloperId developerId) { Changed?.Invoke(this as IDeveloperIdProvider, developerIdToLogout as IDeveloperId); } - catch (Exception error) + catch (Exception ex) { - Log.Logger()?.ReportError($"LoggedOut event signaling failed: {error}"); + Log.Logger()?.ReportError($"LoggedOut event signaling failed: ", ex); } return new ProviderOperationResult(ProviderOperationStatus.Success, null, "The developer account has been logged out successfully", "LogoutDeveloperId succeeded"); @@ -184,7 +185,7 @@ public void HandleOauthRedirection(Uri authorizationResponse) { OAuthRequest? oAuthRequest = null; - lock (OAuthRequestsLock) + lock (_oAuthRequestsLock) { if (OAuthRequests is null) { @@ -193,8 +194,10 @@ public void HandleOauthRedirection(Uri authorizationResponse) if (OAuthRequests.Count is 0) { + // This could happen if the user refreshes the redirected browser window + // causing the OAuth response to be received again. Log.Logger()?.ReportWarn($"No saved OAuth requests to match OAuth response"); - throw new InvalidOperationException(); + return; } var state = OAuthRequest.RetrieveState(authorizationResponse); @@ -203,6 +206,8 @@ public void HandleOauthRedirection(Uri authorizationResponse) if (oAuthRequest == null) { + // This could happen if the user refreshes a previously redirected browser window instead of using + // the new browser window for the response. Log the warning and return. Log.Logger()?.ReportWarn($"Unable to find valid request for received OAuth response"); return; } @@ -218,7 +223,7 @@ public void HandleOauthRedirection(Uri authorizationResponse) public IEnumerable GetLoggedInDeveloperIdsInternal() { List iDeveloperIds = new (); - lock (DeveloperIdsLock) + lock (_developerIdsLock) { iDeveloperIds.AddRange(DeveloperIds); } @@ -246,15 +251,15 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString try { // Save the credential to Credential Vault. - credentialVault.Value.SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); + _credentialVault.Value.SaveCredentials(duplicateDeveloperIds.Single().Url, accessToken); try { Changed?.Invoke(this as IDeveloperIdProvider, duplicateDeveloperIds.Single() as IDeveloperId); } - catch (Exception error) + catch (Exception ex) { - Log.Logger()?.ReportError($"Updated event signaling failed: {error}"); + Log.Logger()?.ReportError($"Updated event signaling failed: ", ex); } } catch (InvalidOperationException) @@ -265,20 +270,20 @@ private void SaveOrOverwriteDeveloperId(DeveloperId newDeveloperId, SecureString } else { - lock (DeveloperIdsLock) + lock (_developerIdsLock) { DeveloperIds.Add(newDeveloperId); } - credentialVault.Value.SaveCredentials(newDeveloperId.Url, accessToken); + _credentialVault.Value.SaveCredentials(newDeveloperId.Url, accessToken); try { Changed?.Invoke(this as IDeveloperIdProvider, newDeveloperId as IDeveloperId); } - catch (Exception error) + catch (Exception ex) { - Log.Logger()?.ReportError($"LoggedIn event signaling failed: {error}"); + Log.Logger()?.ReportError($"LoggedIn event signaling failed: ", ex); } } } @@ -309,6 +314,8 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) // This is a temporary fix, and we should replace this logic once we are sure that most users have updated to newer versions of DevHome. foreach (var loginIdOrUrl in loginIdsAndUrls) { + // Since GitHub loginIds cannot contain /, and URLs would, this is sufficient to differentiate between + // loginIds and URLs. We could alternatively use TryCreate, but there could be some GHES urls that we miss. var isUrl = loginIdOrUrl.Contains('/'); // For loginIds without URL, use GitHub.com as default. @@ -316,41 +323,55 @@ private void RestoreDeveloperIds(IEnumerable loginIdsAndUrls) GitHubClient gitHubClient = new (new ProductHeaderValue(Constants.DEV_HOME_APPLICATION_NAME), hostAddress) { - Credentials = new (credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password), + Credentials = new (_credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password), }; - var user = gitHubClient.User.Current().Result; - - DeveloperId developerId = new (user.Login, user.Name, user.Email, user.Url, gitHubClient); - - lock (DeveloperIdsLock) - { - DeveloperIds.Add(developerId); - } - - Log.Logger()?.ReportInfo($"Restored DeveloperId {user.Url}"); - - // If loginId is currently used to save credential, remove it, and use URL instead. - if (!isUrl) + try { - try + var user = gitHubClient.User.Current().Result; + DeveloperId developerId = new (user.Login, user.Name, user.Email, user.Url, gitHubClient); + lock (_developerIdsLock) { - credentialVault.Value.SaveCredentials( - user.Url, - new NetworkCredential(string.Empty, credentialVault.Value.GetCredentials(loginIdOrUrl)?.Password).SecurePassword); - credentialVault.Value.RemoveCredentials(loginIdOrUrl); - Log.Logger()?.ReportInfo($"Replaced {loginIdOrUrl} with {user.Url} in CredentialManager"); + DeveloperIds.Add(developerId); } - catch (Exception error) + + Log.Logger()?.ReportInfo($"Restored DeveloperId {user.Url}"); + + // If loginId is currently used to save credential, remove it, and use URL instead. + if (!isUrl) { - Log.Logger()?.ReportError($"Error while replacing {loginIdOrUrl} with {user.Url} in CredentialManager: {error.Message}"); + ReplaceSavedLoginIdWithUrl(developerId); } } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error while restoring DeveloperId {loginIdOrUrl} : ", ex); + + // If we are unable to restore a DeveloperId, remove it from CredentialManager to avoid + // the same error next time, and to force the user to login again + _credentialVault.Value.RemoveCredentials(loginIdOrUrl); + } } return; } + private void ReplaceSavedLoginIdWithUrl(DeveloperId developerId) + { + try + { + _credentialVault.Value.SaveCredentials( + developerId.Url, + new NetworkCredential(string.Empty, _credentialVault.Value.GetCredentials(developerId.LoginId)?.Password).SecurePassword); + _credentialVault.Value.RemoveCredentials(developerId.LoginId); + Log.Logger()?.ReportInfo($"Replaced {developerId.LoginId} with {developerId.Url} in CredentialManager"); + } + catch (Exception ex) + { + Log.Logger()?.ReportError($"Error while replacing {developerId.LoginId} with {developerId.Url} in CredentialManager: ", ex); + } + } + internal void RefreshDeveloperId(IDeveloperId developerIdInternal) { Changed?.Invoke(this as IDeveloperIdProvider, developerIdInternal as IDeveloperId); @@ -375,7 +396,7 @@ public void Dispose() // This function is to be used for testing purposes only. public static void ResetInstanceForTests() { - singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); + _singletonDeveloperIdProvider = new (() => new DeveloperIdProvider()); } public IAsyncOperation ShowLogonSession(WindowId windowHandle) => throw new NotImplementedException(); @@ -383,7 +404,7 @@ public static void ResetInstanceForTests() public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) { DeveloperId? developerIdToFind; - lock (DeveloperIdsLock) + lock (_developerIdsLock) { developerIdToFind = DeveloperIds?.Find(e => e.LoginId == developerId.LoginId); if (developerIdToFind == null) @@ -397,5 +418,5 @@ public AuthenticationState GetDeveloperIdState(IDeveloperId developerId) } } - internal PasswordCredential? GetCredentials(IDeveloperId developerId) => credentialVault.Value.GetCredentials(developerId.Url); + internal PasswordCredential? GetCredentials(IDeveloperId developerId) => _credentialVault.Value.GetCredentials(developerId.Url); } diff --git a/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs b/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs index 56e96b71..1ac28961 100644 --- a/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs +++ b/src/GitHubExtension/DeveloperId/IDeveloperIdProviderInternal.cs @@ -6,11 +6,19 @@ using Windows.Foundation; namespace GitHubExtension.DeveloperId; + +// This internal interface extends the public interface IDeveloperIdProvider public interface IDeveloperIdProviderInternal : IDeveloperIdProvider { + // This method triggers login flow for a new developer id through the browser. + // This is called when the user clicks on the "Sign in" button in LoginUI popup. public IAsyncOperation LoginNewDeveloperIdAsync(); + // This method triggers login flow for a new developer id using the provided personal access token. + // This will not open the browser. This is used when the user selects the "Sign in with GHES" + // option in LoginUI popup. public DeveloperId LoginNewDeveloperIdWithPAT(Uri hostAddress, SecureString personalAccessToken); + // This method returns the list of developer id objects that are currently logged in. public IEnumerable GetLoggedInDeveloperIdsInternal(); } diff --git a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs index 408cec3f..de2fb908 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/EnterpriseServerPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs index d4d7deeb..3d546869 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginFailedPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs index 41d9d3c2..cc7bd0bf 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs index f36284b5..942fbb0b 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginSucceededPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs index faae5ff2..34c5a3ab 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/LoginUIPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; using Microsoft.Windows.DevHome.SDK; diff --git a/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs index 7da82276..7401ea06 100644 --- a/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs +++ b/src/GitHubExtension/DeveloperId/LoginUI/WaitingPage.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors // Licensed under the MIT license. -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Helpers; namespace GitHubExtension.DeveloperId.LoginUI; diff --git a/src/GitHubExtension/DeveloperId/LoginUIController.cs b/src/GitHubExtension/DeveloperId/LoginUIController.cs index d2d58f9f..419ef0aa 100644 --- a/src/GitHubExtension/DeveloperId/LoginUIController.cs +++ b/src/GitHubExtension/DeveloperId/LoginUIController.cs @@ -2,8 +2,6 @@ // Licensed under the MIT license. using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; using GitHubExtension.Client; using GitHubExtension.DeveloperId.LoginUI; using GitHubExtension.Helpers; @@ -103,7 +101,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - Log.Logger()?.ReportError($"Error: {ex}"); + Log.Logger()?.ReportError($"Error: ", ex); new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, ex, "Error occurred in login page", ex.Message); } @@ -146,7 +144,7 @@ public IAsyncOperation OnAction(string action, string i { // Probe for Enterprise Server instance _hostAddress = new Uri(enterprisePageInputPayload.EnterpriseServer); - if (!Validation.IsReachableGitHubEnterpriseServerURL(_hostAddress)) + if (!await Validation.IsReachableGitHubEnterpriseServerURL(_hostAddress)) { operationResult = new EnterpriseServerPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_UnreachableErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; @@ -154,13 +152,13 @@ public IAsyncOperation OnAction(string action, string i } catch (UriFormatException ufe) { - Log.Logger()?.ReportError($"Error: {ufe}"); + Log.Logger()?.ReportError($"Error: ", ufe); operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_UriErrorText")}").UpdateExtensionAdaptiveCard(_loginUI); break; } catch (Exception ex) { - Log.Logger()?.ReportError($"Error: {ex}"); + Log.Logger()?.ReportError($"Error: ", ex); operationResult = new EnterpriseServerPage(hostAddress: enterprisePageInputPayload.EnterpriseServer, errorText: $"{Resources.GetResource("LoginUI_EnterprisePage_GenericErrorText")} : {ex}").UpdateExtensionAdaptiveCard(_loginUI); break; } @@ -171,7 +169,7 @@ public IAsyncOperation OnAction(string action, string i } catch (Exception ex) { - Log.Logger()?.ReportError($"Error: {ex}"); + Log.Logger()?.ReportError($"Error: ", ex); operationResult = new LoginFailedPage().UpdateExtensionAdaptiveCard(_loginUI); } @@ -225,13 +223,13 @@ public IAsyncOperation OnAction(string action, string i } catch (UriFormatException ufe) { - Log.Logger()?.ReportError($"Error: {ufe}"); + Log.Logger()?.ReportError($"Error: ", ufe); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error: {ufe}", $"Error: {ufe}"); break; } catch (Exception ex) { - Log.Logger()?.ReportError($"Error: {ex}"); + Log.Logger()?.ReportError($"Error: ", ex); operationResult = new ProviderOperationResult(ProviderOperationStatus.Failure, null, $"Error: {ex}", $"Error: {ex}"); break; } @@ -282,12 +280,12 @@ public IAsyncOperation OnAction(string action, string i { if (ex.Message.Contains("Bad credentials") || ex.Message.Contains("Not Found")) { - Log.Logger()?.ReportError($"Unauthorized Error: {ex}"); + Log.Logger()?.ReportError($"Unauthorized Error: ", ex); operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_BadCredentialsErrorText")} {_hostAddress.OriginalString}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; } - Log.Logger()?.ReportError($"Error: {ex}"); + Log.Logger()?.ReportError($"Error: ", ex); operationResult = new EnterpriseServerPATPage(hostAddress: _hostAddress, errorText: $"{Resources.GetResource("LoginUI_EnterprisePATPage_GenericErrorPrefix")} {ex}", inputPAT: new NetworkCredential(null, enterprisePATPageInputPayload?.PAT).SecurePassword).UpdateExtensionAdaptiveCard(_loginUI); break; }