diff --git a/.github/package_version.env b/.github/package_version.env index d250209..cc2c009 100644 --- a/.github/package_version.env +++ b/.github/package_version.env @@ -1,4 +1,4 @@ PI_MAJOR_VERSION=7 -PI_MINOR_VERSION=0 -PI_BUILD_VERSION=0 +PI_MINOR_VERSION=1 +PI_BUILD_VERSION=1 PI_DEPENDENCIES_SUFFIX=-* diff --git a/.github/workflows/build-test-push.yml b/.github/workflows/build-test-push.yml index 1f49753..f36b370 100644 --- a/.github/workflows/build-test-push.yml +++ b/.github/workflows/build-test-push.yml @@ -21,10 +21,10 @@ on: description: 'The package deployment suffix' required: true options: - - preview + - er - rc - release - default: preview + default: er registry: type: choice description: 'The package registry' @@ -59,9 +59,9 @@ jobs: continue-on-error: false # Build Number - # Establish a unique build number for current run YYMMDD{RUN_NUMBER}{RUN_ATTEMPT} e.g. 2201032602 + # Establish a unique build number for current run YYMMDD{RUN_NUMBER}{RUN_ATTEMPT} - name: Build Number - run: echo "PI_BUILD_NUMBER=$(date +'%-d%-m%y')${{ github.run_number }}" >> $GITHUB_ENV + run: echo "PI_BUILD_NUMBER=$(date +'%y%m%d')${{ github.run_number }}${{ github.run_attempt }}" >> $GITHUB_ENV continue-on-error: false # Setup preview package versions based on trigger @@ -71,11 +71,11 @@ jobs: run: echo "PI_CI_PACKAGE_VERSION=${{ env.PI_MAJOR_VERSION }}.${{ env.PI_MINOR_VERSION }}.${{ env.PI_BUILD_VERSION }}-rc.${{ env.PI_BUILD_NUMBER }}" >> $GITHUB_ENV continue-on-error: false - # Setup preview package versions - # Format: .2.3.6-preview.2201032602 + # Setup engineering package versions + # Format: .2.3.6-er.2201032602 - name: Preview metadata - if: ${{ github.event.inputs.suffix == 'preview' }} - run: echo "PI_CI_PACKAGE_VERSION=${{ env.PI_MAJOR_VERSION }}.${{ env.PI_MINOR_VERSION }}.${{ env.PI_BUILD_VERSION }}-preview.${{ env.PI_BUILD_NUMBER }}" >> $GITHUB_ENV + if: ${{ github.event.inputs.suffix == 'er' }} + run: echo "PI_CI_PACKAGE_VERSION=${{ env.PI_MAJOR_VERSION }}.${{ env.PI_MINOR_VERSION }}.${{ env.PI_BUILD_VERSION }}-er.${{ env.PI_BUILD_NUMBER }}" >> $GITHUB_ENV continue-on-error: false # Setup rc package versions diff --git a/NOTICE.md b/NOTICE.md index 3af4aff..91e97cd 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,22 +1,22 @@ # NOTICE -_Last updated: April 4, 2025_ +_Last updated: April 21, 2026_ -This project utilizes the following open-source frameworks, tools, and libraries: +This project uses the following open-source frameworks, tools, and libraries: -> **Note:** Only direct dependencies are listed below. Transitive dependencies of NuGet packages are not explicitly included. +> **Note:** Only direct dependencies are listed. Transitive NuGet dependencies are not included. -We extend our sincere thanks to the developers and maintainers of these open-source components for their valuable contributions to the community. +We thank the developers and maintainers of these components for their contributions. ## Frameworks -- .NET Framework 4.8 - .NET Standard 2.0 - .NET Standard 2.1 -- .NET 8 and later +- .NET Framework 4.6.1+ +- .NET 8+ ## Libraries -- [xUnit](https://xunit.net/) — Licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) -- [coverlet.collector](https://www.nuget.org/packages/coverlet.collector) — Licensed under the [MIT License](https://opensource.org/licenses/MIT) -- [FluentAssertions v7](https://www.nuget.org/packages/FluentAssertions) — Licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) -- [Report Generator](https://github.com/danielpalme/ReportGenerator) — Licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) -- [Moq](https://github.com/moq/moq4) — Licensed under the [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause) +- [xUnit](https://github.com/xunit/xunit) — [License](https://github.com/xunit/xunit/blob/main/LICENSE) +- [coverlet.collector](https://github.com/coverlet-coverage/coverlet) — [License](https://github.com/coverlet-coverage/coverlet/blob/master/LICENSE) +- [FluentAssertions](https://github.com/fluentassertions/fluentassertions) — [License](https://github.com/fluentassertions/fluentassertions/blob/master/LICENSE) +- [ReportGenerator](https://github.com/danielpalme/ReportGenerator) — [License](https://github.com/danielpalme/ReportGenerator/blob/main/LICENSE.txt) +- [DocFX](https://github.com/dotnet/docfx) — [License](https://github.com/dotnet/docfx/blob/main/LICENSE) \ No newline at end of file diff --git a/Shared.All.Solution.sln b/Shared.All.Solution.sln deleted file mode 100644 index f554b5f..0000000 --- a/Shared.All.Solution.sln +++ /dev/null @@ -1,57 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.31911.260 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FFF09815-02BA-4EC1-A346-AADA12CBF160}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2F984849-BEBC-4AC7-81A4-5B65781E22C4}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{99A64035-E6B3-46D1-989E-71A804DB1048}" - ProjectSection(SolutionItems) = preProject - global.json = global.json - LICENSE = LICENSE - HISTORY.md = HISTORY.md - NOTICE.md = NOTICE.md - build\props\Package.props = build\props\Package.props - .github\package_version.env = .github\package_version.env - build\props\Test.props = build\props\Test.props - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneImlx.Shared", "src\OneImlx.Shared\OneImlx.Shared.csproj", "{DD837075-2818-4341-AD60-33A098E97DB2}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneImlx.Test", "src\OneImlx.Test\OneImlx.Test.csproj", "{673016C3-3163-4F09-90EA-4FDCF13AB1BA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OneImlx.Shared.Tests", "test\OneImlx.Shared.Tests\OneImlx.Shared.Tests.csproj", "{771D2993-59C1-4F37-B28B-BE6C96294ADE}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {DD837075-2818-4341-AD60-33A098E97DB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DD837075-2818-4341-AD60-33A098E97DB2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DD837075-2818-4341-AD60-33A098E97DB2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DD837075-2818-4341-AD60-33A098E97DB2}.Release|Any CPU.Build.0 = Release|Any CPU - {673016C3-3163-4F09-90EA-4FDCF13AB1BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {673016C3-3163-4F09-90EA-4FDCF13AB1BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {673016C3-3163-4F09-90EA-4FDCF13AB1BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {673016C3-3163-4F09-90EA-4FDCF13AB1BA}.Release|Any CPU.Build.0 = Release|Any CPU - {771D2993-59C1-4F37-B28B-BE6C96294ADE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {771D2993-59C1-4F37-B28B-BE6C96294ADE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {771D2993-59C1-4F37-B28B-BE6C96294ADE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {771D2993-59C1-4F37-B28B-BE6C96294ADE}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DD837075-2818-4341-AD60-33A098E97DB2} = {FFF09815-02BA-4EC1-A346-AADA12CBF160} - {673016C3-3163-4F09-90EA-4FDCF13AB1BA} = {FFF09815-02BA-4EC1-A346-AADA12CBF160} - {771D2993-59C1-4F37-B28B-BE6C96294ADE} = {2F984849-BEBC-4AC7-81A4-5B65781E22C4} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {0D9F346A-5457-40C2-ABF5-F4D9B9D88CEA} - EndGlobalSection -EndGlobal diff --git a/Shared.All.Solution.slnx b/Shared.All.Solution.slnx new file mode 100644 index 0000000..aa761fd --- /dev/null +++ b/Shared.All.Solution.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/build/props/Test.props b/build/props/Test.props index 71ec4c9..92b6bc0 100644 --- a/build/props/Test.props +++ b/build/props/Test.props @@ -17,23 +17,12 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + + + + - - - - diff --git a/global.json b/global.json index 7b5f133..62b1fc2 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.100", + "version": "10.0.203", "allowPrerelease": false, "rollForward": "latestMajor" } diff --git a/src/OneImlx.Shared/Licensing/ProductCatalog.cs b/src/OneImlx.Shared/Licensing/ProductCatalog.cs index 16bfd44..a2d2ba9 100644 --- a/src/OneImlx.Shared/Licensing/ProductCatalog.cs +++ b/src/OneImlx.Shared/Licensing/ProductCatalog.cs @@ -1,12 +1,9 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ - -using System.Collections.Generic; using OneImlx.Shared.Infrastructure; +using System.Collections.Generic; namespace OneImlx.Shared.Licensing { @@ -55,6 +52,10 @@ public static string GetPlanDisplayName(string product, string plan) { return "Corporate"; } + case ProductCatalog.TerminalPlanExtension: + { + return "Extension"; + } default: { return "Custom"; @@ -134,6 +135,11 @@ public static bool IsValidPlan(string product, string plan) /// public const string TerminalPlanSolo = "urn:oneimlx:terminal:plan:solo"; + /// + /// The extension plan for . + /// + public const string TerminalPlanExtension = "urn:oneimlx:terminal:plan:extension"; + /// /// A mapping of products to their available license plans. /// @@ -148,6 +154,7 @@ public static bool IsValidPlan(string product, string plan) TerminalPlanSmb, TerminalPlanEnterprise, TerminalPlanCorporate, + TerminalPlanExtension, TerminalPlanCustom } } @@ -161,4 +168,4 @@ public static bool IsValidPlan(string product, string plan) { TerminalFramework, "Cross-Platform Terminal Framework (OneImlx.Terminal)" } }; } -} +} \ No newline at end of file diff --git a/src/OneImlx.Shared/OneImlx.Shared.csproj b/src/OneImlx.Shared/OneImlx.Shared.csproj index 6a6ecd4..81955eb 100644 --- a/src/OneImlx.Shared/OneImlx.Shared.csproj +++ b/src/OneImlx.Shared/OneImlx.Shared.csproj @@ -21,7 +21,7 @@ - - + + diff --git a/src/OneImlx.Shared/README.md b/src/OneImlx.Shared/README.md index b09d816..392b6df 100644 --- a/src/OneImlx.Shared/README.md +++ b/src/OneImlx.Shared/README.md @@ -1,3 +1,3 @@ -The shared package for Perpetual Intelligence® frameworks, AI, and developer tools. +The shared package for Perpetual Intelligence® LLC frameworks, AI, and developer tools. For more information see our [GitHub](https://github.com/perpetualintelligence/shared). diff --git a/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs b/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs index 3aea776..44b9c2a 100644 --- a/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs +++ b/src/OneImlx.Test/FluentAssertions/AssemblyFluentAssertions.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using System; using System.Collections.Generic; @@ -13,7 +10,7 @@ using System.Runtime.CompilerServices; using FluentAssertions; using FluentAssertions.Execution; -using FluentAssertions.Reflection; +using FluentAssertions.Types; using OneImlx.Shared.Infrastructure; namespace OneImlx.Test.FluentAssertions @@ -32,10 +29,7 @@ public static AndConstraint HaveTypesInRootNamespace(this As { var assembly = assertions.Subject; var actualNamespace = assembly.GetName().Name ?? throw new InvalidOperationException("Assembly name null"); - - Execute.Assertion - .ForCondition(actualNamespace == rootNamespace) - .FailWith($"Assembly '{assembly.GetName().Name}' does not have root namespace '{rootNamespace}'."); + actualNamespace.Should().Be(rootNamespace, $"Assembly '{assembly.GetName().Name}' should have root namespace '{rootNamespace}'"); var types = assembly.GetTypes().Where(e => !IsCompilerGenerated(e)); var invalidTypes = types.Where(e => @@ -81,13 +75,8 @@ public static AndConstraint HaveTypesInValidLocations(this A var testDir = Path.Combine(rootPath, "test"); var srcDir = Path.Combine(rootPath, "src"); - Execute.Assertion - .ForCondition(Directory.Exists(testDir)) - .FailWith("'test' directory not found. path={0}", rootPath); - - Execute.Assertion - .ForCondition(Directory.Exists(srcDir)) - .FailWith("'src' directory not found. path={0}", rootPath); + Directory.Exists(testDir).Should().BeTrue("'test' directory not found. path={0}", rootPath); + Directory.Exists(srcDir).Should().BeTrue("'src' directory not found. path={0}", rootPath); IEnumerable namespaces = assembly.GetTypes() .Where(e => !IsCompilerGenerated(e)) @@ -276,4 +265,4 @@ private static bool IsDelegate(Type type) return type.IsSubclassOf(typeof(Delegate)); } } -} +} \ No newline at end of file diff --git a/src/OneImlx.Test/FluentAssertions/ErrorExceptionFluentAssertions.cs b/src/OneImlx.Test/FluentAssertions/ErrorExceptionFluentAssertions.cs index eea3017..307d158 100644 --- a/src/OneImlx.Test/FluentAssertions/ErrorExceptionFluentAssertions.cs +++ b/src/OneImlx.Test/FluentAssertions/ErrorExceptionFluentAssertions.cs @@ -1,9 +1,6 @@ -/* - Copyright (c) 2023 Perpetual Intelligence L.L.C. All Rights Reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using FluentAssertions.Execution; using FluentAssertions.Specialized; @@ -40,15 +37,17 @@ public static ExceptionAssertions WithErrorCode( params object[] becauseArgs) where TException : Exception { - var exception = assertions.Which as ErrorException; - - Execute.Assertion - .BecauseOf(because, becauseArgs) - .WithExpectation("Expected exception with error code {0}{reason}, ", expectedErrorCode) - .ForCondition(assertions.Which is ErrorException errorException && errorException.Error.ErrorCode == expectedErrorCode) - .FailWith("Expected error code to be {0}{reason}, but found {1}.", - expectedErrorCode, - exception?.Error.ErrorCode); + if (assertions.Which is not ErrorException exception) + { + throw new AssertionFailedException("Expected exception to be an ErrorException, but it was not."); + } + + if (exception.Error.ErrorCode != expectedErrorCode) + { + var becauseMessage = string.IsNullOrEmpty(because) ? "" : " because " + string.Format(because, becauseArgs); + throw new AssertionFailedException( + $"Expected error code to be {expectedErrorCode}{becauseMessage}, but found {exception.Error.ErrorCode}."); + } return assertions; } @@ -74,18 +73,19 @@ public static ExceptionAssertions WithErrorDescription( string because = "", params object[] becauseArgs) where TException : ErrorException { - // Attempt to cast the caught exception to ErrorException to access the Error property. - var exception = assertions.Which as ErrorException; - - // Assert that the error code of the exception matches the expected error description. - Execute.Assertion - .ForCondition(exception != null && exception.Error.FormatDescription() == expectedErrorDescription) - .BecauseOf(because, becauseArgs) - .FailWith("Expected error description to be {0}{reason}, but found {1}.", - expectedErrorDescription, - exception?.Error.FormatDescription()); - - // Return the original ExceptionAssertions object to allow further assertion chaining. + if (assertions.Which is not ErrorException exception) + { + throw new AssertionFailedException("Expected exception to be an ErrorException, but it was not."); + } + + var actualDescription = exception.Error.FormatDescription(); + if (actualDescription != expectedErrorDescription) + { + var becauseMessage = string.IsNullOrEmpty(because) ? "" : " because " + string.Format(because, becauseArgs); + throw new AssertionFailedException( + $"Expected error description to be {expectedErrorDescription}{becauseMessage}, but found {actualDescription}."); + } + return assertions; } @@ -105,10 +105,12 @@ public static ExceptionAssertions WithError( params object[] becauseArgs) where TException : ErrorException { - Execute.Assertion - .ForCondition(exceptionAssertions.Which.Error == expectedError) - .BecauseOf(because, becauseArgs) - .FailWith("Expected error to be {0}{reason}, but found {1}.", expectedError, exceptionAssertions.Which.Error); + if (exceptionAssertions.Which.Error != expectedError) + { + var becauseMessage = string.IsNullOrEmpty(because) ? "" : " because " + string.Format(because, becauseArgs); + throw new AssertionFailedException( + $"Expected error to be {expectedError}{becauseMessage}, but found {exceptionAssertions.Which.Error}."); + } return exceptionAssertions; } @@ -128,10 +130,12 @@ public static async Task> WithErrorCode> WithErrorDescription> WithError( { var exceptionAssertions = await task; - Execute.Assertion - .ForCondition(exceptionAssertions.Which.Error == expectedError) - .BecauseOf(because, becauseArgs) - .FailWith("Expected error to be {0}{reason}, but found {1}.", expectedError, exceptionAssertions.Which.Error); + if (exceptionAssertions.Which.Error != expectedError) + { + var becauseMessage = string.IsNullOrEmpty(because) ? "" : " because " + string.Format(because, becauseArgs); + throw new AssertionFailedException( + $"Expected error to be {expectedError}{becauseMessage}, but found {exceptionAssertions.Which.Error}."); + } return exceptionAssertions; } diff --git a/src/OneImlx.Test/FluentAssertions/TypeFluentAssertions.cs b/src/OneImlx.Test/FluentAssertions/TypeFluentAssertions.cs index 7f6708f..fea517f 100644 --- a/src/OneImlx.Test/FluentAssertions/TypeFluentAssertions.cs +++ b/src/OneImlx.Test/FluentAssertions/TypeFluentAssertions.cs @@ -36,9 +36,10 @@ public static AndConstraint HaveConstantCount( var constants = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance) .Where(fi => fi.IsLiteral && !fi.IsInitOnly); - Execute.Assertion - .ForCondition(constants.Count() == expectedCount) - .FailWith($"Expected type '{type.FullName}' to define exactly {expectedCount} constants, but found {constants.Count()}."); + if (constants.Count() != expectedCount) + { + throw new AssertionFailedException($"Expected type '{type.FullName}' to define exactly {expectedCount} constants, but found {constants.Count()}."); + } return new AndConstraint(assertions); } @@ -63,14 +64,15 @@ public static AndConstraint HaveJsonProperty( JsonPropertyNameAttribute? jsonAttribute = propertyInfo?.GetCustomAttribute(); // Assert that the property exists, and its JsonPropertyNameAttribute has the expected name. - Execute.Assertion - .ForCondition(propertyInfo != null) - .FailWith("Expected type {0} to have a property named {1}, but no such property was found.", - typeAssertions.Subject, propertyName) - .Then - .ForCondition(jsonAttribute != null && jsonAttribute.Name == jsonPropertyName) - .FailWith("Expected property {0} on type {1} to be decorated with JsonPropertyNameAttribute with a name of '{2}', but found '{3}'.", - propertyName, typeAssertions.Subject, jsonPropertyName, jsonAttribute?.Name ?? "null"); + if (propertyInfo == null) + { + throw new AssertionFailedException($"Expected type {typeAssertions.Subject} to have a property named {propertyName}, but no such property was found."); + } + + if (jsonAttribute == null || jsonAttribute.Name != jsonPropertyName) + { + throw new AssertionFailedException($"Expected property {propertyName} on type {typeAssertions.Subject} to be decorated with JsonPropertyNameAttribute with a name of '{jsonPropertyName}', but found '{jsonAttribute?.Name ?? "null"}'."); + } // Return an AndConstraint to allow for further assertions. return new AndConstraint(typeAssertions); @@ -91,9 +93,10 @@ public static AndConstraint HavePropertyCount( // Retrieves both static and instance public properties. var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance); - Execute.Assertion - .ForCondition(properties.Length == expectedCount) - .FailWith($"Expected type '{type.FullName}' to define exactly {expectedCount} public properties (both static and instance), but found {properties.Length}."); + if (properties.Length != expectedCount) + { + throw new AssertionFailedException($"Expected type '{type.FullName}' to define exactly {expectedCount} public properties (both static and instance), but found {properties.Length}."); + } return new AndConstraint(assertions); } @@ -124,18 +127,18 @@ public static AndConstraint HaveSnakeCaseJsonNames( { var expectedJsonName = ToExpectedSnakeCase(property.Name); - Execute.Assertion - .ForCondition(jsonPropertyName.Name == expectedJsonName) - .BecauseOf(because, becauseArgs) - .FailWith("Expected property \"{0}\" to have JsonPropertyName \"{1}\", but found \"{2}\".", - property.Name, expectedJsonName, jsonPropertyName.Name); + if (jsonPropertyName.Name != expectedJsonName) + { + var becauseMessage = string.IsNullOrEmpty(because) ? "" : " because " + string.Format(because, becauseArgs); + throw new AssertionFailedException( + $"Expected property \"{property.Name}\" to have JsonPropertyName \"{expectedJsonName}\"{becauseMessage}, but found \"{jsonPropertyName.Name}\"."); + } } else { if (jsonIgnore == null) { - Execute.Assertion - .FailWith("Expected property \"{0}\" to be decorated with either [JsonPropertyName] or [JsonIgnore], but no attribute was found.", property.Name); + throw new AssertionFailedException($"Expected property \"{property.Name}\" to be decorated with either [JsonPropertyName] or [JsonIgnore], but no attribute was found."); } // else: Property is [JsonIgnore], so no validation needed. diff --git a/src/OneImlx.Test/OneImlx.Test.csproj b/src/OneImlx.Test/OneImlx.Test.csproj index 9c9b18d..f3ca65e 100644 --- a/src/OneImlx.Test/OneImlx.Test.csproj +++ b/src/OneImlx.Test/OneImlx.Test.csproj @@ -19,11 +19,8 @@ infrastructure oneimlx - - + diff --git a/test/OneImlx.Shared.Tests/Licensing/ProductCatalogTests.cs b/test/OneImlx.Shared.Tests/Licensing/ProductCatalogTests.cs index 93e44ce..599e573 100644 --- a/test/OneImlx.Shared.Tests/Licensing/ProductCatalogTests.cs +++ b/test/OneImlx.Shared.Tests/Licensing/ProductCatalogTests.cs @@ -1,9 +1,6 @@ -/* - Copyright © 2019-2025 Perpetual Intelligence L.L.C. All rights reserved. - - For license, terms, and data policies, go to: - https://terms.perpetualintelligence.com/articles/intro.html -*/ +// Copyright © 2019-2026 Perpetual Intelligence L.L.C. All rights reserved. +// For license, terms, and data policies, go to: +// https://terms.perpetualintelligence.com/articles/intro.html using FluentAssertions; using OneImlx.Shared.Infrastructure; @@ -18,7 +15,7 @@ public class ProductCatalogTests [Fact] public void Constants_Returns_ExpectedUrns() { - typeof(ProductCatalog).Should().HaveConstantCount(8); + typeof(ProductCatalog).Should().HaveConstantCount(9); // Products ProductCatalog.TerminalFramework.Should().Be("urn:oneimlx:terminal"); @@ -30,6 +27,7 @@ public void Constants_Returns_ExpectedUrns() ProductCatalog.TerminalPlanSmb.Should().Be("urn:oneimlx:terminal:plan:smb"); ProductCatalog.TerminalPlanEnterprise.Should().Be("urn:oneimlx:terminal:plan:enterprise"); ProductCatalog.TerminalPlanCorporate.Should().Be("urn:oneimlx:terminal:plan:corporate"); + ProductCatalog.TerminalPlanExtension.Should().Be("urn:oneimlx:terminal:plan:extension"); ProductCatalog.TerminalPlanCustom.Should().Be("urn:oneimlx:terminal:plan:custom"); } @@ -58,6 +56,7 @@ public void GetPlanDisplayName_InvalidProductForValidPlan_ThrowsErrorException() [InlineData(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanSmb, "SMB")] [InlineData(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanEnterprise, "Enterprise")] [InlineData(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanCorporate, "Corporate")] + [InlineData(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanExtension, "Extension")] [InlineData(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanCustom, "Custom")] public void GetPlanDisplayName_ValidPlan_ReturnsDisplayName(string product, string plan, string expectedDisplayName) { @@ -105,6 +104,7 @@ public void IsValidPlan_Returns_True_ForValidPlans() ProductCatalog.IsValidPlan(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanSmb).Should().BeTrue(); ProductCatalog.IsValidPlan(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanEnterprise).Should().BeTrue(); ProductCatalog.IsValidPlan(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanCorporate).Should().BeTrue(); + ProductCatalog.IsValidPlan(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanExtension).Should().BeTrue(); ProductCatalog.IsValidPlan(ProductCatalog.TerminalFramework, ProductCatalog.TerminalPlanCustom).Should().BeTrue(); } @@ -122,10 +122,11 @@ public void ProductPlans_Returns_TerminalFrameworkPlans() ProductCatalog.TerminalPlanSmb, ProductCatalog.TerminalPlanEnterprise, ProductCatalog.TerminalPlanCorporate, + ProductCatalog.TerminalPlanExtension, ProductCatalog.TerminalPlanCustom }); - plans.Count.Should().Be(7); + plans.Count.Should().Be(8); } [Fact] @@ -137,4 +138,4 @@ public void Products_Returns_Correct() ProductCatalog.Products[ProductCatalog.TerminalFramework].Should().Be("Cross-Platform Terminal Framework (OneImlx.Terminal)"); } } -} +} \ No newline at end of file