diff --git a/src/chocolatey.tests/chocolatey.tests.csproj b/src/chocolatey.tests/chocolatey.tests.csproj index 31363aa7e..0d0809988 100644 --- a/src/chocolatey.tests/chocolatey.tests.csproj +++ b/src/chocolatey.tests/chocolatey.tests.csproj @@ -153,6 +153,7 @@ + diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyFeatureCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyFeatureCommandSpecs.cs index 036a96e23..bc8eb7221 100644 --- a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyFeatureCommandSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyFeatureCommandSpecs.cs @@ -125,7 +125,7 @@ public void Should_use_the_first_unparsed_arg_as_the_subcommand() public void Should_throw_when_more_than_one_unparsed_arg_is_passed() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _unparsedArgs.Add("bbq"); var errored = false; Exception error = null; @@ -170,7 +170,7 @@ public void Should_accept_disable_as_the_subcommand() public void Should_set_unrecognized_values_to_list_as_the_subcommand() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _because(); Configuration.FeatureCommand.Command.Should().Be(FeatureCommandType.List); diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyLicenseCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyLicenseCommandSpecs.cs new file mode 100644 index 000000000..4ea0160b7 --- /dev/null +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyLicenseCommandSpecs.cs @@ -0,0 +1,265 @@ +// Copyright © 2017 - 2025 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using chocolatey.infrastructure.app.attributes; +using chocolatey.infrastructure.app.commands; +using chocolatey.infrastructure.app.configuration; +using chocolatey.infrastructure.app.domain; +using FluentAssertions; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace chocolatey.tests.infrastructure.app.commands +{ + public class ChocolateyLicenseCommandSpecs + { + [ConcernFor("license")] + public abstract class ChocolateyLicenseCommandSpecsBase : TinySpec + { + protected ChocolateyLicenseCommand Command; + protected ChocolateyConfiguration Configuration = new ChocolateyConfiguration(); + + public override void Context() + { + Command = new ChocolateyLicenseCommand(); + } + } + + public class When_Implementing_Command_For : ChocolateyLicenseCommandSpecsBase + { + private List _results; + + public override void Because() + { + _results = Command.GetType().GetCustomAttributes().ToList(); + } + + [Fact] + public void Should_Have_Expected_Number_Of_Commands() + { + _results.Should().HaveCount(1); + } + + [InlineData("license")] + public void Should_Implement_Expected_Command(string name) + { + _results.Should().ContainSingle(r => r.CommandName == name); + } + + [Fact] + public void Should_Specify_Expected_Version_For_All_Commands() + { + _results.Should().AllSatisfy(r => r.Version.Should().Be("2.5.0")); + } + } + + public class When_parsing_additional_arguments_ : ChocolateyLicenseCommandSpecsBase + { + private readonly IList _unparsedArgs = new List(); + private Action _because; + + public override void Because() + { + _because = () => Command.ParseAdditionalArguments(_unparsedArgs, Configuration); + } + + public new void Reset() + { + _unparsedArgs.Clear(); + } + + [Fact] + public void Should_use_the_first_unparsed_arg_as_the_subcommand() + { + Reset(); + _unparsedArgs.Add("info"); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + + [Fact] + public void Should_throw_when_more_than_one_unparsed_arg_is_passed() + { + Reset(); + _unparsedArgs.Add("abc"); + _unparsedArgs.Add("bbq"); + var errored = false; + Exception error = null; + + try + { + _because(); + } + catch (Exception ex) + { + errored = true; + error = ex; + } + + errored.Should().BeTrue(); + error.Should().NotBeNull(); + error.Should().BeOfType(); + error.Message.Should().Contain("A single license command must be listed"); + } + + [Fact] + public void Should_accept_info_as_the_subcommand() + { + Reset(); + _unparsedArgs.Add("info"); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + + [Fact] + public void Should_accept_uppercase_info_as_the_subcommand() + { + Reset(); + _unparsedArgs.Add("INFO"); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + + [Fact] + public void Should_set_unrecognized_values_to_info_as_the_subcommand() + { + Reset(); + _unparsedArgs.Add("abc"); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + + [Fact] + public void Should_default_to_list_as_the_subcommand() + { + Reset(); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + + [Fact] + public void Should_handle_passing_in_an_empty_string() + { + Reset(); + _unparsedArgs.Add(" "); + _because(); + + Configuration.LicenseCommand.Command.Should().Be(LicenseCommandType.Info); + } + } + + public class When_Help_Is_Called : ChocolateyLicenseCommandSpecsBase + { + public override void Because() + { + Command.HelpMessage(Configuration); + } + + [Fact] + public void Should_log_a_message() + { + MockLogger.Verify(l => l.Info(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void Should_log_the_message_we_expect() + { + var messages = MockLogger.MessagesFor(LogLevel.Info); + messages.Should().HaveCount(19); + messages[0].Should().Contain("License Command"); + messages[2].Should().Contain("Show information about the current Chocolatey CLI license."); + } + } + + public class When_DryRun_Is_Called : ChocolateyLicenseCommandSpecsBase + { + public override void Because() + { + Configuration.LicenseCommand.Command = LicenseCommandType.Info; + Command.DryRun(Configuration); + } + + [Fact] + public void Should_log_a_message() + { + MockLogger.Verify(l => l.Warn(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void Should_log_the_message_we_expect() + { + var messages = MockLogger.MessagesFor(LogLevel.Warn); + messages.Should().ContainSingle(); + messages[0].Should().Contain("No Chocolatey license found."); + } + } + + public class When_Run_Is_Called : ChocolateyLicenseCommandSpecsBase + { + public override void Because() + { + Configuration.LicenseCommand.Command = LicenseCommandType.Info; + Command.Run(Configuration); + } + + [Fact] + public void Should_log_a_message() + { + MockLogger.Verify(l => l.Warn(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void Should_log_the_message_we_expect() + { + var messages = MockLogger.MessagesFor(LogLevel.Warn); + messages.Should().ContainSingle(); + messages[0].Should().Contain("No Chocolatey license found."); + } + } + + public class When_Run_Is_Called_With_Limit_Output : ChocolateyLicenseCommandSpecsBase + { + public override void Because() + { + Configuration.LicenseCommand.Command = LicenseCommandType.Info; + Configuration.RegularOutput = false; + Command.Run(Configuration); + } + + [Fact] + public void Should_log_a_message() + { + MockLogger.Verify(l => l.Warn(It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public void Should_log_the_message_we_expect() + { + var messages = MockLogger.MessagesFor(LogLevel.Warn); + messages.Should().ContainSingle(); + messages[0].Should().Contain("No Chocolatey license found."); + } + } + } +} \ No newline at end of file diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyPinCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyPinCommandSpecs.cs index 799714442..01613fb99 100644 --- a/src/chocolatey.tests/infrastructure.app/commands/ChocolateyPinCommandSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateyPinCommandSpecs.cs @@ -181,7 +181,7 @@ public void Should_use_the_first_unparsed_arg_as_the_subcommand() public void Should_throw_when_more_than_one_unparsed_arg_is_passed() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _unparsedArgs.Add("bbq"); var errored = false; Exception error = null; @@ -236,7 +236,7 @@ public void Should_remove_add_as_the_subcommand() public void Should_set_unrecognized_values_to_list_as_the_subcommand() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _because(); Configuration.PinCommand.Command.Should().Be(PinCommandType.List); diff --git a/src/chocolatey.tests/infrastructure.app/commands/ChocolateySourceCommandSpecs.cs b/src/chocolatey.tests/infrastructure.app/commands/ChocolateySourceCommandSpecs.cs index c117ed73c..68f3331b2 100644 --- a/src/chocolatey.tests/infrastructure.app/commands/ChocolateySourceCommandSpecs.cs +++ b/src/chocolatey.tests/infrastructure.app/commands/ChocolateySourceCommandSpecs.cs @@ -167,7 +167,7 @@ public void Should_use_the_first_unparsed_arg_as_the_subcommand() public void Should_throw_when_more_than_one_unparsed_arg_is_passed() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _unparsedArgs.Add("bbq"); var errored = false; Exception error = null; @@ -242,7 +242,7 @@ public void Should_accept_disable_as_the_subcommand() public void Should_set_unrecognized_values_to_list_as_the_subcommand() { Reset(); - _unparsedArgs.Add("wtf"); + _unparsedArgs.Add("abc"); _because(); Configuration.SourceCommand.Command.Should().Be(SourceCommandType.List); diff --git a/src/chocolatey/chocolatey.csproj b/src/chocolatey/chocolatey.csproj index c4a16d884..53a7b4359 100644 --- a/src/chocolatey/chocolatey.csproj +++ b/src/chocolatey/chocolatey.csproj @@ -178,6 +178,7 @@ + @@ -186,6 +187,7 @@ + diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyLicenseCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyLicenseCommand.cs new file mode 100644 index 000000000..286417f1b --- /dev/null +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyLicenseCommand.cs @@ -0,0 +1,209 @@ +// Copyright © 2017 - 2025 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using chocolatey.infrastructure.app.attributes; +using chocolatey.infrastructure.app.configuration; +using chocolatey.infrastructure.app.domain; +using chocolatey.infrastructure.commandline; +using chocolatey.infrastructure.commands; +using chocolatey.infrastructure.licensing; +using chocolatey.infrastructure.logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace chocolatey.infrastructure.app.commands +{ + [CommandFor("license", "display Chocolatey license information", Version = "2.5.0")] + public class ChocolateyLicenseCommand : ChocolateyCommandBase, ICommand + { + private static readonly Regex _licenseCountRegex = new Regex(@"\[.*?(?\d+).*?\]"); + + public void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfiguration configuration) + { + // We don't currently expect to have any arguments + } + + public void ParseAdditionalArguments(IList unparsedArguments, ChocolateyConfiguration configuration) + { + if (unparsedArguments.Count > 1) + { + throw new ApplicationException("A single license command must be listed. Please see the help menu for those commands"); + } + + var command = LicenseCommandType.Unknown; + var unparsedCommand = unparsedArguments.DefaultIfEmpty(string.Empty).FirstOrDefault(); + Enum.TryParse(unparsedCommand, true, out command); + + if (command == LicenseCommandType.Unknown) + { + if (!string.IsNullOrWhiteSpace(unparsedCommand)) + { + this.Log().Warn("Unknown command {0}. Setting to info.".FormatWith(unparsedCommand)); + } + + command = LicenseCommandType.Info; + } + + configuration.LicenseCommand.Command = command; + } + + public void Validate(ChocolateyConfiguration configuration) + { + // We don't currently accept any arguments, so there is no validation + } + + public bool MayRequireAdminAccess() + { + return false; + } + + public void DryRun(ChocolateyConfiguration configuration) + { + Run(configuration); + } + + public void Run(ChocolateyConfiguration config) + { + switch (config.LicenseCommand.Command) + { + case LicenseCommandType.Info: + GetLicense(config); + break; + } + } + + private void GetLicense(ChocolateyConfiguration config) + { + var ourLicense = LicenseValidation.Validate(); + var logger = config.RegularOutput ? ChocolateyLoggers.Normal : ChocolateyLoggers.LogFileOnly; + + if (ourLicense.LicenseType == ChocolateyLicenseType.Foss || ourLicense.LicenseType == ChocolateyLicenseType.Unknown) + { + this.Log().Warn(logger, "No Chocolatey license found."); + return; + } + + if (!ourLicense.IsValid) + { + this.Log().Warn(logger, "Invalid Chocolatey {0} license found: {1}", ourLicense.LicenseType, ourLicense.InvalidReason); + Environment.ExitCode = 1; + return; + } + + var nodeCount = ParseLicenseStringForLicenseCount(ourLicense.Name); + + if (config.RegularOutput) + { + this.Log().Info("Registered to: {0}".FormatWith(ourLicense.Name)); + this.Log().Info("Expiration Date: {0}".FormatWith(ourLicense.ExpirationDate?.ToString("dd MMMM yyyy"))); + this.Log().Info("License type: {0}".FormatWith(ourLicense.LicenseType)); + this.Log().Info("Node Count: {0}".FormatWith(nodeCount)); + } + else + { + // Headers: Name, LicenseType, ExpirationDate, NodeCount + this.Log().Info("{0}|{1}|{2}|{3}".FormatWith(ourLicense.Name, ourLicense.LicenseType, ourLicense.ExpirationDate?.ToString("yyyy-MM-dd"), nodeCount)); + } + } + + /// + /// Parse the license count from the Name property of a Chocolatey license file. + /// + /// The Name property of a Chocolatey license file + /// that should be parsed of the license count. + /// + /// + /// There are no tests for this method, but that is due to the fact that this + /// method is extracted from the Chocolatey Central Management + /// codebase, and it has _several_ tests for it, so it felt that these do not + /// need to be duplicated here. + /// + /// The license count. + private int ParseLicenseStringForLicenseCount(string licenseName) + { + if (string.IsNullOrEmpty(licenseName)) + { + return 0; + } + + // Use Regex to find the licensed machine count + var match = _licenseCountRegex.Match(licenseName); + + if (match.Success && int.TryParse(match.Groups["licensedMachineCount"].Value, out var licenseCount)) + { + return licenseCount; + } + + return 0; + } + + protected override string GetCommandDescription(CommandForAttribute attribute, ChocolateyConfiguration configuration) + { + return @"Show information about the current Chocolatey CLI license."; + } + + protected override IEnumerable GetCommandExamples(CommandForAttribute[] attributes, ChocolateyConfiguration configuration) + { + return new[] + { + "choco license", + "choco license info" + }; + } + + protected override IEnumerable GetCommandUsage(CommandForAttribute[] attributes, ChocolateyConfiguration configuration) + { + return new[] + { + "choco license [info] []", + }; + } + +#pragma warning disable IDE0022, IDE1006 + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void configure_argument_parser(OptionSet optionSet, ChocolateyConfiguration configuration) + => ConfigureArgumentParser(optionSet, configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void handle_additional_argument_parsing(IList unparsedArguments, ChocolateyConfiguration configuration) + => ParseAdditionalArguments(unparsedArguments, configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void handle_validation(ChocolateyConfiguration configuration) + => Validate(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void help_message(ChocolateyConfiguration configuration) + => HelpMessage(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual bool may_require_admin_access() + => MayRequireAdminAccess(); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void noop(ChocolateyConfiguration configuration) + => DryRun(configuration); + + [Obsolete("This overload is deprecated and will be removed in v3.")] + public virtual void run(ChocolateyConfiguration configuration) + => Run(configuration); + +#pragma warning restore IDE0022, IDE1006 + } +} diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 975e1c5a0..f855a0246 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -53,6 +53,7 @@ public ChocolateyConfiguration() PackCommand = new PackCommandConfiguration(); PushCommand = new PushCommandConfiguration(); PinCommand = new PinCommandConfiguration(); + LicenseCommand = new LicenseCommandConfiguration(); OutdatedCommand = new OutdatedCommandConfiguration(); Proxy = new ProxyConfiguration(); ExportCommand = new ExportCommandConfiguration(); @@ -471,6 +472,14 @@ private void AppendOutput(StringBuilder propertyValues, string append) /// public PinCommandConfiguration PinCommand { get; set; } + /// + /// Configuration related specifically to License command + /// + /// + /// On .NET 4.0, get error CS0200 when private set - see http://stackoverflow.com/a/23809226/18475 + /// + public LicenseCommandConfiguration LicenseCommand { get; set; } + /// /// Configuration related specifically to Outdated command /// @@ -685,6 +694,12 @@ public sealed class PinCommandConfiguration public PinCommandType Command { get; set; } } + [Serializable] + public sealed class LicenseCommandConfiguration + { + public LicenseCommandType Command { get; set; } + } + [Serializable] public sealed class OutdatedCommandConfiguration { diff --git a/src/chocolatey/infrastructure.app/domain/LicenseCommandType.cs b/src/chocolatey/infrastructure.app/domain/LicenseCommandType.cs new file mode 100644 index 000000000..6fd71e207 --- /dev/null +++ b/src/chocolatey/infrastructure.app/domain/LicenseCommandType.cs @@ -0,0 +1,24 @@ +// Copyright © 2017 - 2025 Chocolatey Software, Inc +// Copyright © 2011 - 2017 RealDimensions Software, LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace chocolatey.infrastructure.app.domain +{ + public enum LicenseCommandType + { + Unknown, + Info + } +} diff --git a/tests/pester-tests/commands/choco-license.Tests.ps1 b/tests/pester-tests/commands/choco-license.Tests.ps1 new file mode 100644 index 000000000..65e1b4bac --- /dev/null +++ b/tests/pester-tests/commands/choco-license.Tests.ps1 @@ -0,0 +1,57 @@ +Import-Module helpers/common-helpers + +Describe "choco license" -Tag Chocolatey, LicenseCommand { + BeforeDiscovery { + $HasLicensedExtension = Test-PackageIsEqualOrHigher -PackageName 'chocolatey.extension' -Version '6.0.0' + } + + BeforeAll { + Remove-NuGetPaths + Initialize-ChocolateyTestInstall + New-ChocolateyInstallSnapshot + } + + AfterAll { + Remove-ChocolateyTestInstall + } + + Context "License (<_>)" -ForEach @("", "info", "bob") -Skip:($HasLicensedExtension) { + BeforeAll { + $Output = Invoke-Choco license $_ + } + + It "Exits successfully (0)" { + $Output.ExitCode | Should -Be 0 -Because $Output.String + } + + It "Reports available license" { + $ExpectedOutput = if($HasLicensedExtension) { + "Registered to:" + } else { + "No Chocolatey license found." + } + $Output.Lines | Should -Contain $ExpectedOutput -Because $Output.String + } + } + + Context "Help Documentation (<_>) - OSS" -ForEach @("--help", "-?", "-help") { + BeforeAll { + $Output = Invoke-Choco license $_ + } + + It "Exits successfully (0)" { + $Output.ExitCode | Should -Be 0 -Because $Output.String + } + + It "Outputs Help for License" { + $Output.String | Should -Match "License Command" -Because $Output.String + } + + It "Outputs help documentation for license command" { + $Output.Lines | Should -Contain "Show information about the current Chocolatey CLI license." -Because $Output.String + } + } + + # This needs to be the last test in this block, to ensure NuGet configurations aren't being created. + Test-NuGetPaths +} \ No newline at end of file diff --git a/tests/pester-tests/commands/choco-support.Tests.ps1 b/tests/pester-tests/commands/choco-support.Tests.ps1 index 206af9b57..12ac9cd89 100644 --- a/tests/pester-tests/commands/choco-support.Tests.ps1 +++ b/tests/pester-tests/commands/choco-support.Tests.ps1 @@ -51,7 +51,7 @@ Describe "choco support" -Tag Chocolatey, SupportCommand { $ExpectedOutput = if($HasLicensedExtension) { "As a licensed customer, you can reach out to" } else { - "As an open-source user of Chocolatey CLI, we are not able to" + "As a user of Chocolatey CLI open-source, we are unable to" } $Output.Lines | Should -Contain $ExpectedOutput -Because $Output.String }