diff --git a/README.md b/README.md index 60f9f88f..80cef0f1 100644 --- a/README.md +++ b/README.md @@ -176,16 +176,13 @@ Running unit and E2E tests are a great way to ensure that functionality is prese * Fill out the test parameters in the `WingetCreateTests/Test.runsettings` file * `WingetPkgsTestRepoOwner`: The repository owner of the winget-pkgs-submission-test repo. (Repo owner must be forked from main "winget-pkgs-submission-test" repo) * `WingetPkgsTestRepo`: The winget-pkgs test repository. (winget-pkgs-submission-test) - * `GitHubApiKey`: GitHub personal access token for testing. - * Instructions on [how to generate your own GitHubApiKey](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token). - * Direct link to GitHub [Personal Access Tokens page](https://github.com/settings/tokens). - * `GitHubAppPrivateKey`: Leave blank, this is only used by the build server. * Set the solution wide runsettings file for the tests * Go to `Test` menu > `Configure Run Settings` -> `Select Solution Wide runsettings File` -> Choose your configured runsettings file -> [!CAUTION] -> You should treat your access token like a password. To avoid exposing your PAT, be sure to reset changes to the `WingetCreateTests/Test.runsettings` file before committing your changes. You can also use the command `git update-index --skip-worktree src/WingetCreateTests/WingetCreateTests/Test.runsettings` command to untrack changes to the file and prevent it from being committed. +* Set up your github token: + * __[Recommended]__ Run 'wingetcreate token -s` to go through the Github authentication flow + * Or create a personal access token with the `repo` permission and set it as an environment variable `WINGET_CREATE_GITHUB_TOKEN`. _(This option is more convenient for CI/CD pipelines.)_ ## Contributing diff --git a/doc/new-locale.md b/doc/new-locale.md index e6d09e67..a7eceee4 100644 --- a/doc/new-locale.md +++ b/doc/new-locale.md @@ -28,7 +28,7 @@ The following arguments are available: | **-r, --reference-locale** | Existing locale manifest to be used as reference for default values. If not provided, the default locale manifest will be used. | **-o, --out** | The output directory where the newly created manifests will be saved locally. | **-f,--format** | Output format of the manifest. Default is "yaml". | -| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo | +| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command | Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions). diff --git a/doc/new.md b/doc/new.md index b3687803..e0471c2a 100644 --- a/doc/new.md +++ b/doc/new.md @@ -28,7 +28,7 @@ The following arguments are available: |--------------|-------------| | **-o,--out** | The output directory where the newly created manifests will be saved locally | | **-f,--format** | Output format of the manifest. Default is "yaml". | -| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo | +| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command | ## Winget-Create New Command flow diff --git a/doc/show.md b/doc/show.md index 2265ed76..1586b683 100644 --- a/doc/show.md +++ b/doc/show.md @@ -28,7 +28,7 @@ The following arguments are available: | **-l, --locale-manifests** | Switch to display all locale manifests. | **--version-manifest** | Switch to display the version manifest. | **-f,--format** | Output format of the manifest. Default is "yaml". | -| **-t, --token** | GitHub personal access token used for authenticated access to the GitHub API. It is recommended to provide a token to get a higher [API rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting). +| **-t, --token** | GitHub personal access token used for authenticated access to the GitHub API. It is recommended to provide a token to get a higher [API rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting).
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command. | Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions). diff --git a/doc/submit.md b/doc/submit.md index 6d70f56d..fea551cb 100644 --- a/doc/submit.md +++ b/doc/submit.md @@ -17,7 +17,7 @@ The following arguments are available: |--------------|-------------| | **-p, --prtitle** | The title of the pull request submitted to GitHub. | **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false. -| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. +| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command. | If you have provided your [GitHub token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) on the command line with the **submit** command and the device is registered with GitHub, **Winget-Create** will submit your PR to [Windows Package Manager repo](https://docs.microsoft.com/windows/package-manager/). diff --git a/doc/token.md b/doc/token.md index 478ff798..e8a0e38d 100644 --- a/doc/token.md +++ b/doc/token.md @@ -7,6 +7,13 @@ Instructions on setting up GitHub Token for Winget-Create can be found [here](.. ## Usage +> [!WARNING] +> Using the `--token` argument may result in the token being logged. +> +> For local development, it is recommended to go through the OAuth flow by omitting the `--token` argument. +> +> For CI/CD scenarios, it is recommended to use the 'WINGET_CREATE_GITHUB_TOKEN' environment variable to store the token. + `wingetcreate.exe token [\]` ### Store a new GitHub token in your local cache @@ -25,5 +32,5 @@ The following arguments are available: |---------------- |-------------| | **-c, --clear** | Required. Clear the cached GitHub token | **-s, --store** | Required. Set the cached GitHub token. Can specify token to cache with --token parameter, otherwise will initiate OAuth flow. -| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. +| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | **-?, --help** | Gets additional help on this command. | diff --git a/doc/update-locale.md b/doc/update-locale.md index 3fc33118..c4e0e966 100644 --- a/doc/update-locale.md +++ b/doc/update-locale.md @@ -27,7 +27,7 @@ The following arguments are available: | **-l, --locale** | The package locale to update the manifest for. If not provided, the tool will prompt you a list of existing locales to choose from. | **-o, --out** | The output directory where the newly created manifests will be saved locally. | **-f,--format** | Output format of the manifest. Default is "yaml". | -| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo | +| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command | Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions). diff --git a/doc/update.md b/doc/update.md index 457a750e..513647d2 100644 --- a/doc/update.md +++ b/doc/update.md @@ -119,7 +119,7 @@ The following arguments are available: | **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false. | | **-i, --interactive** | Boolean value for making the update command interactive. If true, the tool will prompt the user for input. Default is false. | | **-f,--format** | Output format of the manifest. Default is "yaml". | -| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. | +| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ | | **-?, --help** | Gets additional help on this command. | ## Submit diff --git a/pipelines/azure-pipelines.yml b/pipelines/azure-pipelines.yml index 435f195c..e1073bc2 100644 --- a/pipelines/azure-pipelines.yml +++ b/pipelines/azure-pipelines.yml @@ -134,7 +134,9 @@ extends: testSelector: "testAssemblies" testAssemblyVer2: 'src\WingetCreateTests\WingetCreateTests\bin\$(buildPlatform)\$(buildConfiguration)\$(targetFramework)\WingetCreateTests.dll' runSettingsFile: 'src\WingetCreateTests\WingetCreateTests\Test.runsettings' - overrideTestrunParameters: '-WingetPkgsTestRepoOwner microsoft -WingetPkgsTestRepo winget-pkgs-submission-test -GitHubAppPrivateKey "$(GitHubApp_PrivateKey)"' + overrideTestrunParameters: '-WingetPkgsTestRepoOwner microsoft -WingetPkgsTestRepo winget-pkgs-submission-test' + env: + WINGET_CREATE_APP_KEY: $(GitHubApp_PrivateKey) - task: 1ES.PublishPipelineArtifact@1 inputs: diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs index c7e6ac17..c6c2edb7 100644 --- a/src/WingetCreateCLI/Commands/BaseCommand.cs +++ b/src/WingetCreateCLI/Commands/BaseCommand.cs @@ -126,22 +126,19 @@ public async Task LoadGitHubClient(bool requireToken = false) if (string.IsNullOrEmpty(this.GitHubToken)) { Logger.Trace("No token parameter, reading cached token"); - this.GitHubToken = GitHubOAuth.ReadTokenCache(); - if (string.IsNullOrEmpty(this.GitHubToken)) + if (GitHubOAuth.TryReadTokenCache(out var token)) { - if (requireToken) - { - Logger.Trace("No token found in cache, launching OAuth flow"); - if (!await this.GetTokenFromOAuth()) - { - Logger.Trace("Failed to obtain token from OAuth flow."); - return false; - } - } + this.GitHubToken = token; + isCacheToken = true; } - else + else if (requireToken) { - isCacheToken = true; + Logger.Trace("No token found in cache, launching OAuth flow"); + if (!await this.GetTokenFromOAuth()) + { + Logger.Trace("Failed to obtain token from OAuth flow."); + return false; + } } } diff --git a/src/WingetCreateCLI/GitHubOAuth.cs b/src/WingetCreateCLI/GitHubOAuth.cs index 045807bd..cada66ca 100644 --- a/src/WingetCreateCLI/GitHubOAuth.cs +++ b/src/WingetCreateCLI/GitHubOAuth.cs @@ -5,12 +5,9 @@ namespace Microsoft.WingetCreateCLI { using System; using System.ComponentModel; - using System.IO; - using System.Security.Cryptography; - using System.Text; using System.Text.Json; using System.Text.Json.Serialization; - using System.Threading.Tasks; + using System.Threading.Tasks; using Microsoft.WingetCreateCLI.Logging; using Microsoft.WingetCreateCLI.Properties; using Microsoft.WingetCreateCLI.Telemetry; @@ -25,49 +22,27 @@ public static class GitHubOAuth { private const string GitHubDeviceEndpoint = "https://github.com/login/device/code"; private const string GitHubTokenEndpoint = "https://github.com/login/oauth/access_token"; - private const string GrantType = "urn:ietf:params:oauth:grant-type:device_code"; - private static readonly string TokenFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin"); - - /// - /// Create byte array for additional entropy when using Protect method. - /// - private static readonly byte[] EntropyBytes = Encoding.UTF8.GetBytes(TokenFile); - - /// - /// Deletes the cached token. - /// - public static void DeleteTokenCache() - { - File.Delete(TokenFile); - } - - /// - /// Reads and decrypts the cached token, if one exists. - /// - /// Decrypted cached token. - public static string ReadTokenCache() - { - if (File.Exists(TokenFile)) - { - var protectedBytes = File.ReadAllBytes(TokenFile); - var bytes = ProtectedData.Unprotect(protectedBytes, EntropyBytes, DataProtectionScope.CurrentUser); - return Encoding.UTF8.GetString(bytes); - } - else - { - return null; - } - } - - /// - /// Encrypts and writes the token to the file cache. - /// - /// Token to be cached. - public static void WriteTokenCache(string token) - { - var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(token), EntropyBytes, DataProtectionScope.CurrentUser); - File.WriteAllBytes(TokenFile, bytes); - } + private const string GrantType = "urn:ietf:params:oauth:grant-type:device_code"; + + /// + /// Deletes the token. + /// + /// True if the token was deleted, false otherwise. + public static bool DeleteTokenCache() => TokenHelper.Delete(); + + /// + /// Writes the token. + /// + /// Token to be cached. + /// True if the token was written, false otherwise. + public static bool WriteTokenCache(string token) => TokenHelper.Write(token); + + /// + /// Reads the token. + /// + /// Output token. + /// True if the token was read, false otherwise. + public static bool TryReadTokenCache(out string token) => TokenHelper.TryRead(out token); /// /// Sends a POST request to GitHub's authorization server to obtain a device code. @@ -236,5 +211,5 @@ public class TokenErrorResponse [JsonPropertyName("error_uri")] public string ErrorUri { get; set; } } - } -} + } +} diff --git a/src/WingetCreateCLI/NativeMethods.txt b/src/WingetCreateCLI/NativeMethods.txt new file mode 100644 index 00000000..3fbe5436 --- /dev/null +++ b/src/WingetCreateCLI/NativeMethods.txt @@ -0,0 +1,4 @@ +CredDelete +CredFree +CredRead +CredWrite diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs index 85f4d453..9cb7e5d5 100644 --- a/src/WingetCreateCLI/Program.cs +++ b/src/WingetCreateCLI/Program.cs @@ -64,6 +64,12 @@ private static async Task Main(string[] args) return args.Any() ? 1 : 0; } + // If the user has provided a token via the command line, warn them that it may be logged + if (!string.IsNullOrEmpty(command.GitHubToken)) + { + Logger.WarnLocalized(nameof(Resources.GitHubTokenWarning_Message)); + } + bool commandHandlesToken = command is not CacheCommand and not InfoCommand and not SettingsCommand; // Do not load github client for commands that do not deal with a GitHub token. diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index 5e7fb4b1..f4f99c6d 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -1150,7 +1150,9 @@ public static string GitHubLoginFail_Error { } /// - /// Looks up a localized string similar to GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.. + /// Looks up a localized string similar to GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. + /// + ///Warning: Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token.. /// public static string GitHubToken_HelpText { get { @@ -1158,6 +1160,15 @@ public static string GitHubToken_HelpText { } } + /// + /// Looks up a localized string similar to Warning: Using the --token argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token.. + /// + public static string GitHubTokenWarning_Message { + get { + return ResourceManager.GetString("GitHubTokenWarning_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to Windows Package Manager Manifest Creator v{0}. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index c1faa830..42b13aaa 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -237,7 +237,9 @@ GitHub login failed. - GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. + GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. + +Warning: Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token. Windows Package Manager Manifest Creator v{0} @@ -653,6 +655,9 @@ Token was invalid. Please generate a new GitHub token and try again. + + Warning: Using the --token argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token. + GitHub api rate limit exceeded. To extend your rate limit, provide your GitHub token with the '-t' flag or store one using the 'token --store' command. '-t' refers to a command line switch argument diff --git a/src/WingetCreateCLI/TokenHelper.cs b/src/WingetCreateCLI/TokenHelper.cs new file mode 100644 index 00000000..364feb25 --- /dev/null +++ b/src/WingetCreateCLI/TokenHelper.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI +{ + using System; + using System.IO; + using System.Runtime.InteropServices; + using System.Security.Cryptography; + using System.Text; + using Microsoft.WingetCreateCLI.Logging; + using Windows.Win32; + using Windows.Win32.Foundation; + using Windows.Win32.Security.Credentials; + + /// + /// Provides functionality for caching and retrieving the GitHub OAuth token. + /// + public static class TokenHelper + { + // Windows credentials manager + private const string CredTargetName = "winget-create:GitHub [repo]"; + private const string CredUserName = "Personal Access Token"; + + // Environment variable + private const string TokenEnvironmentVariable = "WINGET_CREATE_GITHUB_TOKEN"; + + // File (legacy cache) + private static readonly string TokenFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin"); + private static readonly byte[] EntropyBytes = Encoding.UTF8.GetBytes(TokenFile); + + /// + /// Deletes the token. + /// + /// True if the token was deleted, false otherwise. + public static bool Delete() + { + return PInvoke.CredDelete(CredTargetName, CRED_TYPE.CRED_TYPE_GENERIC); + } + + /// + /// Reads the token. + /// + /// Output token. + /// True if the token was read, false otherwise. + public static unsafe bool TryRead(out string token) => + TryReadFromEnvironmentVariable(out token) || + TryReadFromCredentialManager(out token) || + TryReadFromFileAndMigrate(out token); + + /// + /// Writes the token. + /// + /// Token to be cached. + /// True if the token was written, false otherwise. + public static unsafe bool Write(string token) + { + var tokenBytes = Encoding.Unicode.GetBytes(token); + fixed (byte* tokenBytesPtr = tokenBytes) + { + var credTargetNamePtr = Marshal.StringToHGlobalUni(CredTargetName); + var credUserNamePtr = Marshal.StringToHGlobalUni(CredUserName); + + try + { + var credential = new CREDENTIALW + { + Type = CRED_TYPE.CRED_TYPE_GENERIC, + TargetName = new PWSTR(credTargetNamePtr), + UserName = new PWSTR(credUserNamePtr), + CredentialBlobSize = (uint)tokenBytes.Length, + CredentialBlob = tokenBytesPtr, + Persist = CRED_PERSIST.CRED_PERSIST_LOCAL_MACHINE, + }; + + return PInvoke.CredWrite(&credential, /* None */ 0); + } + finally + { + Marshal.FreeHGlobal(credTargetNamePtr); + Marshal.FreeHGlobal(credUserNamePtr); + } + } + } + + /// + /// Tries to read the token from the Windows credentials manager. + /// + /// Output token. + /// True if the token was read, false otherwise. + private static unsafe bool TryReadFromCredentialManager(out string token) + { + if (PInvoke.CredRead(CredTargetName, CRED_TYPE.CRED_TYPE_GENERIC, out CREDENTIALW* credentialObject) && credentialObject != null) + { + try + { + var accessTokenInBytes = new byte[credentialObject->CredentialBlobSize]; + Marshal.Copy((IntPtr)credentialObject->CredentialBlob, accessTokenInBytes, 0, accessTokenInBytes.Length); + token = Encoding.Unicode.GetString(accessTokenInBytes); + return true; + } + finally + { + PInvoke.CredFree(credentialObject); + } + } + + token = null; + return false; + } + + /// + /// Tries to read the token from the environment variable. + /// + /// Output token. + /// True if the token was read, false otherwise. + private static bool TryReadFromEnvironmentVariable(out string token) + { + var envToken = Environment.GetEnvironmentVariable(TokenEnvironmentVariable); + if (!string.IsNullOrEmpty(envToken)) + { + token = envToken; + return true; + } + + token = null; + return false; + } + + /// + /// Tries to read the token from the file and migrate it to the Windows credentials manager. + /// + /// Output token. + /// True if the token was read, false otherwise. + private static bool TryReadFromFileAndMigrate(out string token) + { + try + { + if (TryReadFromFile(out token)) + { + // Migrate to Windows credentials manager + Write(token); + return true; + } + } + finally + { + CleanUpLegacyTokenFile(); + } + + token = null; + return false; + } + + /// + /// Tries to read the token from the file. + /// + /// Output token. + /// True if the token was read, false otherwise. + private static bool TryReadFromFile(out string token) + { + if (File.Exists(TokenFile)) + { + try + { + var protectedBytes = File.ReadAllBytes(TokenFile); + var bytes = ProtectedData.Unprotect(protectedBytes, EntropyBytes, DataProtectionScope.CurrentUser); + token = Encoding.UTF8.GetString(bytes); + return true; + } + catch (Exception e) + { + Logger.Trace($"Failed to read token from file. Message: {e.Message}"); + } + } + + token = null; + return false; + } + + /// + /// Cleans up the legacy token file. + /// + private static void CleanUpLegacyTokenFile() + { + if (File.Exists(TokenFile)) + { + try + { + File.Delete(TokenFile); + } + catch (Exception e) + { + Logger.Trace($"Failed to delete token file. Message: {e.Message}"); + } + } + } + } +} diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj index ada97f5d..ed20a01c 100644 --- a/src/WingetCreateCLI/WingetCreateCLI.csproj +++ b/src/WingetCreateCLI/WingetCreateCLI.csproj @@ -21,6 +21,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/WingetCreateTests/WingetCreateTests/Test.runsettings b/src/WingetCreateTests/WingetCreateTests/Test.runsettings index ae727b6d..ec18a8cf 100644 --- a/src/WingetCreateTests/WingetCreateTests/Test.runsettings +++ b/src/WingetCreateTests/WingetCreateTests/Test.runsettings @@ -5,16 +5,10 @@ Parameters used by tests at run time. WingetPkgsTestRepoOwner: The repository owner of the winget-pkgs submission test repo. (Repo owner must be forked from main "winget-pkgs-submission-test" repo) WingetPkgsTestRepo: The winget-pkgs test repository. - GitHubApiKey: GitHub access token for testing. - GitHubAppPrivateKey: GitHub app installation access token for the winget-create app. --> - - - - diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs index 7f153ecf..af320f41 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs @@ -4,11 +4,13 @@ namespace Microsoft.WingetCreateUnitTests { using System; - using System.Threading.Tasks; - using Microsoft.WingetCreateCLI.Commands; + using System.Threading.Tasks; + using Microsoft.WingetCreateCLI; + using Microsoft.WingetCreateCLI.Commands; using Microsoft.WingetCreateCore.Common; - using NUnit.Framework; - + using NUnit.Framework; + using NUnit.Framework.Legacy; + /// /// Base class for unit tests which need to interact with GitHub. /// @@ -41,6 +43,10 @@ public class GitHubTestsBase [OneTimeSetUp] public async Task SetupBase() { + // Ensure keys are not set in runsettings file + ClassicAssert.True(string.IsNullOrEmpty(TestContext.Parameters.Get("GitHubApiKey")), "GitHubApiKey should not be set in runsettings file"); + ClassicAssert.True(string.IsNullOrEmpty(TestContext.Parameters.Get("GitHubAppPrivateKey")), "GitHubAppPrivateKey should not be set in runsettings file"); + this.WingetPkgsTestRepoOwner = TestContext.Parameters.Get("WingetPkgsTestRepoOwner") ?? throw new ArgumentNullException("WingetPkgsTestRepoOwner must be set in runsettings file"); this.WingetPkgsTestRepo = TestContext.Parameters.Get("WingetPkgsTestRepo") ?? throw new ArgumentNullException("WingetPkgsTestRepo must be set in runsettings file"); @@ -48,26 +54,46 @@ public async Task SetupBase() { throw new ArgumentException($"Invalid configuration specified, you can not run tests against default repo {BaseCommand.DefaultWingetRepoOwner}/{BaseCommand.DefaultWingetRepo}"); } - - string gitHubApiKey = TestContext.Parameters.Get("GitHubApiKey"); - string gitHubAppPrivateKey = TestContext.Parameters.Get("GitHubAppPrivateKey"); - - if (!string.IsNullOrEmpty(gitHubApiKey)) - { - TestContext.Progress.WriteLine("Using GitHubApiKey value for tests"); - this.GitHubApiKey = gitHubApiKey; - this.SubmitPRToFork = true; - } - else if (!string.IsNullOrEmpty(gitHubAppPrivateKey)) - { - TestContext.Progress.WriteLine("Using GitHubAppPrivateKey value for tests"); - this.GitHubApiKey = await GitHub.GetGitHubAppInstallationAccessToken(gitHubAppPrivateKey, Constants.GitHubAppId, this.WingetPkgsTestRepoOwner, this.WingetPkgsTestRepo); - this.SubmitPRToFork = false; + + string gitHubAppPrivateKey = Environment.GetEnvironmentVariable("WINGET_CREATE_APP_KEY"); + if (string.IsNullOrEmpty(gitHubAppPrivateKey)) + { + await this.ConfigureForLocalTestsAsync(); } else - { - throw new ArgumentNullException("Either GitHubApiKey or GitHubAppPrivateKey must be set in runsettings file"); + { + await this.ConfigureForPipelineTestsAsync(gitHubAppPrivateKey); } + } + + /// + /// Configures the tests to run in a pipeline. + /// + /// Github app private key. + private async Task ConfigureForPipelineTestsAsync(string gitHubAppPrivateKey) + { + TestContext.Progress.WriteLine("Running in pipeline, using GitHubAppPrivateKey value for tests"); + this.GitHubApiKey = await GitHub.GetGitHubAppInstallationAccessToken(gitHubAppPrivateKey, Constants.GitHubAppId, this.WingetPkgsTestRepoOwner, this.WingetPkgsTestRepo); + this.SubmitPRToFork = false; + } + + /// + /// Configures the tests to run locally. + /// + private async Task ConfigureForLocalTestsAsync() + { + TestContext.Progress.WriteLine("Running locally, using Github token for tests"); + if (TokenHelper.TryRead(out string token)) + { + this.GitHubApiKey = token; + this.SubmitPRToFork = true; + } + else + { + ClassicAssert.Fail("No GitHub token found.\n" + + ">> Please run 'wingetcreate token -s'\n" + + ">> Or set the 'WINGET_CREATE_GITHUB_TOKEN' environment variable to a valid GitHub token."); + } } - } + } } diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs index 5cc8a0a7..9193556b 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. namespace Microsoft.WingetCreateUnitTests -{ - using System.IO; +{ + using System; using System.Threading.Tasks; using Microsoft.WingetCreateCLI; using Microsoft.WingetCreateCLI.Commands; @@ -15,29 +15,89 @@ namespace Microsoft.WingetCreateUnitTests /// Test cases for verifying that the "token" command is working as expected. /// public class TokenCommandTests : GitHubTestsBase - { - /// - /// Verifies that the Token command works as expected. - /// - /// A representing the asynchronous unit test. - [Test] - public async Task TokenCommand() - { - Logger.Initialize(); - - // Preemptively clear existing token - var command = new TokenCommand { Clear = true }; - ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded"); - - string tokenCacheFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin"); - FileAssert.DoesNotExist(tokenCacheFile, "Token cache file shouldn't exist before running Token --store command"); - command = new TokenCommand { Store = true, GitHubToken = this.GitHubApiKey }; - ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded"); - FileAssert.Exists(tokenCacheFile, "Token cache file should exist after storing token"); - - command = new TokenCommand { Clear = true }; - ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded"); - FileAssert.DoesNotExist(tokenCacheFile, "Token cache file shouldn't exist after running Token --clear command"); - } + { + private const string TokenEnvironmentVariable = "WINGET_CREATE_GITHUB_TOKEN"; + + /// + /// OneTimeSetup method for the unit tests. + /// + [OneTimeSetUp] + public void OneTimeSetUp() + { + Logger.Initialize(); + } + + /// + /// SetUp method for the unit tests. + /// + [SetUp] + public void SetUp() + { + TokenHelper.Delete(); + Environment.SetEnvironmentVariable(TokenEnvironmentVariable, null); + } + + /// + /// TearDown method for the unit tests. + /// + [TearDown] + public void TearDown() + { + TokenHelper.Delete(); + Environment.SetEnvironmentVariable(TokenEnvironmentVariable, null); + } + + /// + /// Test case for verifying that the "token" can be read from the environment variable after clearing the token cache. + /// + [Test] + public async Task TokenClearAndReadFromEnvironmentVariable() + { + await this.ExecuteTokenClearCommand(); + ClassicAssert.IsFalse(TokenHelper.TryRead(out var _), "Token cache shouldn't exist"); + + Environment.SetEnvironmentVariable(TokenEnvironmentVariable, "MockToken"); + ClassicAssert.IsTrue(TokenHelper.TryRead(out var _), "Token cache should exist after setting environment variable"); + } + + /// + /// Test case for verifying that the "token --clear" command is working as expected. + /// + [Test] + public async Task TokenClearCommand() + { + await this.ExecuteTokenStoreCommand(); + await this.ExecuteTokenClearCommand(); + ClassicAssert.IsFalse(TokenHelper.TryRead(out var _), "Token cache shouldn't exist after running Token --clear command"); + } + + /// + /// Test case for verifying that the "token --store" command is working as expected. + /// + [Test] + public async Task TokenStoreCommand() + { + await this.ExecuteTokenClearCommand(); + await this.ExecuteTokenStoreCommand(); + ClassicAssert.IsTrue(TokenHelper.TryRead(out var _), "Token cache should exist after running Token --store command"); + } + + /// + /// Executes the token clear command. + /// + private async Task ExecuteTokenClearCommand() + { + var command = new TokenCommand { Clear = true }; + ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded"); + } + + /// + /// Executes the token store command. + /// + private async Task ExecuteTokenStoreCommand() + { + var command = new TokenCommand { Store = true, GitHubToken = this.GitHubApiKey }; + ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded"); + } } }