From 69faca33da5afa94d22f1eac238e991597d992fe Mon Sep 17 00:00:00 2001 From: Ninja Date: Tue, 6 Dec 2022 21:16:18 +0000 Subject: [PATCH 1/2] - Add Code - Check in point 2 --- .github/workflows/CI-Build.yml | 18 ++-- Ninja.FeatureOn.sln => Ninja.FeatureOne.sln | 26 ++--- README.md | 21 ++-- src/FeatureOn/Logger/NullLogger.cs | 10 -- src/FeatureOn/Stores/FeatureStore.cs | 54 ----------- src/FeatureOn/Stores/IStoreProvider.cs | 17 ---- src/FeatureOn/Toggles/FeatureToggle.cs | 27 ------ src/FeatureOn/Toggles/IFeatureToggle.cs | 9 -- src/FeatureOn/Toggles/IToggleCondition.cs | 7 -- .../Toggles/ToggleConditionFactory.cs | 77 --------------- src/FeatureOn/Toggles/ToggleConditionState.cs | 17 ---- src/FeatureOn/Toggles/ToggleOperator.cs | 8 -- src/{FeatureOn => FeatureOne}/AssemblyInfo.cs | 6 +- src/{FeatureOn => FeatureOne}/Constants.cs | 2 +- src/{FeatureOn => FeatureOne/Core}/Feature.cs | 10 +- src/FeatureOne/Core/ICondition.cs | 15 +++ src/FeatureOne/Core/IToggle.cs | 10 ++ src/FeatureOne/Core/NamePostFix.cs | 16 ++++ src/FeatureOne/Core/Operator.cs | 8 ++ .../Core/Stores/ConditionFactory.cs | 87 +++++++++++++++++ src/FeatureOne/Core/Stores/FeatureStore.cs | 52 ++++++++++ src/FeatureOne/Core/Stores/IStoreProvider.cs | 44 +++++++++ .../Core/Stores/IToggleDeserializer.cs | 7 ++ .../Core/Stores/ToggleDeserializer.cs | 22 +++++ src/FeatureOne/Core/Toggle.cs | 24 +++++ .../Toggles/Conditions/RegexCondition.cs | 9 +- .../Toggles/Conditions/SimpleCondition.cs | 5 +- .../FeatureConfiguration.cs | 4 +- src/{FeatureOn => FeatureOne}/FeatureName.cs | 5 +- .../FeatureOne.csproj} | 15 ++- src/{FeatureOn => FeatureOne}/Features.cs | 32 +++---- src/{FeatureOn => FeatureOne}/IFeature.cs | 7 +- .../Logger => FeatureOne}/IFeatureLogger.cs | 5 +- .../Stores => FeatureOne}/IFeatureStore.cs | 5 +- src/FeatureOne/LambdaComparer.cs | 22 +++++ src/FeatureOne/NullLogger.cs | 17 ++++ .../FeatureNameTest.cs | 5 +- .../FeatureOne.Tests.csproj} | 3 +- .../FeatureTest.cs | 42 +++++--- test/FeatureOne.Tests/FeaturesTests.cs | 87 +++++++++++++++++ .../Stores/FeatureStoreTests.cs | 95 +++++++++++++++++++ .../Stores/ToggleDeserializerTests.cs | 36 +++++++ test/FeatureOne.Tests/ToggleTests.cs | 25 +++++ .../Toggles/NamePostFixTest.cs | 22 +++++ .../Toggles/RegexConditionTest.cs | 10 +- .../Toggles/SimpleConditionTest.cs | 8 +- .../Toggles/ToggleConditionFactoryTest.cs | 39 ++++++++ .../Usings.cs | 0 48 files changed, 752 insertions(+), 340 deletions(-) rename Ninja.FeatureOn.sln => Ninja.FeatureOne.sln (61%) delete mode 100644 src/FeatureOn/Logger/NullLogger.cs delete mode 100644 src/FeatureOn/Stores/FeatureStore.cs delete mode 100644 src/FeatureOn/Stores/IStoreProvider.cs delete mode 100644 src/FeatureOn/Toggles/FeatureToggle.cs delete mode 100644 src/FeatureOn/Toggles/IFeatureToggle.cs delete mode 100644 src/FeatureOn/Toggles/IToggleCondition.cs delete mode 100644 src/FeatureOn/Toggles/ToggleConditionFactory.cs delete mode 100644 src/FeatureOn/Toggles/ToggleConditionState.cs delete mode 100644 src/FeatureOn/Toggles/ToggleOperator.cs rename src/{FeatureOn => FeatureOne}/AssemblyInfo.cs (90%) rename src/{FeatureOn => FeatureOne}/Constants.cs (81%) rename src/{FeatureOn => FeatureOne/Core}/Feature.cs (64%) create mode 100644 src/FeatureOne/Core/ICondition.cs create mode 100644 src/FeatureOne/Core/IToggle.cs create mode 100644 src/FeatureOne/Core/NamePostFix.cs create mode 100644 src/FeatureOne/Core/Operator.cs create mode 100644 src/FeatureOne/Core/Stores/ConditionFactory.cs create mode 100644 src/FeatureOne/Core/Stores/FeatureStore.cs create mode 100644 src/FeatureOne/Core/Stores/IStoreProvider.cs create mode 100644 src/FeatureOne/Core/Stores/IToggleDeserializer.cs create mode 100644 src/FeatureOne/Core/Stores/ToggleDeserializer.cs create mode 100644 src/FeatureOne/Core/Toggle.cs rename src/{FeatureOn => FeatureOne/Core}/Toggles/Conditions/RegexCondition.cs (79%) rename src/{FeatureOn => FeatureOne/Core}/Toggles/Conditions/SimpleCondition.cs (64%) rename src/{FeatureOn/Configuration => FeatureOne}/FeatureConfiguration.cs (80%) rename src/{FeatureOn => FeatureOne}/FeatureName.cs (94%) rename src/{FeatureOn/FeatureOn.csproj => FeatureOne/FeatureOne.csproj} (78%) rename src/{FeatureOn => FeatureOne}/Features.cs (78%) rename src/{FeatureOn => FeatureOne}/IFeature.cs (60%) rename src/{FeatureOn/Logger => FeatureOne}/IFeatureLogger.cs (86%) rename src/{FeatureOn/Stores => FeatureOne}/IFeatureStore.cs (73%) create mode 100644 src/FeatureOne/LambdaComparer.cs create mode 100644 src/FeatureOne/NullLogger.cs rename test/{FeatureOn.Tests => FeatureOne.Tests}/FeatureNameTest.cs (92%) rename test/{FeatureOn.Tests/FeatureOn.Tests.csproj => FeatureOne.Tests/FeatureOne.Tests.csproj} (83%) rename test/{FeatureOn.Tests => FeatureOne.Tests}/FeatureTest.cs (74%) create mode 100644 test/FeatureOne.Tests/FeaturesTests.cs create mode 100644 test/FeatureOne.Tests/Stores/FeatureStoreTests.cs create mode 100644 test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs create mode 100644 test/FeatureOne.Tests/ToggleTests.cs create mode 100644 test/FeatureOne.Tests/Toggles/NamePostFixTest.cs rename test/{FeatureOn.Tests => FeatureOne.Tests}/Toggles/RegexConditionTest.cs (86%) rename test/{FeatureOn.Tests => FeatureOne.Tests}/Toggles/SimpleConditionTest.cs (64%) create mode 100644 test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs rename test/{FeatureOn.Tests => FeatureOne.Tests}/Usings.cs (100%) diff --git a/.github/workflows/CI-Build.yml b/.github/workflows/CI-Build.yml index 8b94149..477a2ac 100644 --- a/.github/workflows/CI-Build.yml +++ b/.github/workflows/CI-Build.yml @@ -10,7 +10,7 @@ jobs: Run-Lint: runs-on: ubuntu-latest env: - github-token: '${{ secrets.GH_Packages }}' + github-token: '${{ secrets.GITHUB_TOKEN }}' steps: - name: Step-01 Checkout code uses: actions/checkout@v3 @@ -30,7 +30,7 @@ jobs: semVersion: ${{ steps.gitversion.outputs.MajorMinorPatch }} branchName: ${{ steps.gitversion.outputs.branchName }} env: - working-directory: /home/runner/work/FeatureOn/FeatureOn + working-directory: /home/runner/work/FeatureOne/FeatureOne steps: - name: Step-01 Install GitVersion @@ -79,7 +79,7 @@ jobs: semVersion: ${{ steps.gitversion.outputs.MajorMinorPatch }} branchName: ${{ steps.gitversion.outputs.branchName }} env: - working-directory: /home/runner/work/FeatureOn/FeatureOn + working-directory: /home/runner/work/FeatureOne/FeatureOne steps: - name: Step-01 Install GitVersion @@ -131,8 +131,8 @@ jobs: outputs: semVersion: ${{ needs.Build-Release.outputs.semVersion }} env: - github-token: '${{ secrets.GH_Packages }}' - working-directory: /home/runner/work/FeatureOn/FeatureOn + github-token: '${{ secrets.GITHUB_TOKEN }}' + working-directory: /home/runner/work/FeatureOne/FeatureOne steps: - name: Step-01 Retrieve Build Artifacts uses: actions/download-artifact@v3 @@ -152,7 +152,7 @@ jobs: runs-on: ubuntu-latest env: nuget-token: '${{ secrets.NUGET_API_KEY }}' - working-directory: /home/runner/work/FeatureOn/FeatureOn + working-directory: /home/runner/work/FeatureOne/FeatureOne steps: - name: Step-01 Retrieve Build Artifacts uses: actions/download-artifact@v3 @@ -166,10 +166,10 @@ jobs: -X POST \ -H "Accept:application/vnd.github+json" \ -H "Authorization:token ${{ secrets.GITHUB_TOKEN }}" \ - https://api.github.com/ninjarocks/FeatureOn/releases \ - -d '{"tag_name":v${{ needs.Package-Artifacts.outputs.semVersion }},"target_commitish":"master","name":"FeatureOn","body":"Relese version ${{ needs.Package.outputs.semVersion }}","draft":false,"prerelease":false,"generate_release_notes":false}' + https://api.github.com/ninjarocks/FeatureOne/releases \ + -d '{"tag_name":v${{ needs.Package-Artifacts.outputs.semVersion }},"target_commitish":"master","name":"FeatureOne","body":"Relese version ${{ needs.Package.outputs.semVersion }}","draft":false,"prerelease":false,"generate_release_notes":false}' - name: Step-03 Publish to Nuget Org - run: dotnet nuget push ${{env.working-directory}}/src/FeatureOn/bin/Release/*.nupkg --skip-duplicate --api-key ${{ env.nuget-token }} --source https://api.nuget.org/v3/index.json + run: dotnet nuget push ${{env.working-directory}}/src/FeatureOne/bin/Release/*.nupkg --skip-duplicate --api-key ${{ env.nuget-token }} --source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/Ninja.FeatureOn.sln b/Ninja.FeatureOne.sln similarity index 61% rename from Ninja.FeatureOn.sln rename to Ninja.FeatureOne.sln index 61cee9d..941ec11 100644 --- a/Ninja.FeatureOn.sln +++ b/Ninja.FeatureOne.sln @@ -5,7 +5,7 @@ VisualStudioVersion = 17.4.33110.190 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B5EFC87D-E5C5-488E-AA89-06D26027E4C7}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{E864826B-B5D2-4DAD-B53A-2A3976226B53}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E864826B-B5D2-4DAD-B53A-2A3976226B53}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".misc", ".misc", "{9930EAFC-568F-4676-82AB-2B8F9D7535B3}" ProjectSection(SolutionItems) = preProject @@ -21,9 +21,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "github", "github", "{FB8FCD .github\workflows\codeql.yml = .github\workflows\codeql.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOn", "src\FeatureOn\FeatureOn.csproj", "{4A778AE0-529E-4EE2-953F-9B7FFB982F14}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOne", "src\FeatureOne\FeatureOne.csproj", "{65BD1F94-CEF4-4CC1-8482-3FF72BBF620F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOn.Tests", "test\FeatureOn.Tests\FeatureOn.Tests.csproj", "{A0E75FB8-73DB-4321-9B29-EBDE27741911}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeatureOne.Tests", "test\FeatureOne.Tests\FeatureOne.Tests.csproj", "{F51B62E7-1A8F-41DC-B027-074932A01D33}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -31,22 +31,22 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4A778AE0-529E-4EE2-953F-9B7FFB982F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4A778AE0-529E-4EE2-953F-9B7FFB982F14}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4A778AE0-529E-4EE2-953F-9B7FFB982F14}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4A778AE0-529E-4EE2-953F-9B7FFB982F14}.Release|Any CPU.Build.0 = Release|Any CPU - {A0E75FB8-73DB-4321-9B29-EBDE27741911}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A0E75FB8-73DB-4321-9B29-EBDE27741911}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A0E75FB8-73DB-4321-9B29-EBDE27741911}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A0E75FB8-73DB-4321-9B29-EBDE27741911}.Release|Any CPU.Build.0 = Release|Any CPU + {65BD1F94-CEF4-4CC1-8482-3FF72BBF620F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65BD1F94-CEF4-4CC1-8482-3FF72BBF620F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65BD1F94-CEF4-4CC1-8482-3FF72BBF620F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65BD1F94-CEF4-4CC1-8482-3FF72BBF620F}.Release|Any CPU.Build.0 = Release|Any CPU + {F51B62E7-1A8F-41DC-B027-074932A01D33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F51B62E7-1A8F-41DC-B027-074932A01D33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F51B62E7-1A8F-41DC-B027-074932A01D33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F51B62E7-1A8F-41DC-B027-074932A01D33}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {FB8FCDD0-A3D4-4776-85E0-C47ECFB3D310} = {9930EAFC-568F-4676-82AB-2B8F9D7535B3} - {4A778AE0-529E-4EE2-953F-9B7FFB982F14} = {B5EFC87D-E5C5-488E-AA89-06D26027E4C7} - {A0E75FB8-73DB-4321-9B29-EBDE27741911} = {E864826B-B5D2-4DAD-B53A-2A3976226B53} + {65BD1F94-CEF4-4CC1-8482-3FF72BBF620F} = {B5EFC87D-E5C5-488E-AA89-06D26027E4C7} + {F51B62E7-1A8F-41DC-B027-074932A01D33} = {E864826B-B5D2-4DAD-B53A-2A3976226B53} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {745CC402-145C-4E31-BAB7-DA93F8292512} diff --git a/README.md b/README.md index 51f371b..fd88353 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,21 @@ -# FeatureOn -FeatureOn - Library to implement feature toggles. +# FeatureOne +FeatureOne - Library to implement feature toggles. Example feature list ``` { - "feature-01":{ - "toggle"{ + "feature-01":{ -- Feature name + "toggle"{ -- Toggle details for the feature - "operator":"all", + "operator":"[any|all]", -- evalue true when any condition is met or all conditions are met. "conditions":[{ - "type":"Regex", - "claim":"email", - "expression":"*@gbk.com" + "type":"Regex", -- RegexCondition evalues when the spefied claim in context has a match against the specified expression + "claim":"email", -- Name of claim to use for evaluation. + "expression":"*@gbk.com" -- Regex expression to use for evaulation. }, { - "type":"Regex", - "claim":"user_role", - "expression":"administrator" + "type":"Simple", -- SimpleCondition to set the feature to be enabled or disabled. + "isEnabled":[true|false] -- Feature is enabled when true else disabled. }] } } diff --git a/src/FeatureOn/Logger/NullLogger.cs b/src/FeatureOn/Logger/NullLogger.cs deleted file mode 100644 index c160523..0000000 --- a/src/FeatureOn/Logger/NullLogger.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace FeatureOn.Logger -{ - public class NullLogger : IFeatureLogger - { - public void Info(string message) { } - public void Debug(string message) { } - public void Warn(string message) { } - public void Error(string message) { } - } -} \ No newline at end of file diff --git a/src/FeatureOn/Stores/FeatureStore.cs b/src/FeatureOn/Stores/FeatureStore.cs deleted file mode 100644 index 4e82e57..0000000 --- a/src/FeatureOn/Stores/FeatureStore.cs +++ /dev/null @@ -1,54 +0,0 @@ -using FeatureOn.Configuration; -using FeatureOn.Toggle; -using FeatureOn.Toggles; -using Ninja.FeatureIt; -using System.Text.Json; - -namespace FeatureOn.Stores -{ - internal class FeatureStore : IFeatureStore - { - IStoreProvider storeProvider; - private readonly FeatureConfiguration Configuration; - - public FeatureStore(IStoreProvider storeProvider):this(storeProvider, new FeatureConfiguration()) { } - public FeatureStore(IStoreProvider storeProvider, FeatureConfiguration configuration) - { - this.storeProvider = storeProvider; - this.Configuration = configuration; - } - public IEnumerable FindStartsWith(string key) - { - return GetAll().Where(x => x.Name.Value.StartsWith(key, StringComparison.OrdinalIgnoreCase)); - } - - public IEnumerable GetAll() - { - var jsonStrings = storeProvider.Get(); - if (jsonStrings ==null || !jsonStrings.Any()) return Enumerable.Empty(); - - var result = new List(); - - foreach (var jsonString in jsonStrings) - { - FeatureRaw? rawfeature = null; - try - { - rawfeature = JsonSerializer.Deserialize(jsonString); - if (rawfeature == null) continue; - - result.Add(new Feature(rawfeature.Name, new FeatureToggle( - Enum.TryParse(rawfeature.Operator, out var @operator) - ? @operator - : ToggleOperator.All, - ToggleConditionFactory.Create(rawfeature.Conditions)))); - } - catch (Exception ex) - { - Configuration.Logger?.Warn($"Action='Failed to Deserialize', Feature='{rawfeature?.Name}', Exception='{ex}'."); - } - } - return result; - } - } -} diff --git a/src/FeatureOn/Stores/IStoreProvider.cs b/src/FeatureOn/Stores/IStoreProvider.cs deleted file mode 100644 index e622a5c..0000000 --- a/src/FeatureOn/Stores/IStoreProvider.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Nodes; - -namespace FeatureOn.Stores -{ - internal interface IStoreProvider - { - string[] Get(); - } - - public class FeatureRaw - { - public string Name { get; set; } - public string Operator { get; set; } - public JsonObject[] Conditions { get; set; } - - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/FeatureToggle.cs b/src/FeatureOn/Toggles/FeatureToggle.cs deleted file mode 100644 index 1d1f37d..0000000 --- a/src/FeatureOn/Toggles/FeatureToggle.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FeatureOn.Toggle; -using FeatureOn.Toggle.Conditions; - -namespace FeatureOn.Toggles -{ - public class FeatureToggle : IFeatureToggle - { - public FeatureToggle(ToggleOperator @operator, IToggleCondition[] conditions) - { - Operator = @operator; - Conditions = conditions ?? new[] { new SimpleCondition() }; - } - - public ToggleOperator Operator { get; set; } = ToggleOperator.Any; - public IToggleCondition[] Conditions { get; set; } - - public bool Run(IDictionary claims) - { - if (claims == null) - return false; - - return Operator == ToggleOperator.Any - ? Conditions.Any(x => x.Evaluate(claims)) - : Conditions.All(x => x.Evaluate(claims)); - } - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/IFeatureToggle.cs b/src/FeatureOn/Toggles/IFeatureToggle.cs deleted file mode 100644 index 3251eeb..0000000 --- a/src/FeatureOn/Toggles/IFeatureToggle.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace FeatureOn.Toggle -{ - public interface IFeatureToggle - { - ToggleOperator Operator { get; set; } - IToggleCondition[] Conditions { get; set; } - bool Run(IDictionary claims); - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/IToggleCondition.cs b/src/FeatureOn/Toggles/IToggleCondition.cs deleted file mode 100644 index e0656e4..0000000 --- a/src/FeatureOn/Toggles/IToggleCondition.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace FeatureOn.Toggle -{ - public interface IToggleCondition - { - bool Evaluate(IDictionary claims); - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/ToggleConditionFactory.cs b/src/FeatureOn/Toggles/ToggleConditionFactory.cs deleted file mode 100644 index 6668513..0000000 --- a/src/FeatureOn/Toggles/ToggleConditionFactory.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Reflection; -using System.Text.Json.Nodes; -using FeatureOn.Toggle; - -namespace FeatureOn.Toggles -{ - public static class ToggleConditionFactory - { - private static Type[] loaddedTypes; - - private static Type[] LoaddedTypes - { - get - { - if (loaddedTypes == null || loaddedTypes.Length == 0) - loaddedTypes = Assembly.GetExecutingAssembly().GetTypes(); - - return loaddedTypes; - } - } - - public static IToggleCondition Create(JsonObject JsonObject) - { - - if (JsonObject == null) - throw new ArgumentNullException(nameof(JsonObject)); - - var typeName = JsonObject?["type"]?.ToString(); - - var toggle = CreateInstance(typeName); - - HydrateToggle(toggle, JsonObject); - - - return toggle; - } - public static IToggleCondition[] Create(JsonObject[] conditions) - { - if (conditions == null) - throw new ArgumentNullException(nameof(conditions)); - - return conditions.Select(s => Create(s)).ToArray(); - } - private static void HydrateToggle(IToggleCondition toggleCondition, JsonObject state) - { - foreach (var propertyInfo in GetProperties(toggleCondition)) - { - var name = propertyInfo.Name.ToLower(); - if (state.ContainsKey(name) == false) - continue; - - var storedValue = state[name]?.GetValue(); - propertyInfo.SetValue(toggleCondition, storedValue, null); - } - } - - private static PropertyInfo[] GetProperties(IToggleCondition condition) - { - var propertyInfos = condition.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.CanWrite) - .ToArray(); - - return propertyInfos; - } - - private static IToggleCondition CreateInstance(string typeName) - { - var type = LoaddedTypes - .FirstOrDefault(p => typeof(IToggleCondition).IsAssignableFrom(p) && p.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase)); - - if (type == null) - throw new Exception($"Could not find a toggle type for: '{typeName}'"); - - return (IToggleCondition)Activator.CreateInstance(type, true); - } - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/ToggleConditionState.cs b/src/FeatureOn/Toggles/ToggleConditionState.cs deleted file mode 100644 index 7175904..0000000 --- a/src/FeatureOn/Toggles/ToggleConditionState.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Text.Json.Serialization; - -namespace FeatureOn.Toggles -{ - public class ToggleConditionState - { - public string Type { get; } - public IDictionary Values { get; } = new Dictionary(); - - [JsonConstructor] - public ToggleConditionState(string type, IDictionary values) - { - Type = type.Trim(); - Values = values; - } - } -} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/ToggleOperator.cs b/src/FeatureOn/Toggles/ToggleOperator.cs deleted file mode 100644 index c9b0c69..0000000 --- a/src/FeatureOn/Toggles/ToggleOperator.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace FeatureOn.Toggle -{ - public enum ToggleOperator - { - All = 0, - Any = 1 - } -} \ No newline at end of file diff --git a/src/FeatureOn/AssemblyInfo.cs b/src/FeatureOne/AssemblyInfo.cs similarity index 90% rename from src/FeatureOn/AssemblyInfo.cs rename to src/FeatureOne/AssemblyInfo.cs index 16a5797..8df5304 100644 --- a/src/FeatureOn/AssemblyInfo.cs +++ b/src/FeatureOne/AssemblyInfo.cs @@ -17,10 +17,10 @@ [assembly: System.Reflection.AssemblyDescriptionAttribute("Library to provide feature toggles for projects.")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] [assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] -[assembly: System.Reflection.AssemblyProductAttribute("FeatureOn")] -[assembly: System.Reflection.AssemblyTitleAttribute("FeatureOn")] +[assembly: System.Reflection.AssemblyProductAttribute("FeatureOne")] +[assembly: System.Reflection.AssemblyTitleAttribute("FeatureOne")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://github.com/NinjaRocks/FeatureOn")] +[assembly: System.Reflection.AssemblyMetadataAttribute("RepositoryUrl", "https://github.com/NinjaRocks/FeatureOne")] // Generated by the MSBuild WriteCodeFragment class. diff --git a/src/FeatureOn/Constants.cs b/src/FeatureOne/Constants.cs similarity index 81% rename from src/FeatureOn/Constants.cs rename to src/FeatureOne/Constants.cs index 7854ed9..1bac9ab 100644 --- a/src/FeatureOn/Constants.cs +++ b/src/FeatureOne/Constants.cs @@ -1,4 +1,4 @@ -namespace Ninja.FeatureIt +namespace FeatureOne { public static class Constants { diff --git a/src/FeatureOn/Feature.cs b/src/FeatureOne/Core/Feature.cs similarity index 64% rename from src/FeatureOn/Feature.cs rename to src/FeatureOne/Core/Feature.cs index 61fd75c..a2cea9c 100644 --- a/src/FeatureOn/Feature.cs +++ b/src/FeatureOne/Core/Feature.cs @@ -1,7 +1,4 @@ -using FeatureOn.Toggle; -using Ninja.FeatureIt; - -namespace FeatureOn +namespace FeatureOne.Core { public class Feature : IFeature { @@ -9,14 +6,15 @@ protected Feature() { } - public Feature(string name, IFeatureToggle toggle) + public Feature(string name, IToggle toggle) { Name = new FeatureName(name); Toggle = toggle; } public FeatureName Name { get; protected set; } - public IFeatureToggle Toggle { get; protected set; } + public IToggle Toggle { get; protected set; } + public bool IsEnabled(IDictionary claims) => Toggle.Run(claims); } } \ No newline at end of file diff --git a/src/FeatureOne/Core/ICondition.cs b/src/FeatureOne/Core/ICondition.cs new file mode 100644 index 0000000..0197906 --- /dev/null +++ b/src/FeatureOne/Core/ICondition.cs @@ -0,0 +1,15 @@ +namespace FeatureOne.Core +{ + /// + /// Interface to implement toggle condition. + /// + public interface ICondition + { + /// + /// Implement method to evaulate toggle condition. + /// + /// List of user claims; could be null + /// + bool Evaluate(IDictionary claims); + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/IToggle.cs b/src/FeatureOne/Core/IToggle.cs new file mode 100644 index 0000000..2ea1675 --- /dev/null +++ b/src/FeatureOne/Core/IToggle.cs @@ -0,0 +1,10 @@ +namespace FeatureOne.Core +{ + public interface IToggle + { + Operator Operator { get; set; } + ICondition[] Conditions { get; set; } + + bool Run(IDictionary claims); + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/NamePostFix.cs b/src/FeatureOne/Core/NamePostFix.cs new file mode 100644 index 0000000..f2fceab --- /dev/null +++ b/src/FeatureOne/Core/NamePostFix.cs @@ -0,0 +1,16 @@ +namespace FeatureOne.Core +{ + public class NamePostFix + { + public string Name { get; private set; } + + public NamePostFix(string name, string postFix) + { + if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); + + var names = name.Split(postFix); + Name = names.Length >= 1 + ? $"{names[0]}{postFix}" : $"{name}{postFix}"; + } + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Operator.cs b/src/FeatureOne/Core/Operator.cs new file mode 100644 index 0000000..4bc0e19 --- /dev/null +++ b/src/FeatureOne/Core/Operator.cs @@ -0,0 +1,8 @@ +namespace FeatureOne.Core +{ + public enum Operator + { + Any = 0, + All = 1 + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Stores/ConditionFactory.cs b/src/FeatureOne/Core/Stores/ConditionFactory.cs new file mode 100644 index 0000000..6c4e40c --- /dev/null +++ b/src/FeatureOne/Core/Stores/ConditionFactory.cs @@ -0,0 +1,87 @@ +using System.ComponentModel; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace FeatureOne.Core.Stores +{ + public static class ConditionFactory + { + private static Type[] loaddedTypes; + + private static Type[] LoaddedTypes + { + get + { + if (loaddedTypes == null || loaddedTypes.Length == 0) + loaddedTypes = Assembly.GetExecutingAssembly().GetTypes(); + + return loaddedTypes; + } + } + + public static ICondition Create(JsonObject JsonObject) + { + if (JsonObject == null) + throw new ArgumentNullException(nameof(JsonObject)); + + var typeName = JsonObject?["type"]?.ToString(); + + var toggle = CreateInstance(new NamePostFix(typeName, "Condition")); + + HydrateToggle(toggle, JsonObject); + + return toggle; + } + + public static ICondition[] Create(JsonObject[] conditions) + { + if (conditions == null) + throw new ArgumentNullException(nameof(conditions)); + + return conditions.Select(s => Create(s)).ToArray(); + } + + private static void HydrateToggle(ICondition toggleCondition, JsonObject state) + { + var properties = GetProperties(toggleCondition); + foreach (var propertyInfo in properties) + { + var name = propertyInfo.Name; + var keyValues = state.Deserialize>(); + + if (!keyValues.Keys.Contains(name, + new LambdaComparer((x, y) => x.Equals(y, StringComparison.OrdinalIgnoreCase)))) + continue; + + var value = state.Where(x => x.Key.Equals(name, StringComparison.OrdinalIgnoreCase)) + .First().Value?.ToString(); + + var converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType); + var propValue = converter.ConvertFrom(value); + + propertyInfo.SetValue(toggleCondition, propValue, null); + } + } + + private static PropertyInfo[] GetProperties(ICondition condition) + { + var propertyInfos = condition.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanWrite) + .ToArray(); + + return propertyInfos; + } + + private static ICondition CreateInstance(NamePostFix conditionName) + { + var type = LoaddedTypes + .FirstOrDefault(p => typeof(ICondition).IsAssignableFrom(p) && p.Name.Equals(conditionName.Name, StringComparison.OrdinalIgnoreCase)); + + if (type == null) + throw new Exception($"Could not find a toggle type for: '{conditionName.Name}'"); + + return (ICondition)Activator.CreateInstance(type, true); + } + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Stores/FeatureStore.cs b/src/FeatureOne/Core/Stores/FeatureStore.cs new file mode 100644 index 0000000..1109875 --- /dev/null +++ b/src/FeatureOne/Core/Stores/FeatureStore.cs @@ -0,0 +1,52 @@ + +namespace FeatureOne.Core.Stores +{ + public class FeatureStore : IFeatureStore + { + private IStoreProvider storeProvider; + private readonly FeatureConfiguration Configuration; + private IToggleDeserializer toggleDeserializer; + + public FeatureStore(IStoreProvider storeProvider) : this(storeProvider, new ToggleDeserializer(), new FeatureConfiguration()) + { + } + + public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer) : this(storeProvider, toggleDeserializer, new FeatureConfiguration()) + { + } + + public FeatureStore(IStoreProvider storeProvider, IToggleDeserializer toggleDeserializer, FeatureConfiguration configuration) + { + this.storeProvider = storeProvider; + Configuration = configuration; + this.toggleDeserializer = toggleDeserializer; + } + + public IEnumerable FindStartsWith(string key) + { + return GetAll().Where(x => x.Name.Value.StartsWith(key, StringComparison.OrdinalIgnoreCase)); + } + + public IEnumerable GetAll() + { + var features = storeProvider.Get(); + if (features == null || !features.Any()) return Enumerable.Empty(); + + var result = new List(); + + foreach (var feature in features) + { + try + { + result.Add(new Feature(feature.Name, toggleDeserializer.Deserializer(feature.Toggle))); + } + catch (Exception ex) + { + Configuration.Logger?.Error($"Action='Failed to Deserialize', Feature='{feature.Name}', Exception='{ex}'."); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Stores/IStoreProvider.cs b/src/FeatureOne/Core/Stores/IStoreProvider.cs new file mode 100644 index 0000000..4d0f84c --- /dev/null +++ b/src/FeatureOne/Core/Stores/IStoreProvider.cs @@ -0,0 +1,44 @@ +namespace FeatureOne.Core.Stores +{ + /// + /// Interface to implement feature store provider. + /// + public interface IStoreProvider + { + /// + /// Implement this method to return all features from store provider. + /// + /// + /// Example: + /// Name - Feature-01 + /// Toggle - Json string as + /// { + /// "operator":"all", + /// "conditions":[{ + /// "type":"Regex", + /// "claim":"email", + /// "expression":"*@gbk.com" + /// }, + /// { + /// "type":"RegexCondition", + /// "claim":"user_role", + /// "expression":"^administrator$" + /// }] + /// } + /// + /// + FeatureRecord[] Get(); + } + + public class FeatureRecord + { + public FeatureRecord(string name, string toggle) + { + Name = name; + Toggle = toggle; + } + + public string Name { get; set; } + public string Toggle { get; set; } + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Stores/IToggleDeserializer.cs b/src/FeatureOne/Core/Stores/IToggleDeserializer.cs new file mode 100644 index 0000000..71eb766 --- /dev/null +++ b/src/FeatureOne/Core/Stores/IToggleDeserializer.cs @@ -0,0 +1,7 @@ +namespace FeatureOne.Core.Stores +{ + public interface IToggleDeserializer + { + IToggle Deserializer(string input); + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Stores/ToggleDeserializer.cs b/src/FeatureOne/Core/Stores/ToggleDeserializer.cs new file mode 100644 index 0000000..ca2c4fd --- /dev/null +++ b/src/FeatureOne/Core/Stores/ToggleDeserializer.cs @@ -0,0 +1,22 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace FeatureOne.Core.Stores +{ + public class ToggleDeserializer : IToggleDeserializer + { + public IToggle Deserializer(string input) + { + var jObject = JsonNode.Parse(input); + + var toggleOperator = jObject["operator"]?.ToString() ?? Operator.Any.ToString(); + var toggleConditions = jObject["conditions"].Deserialize(); + + return new Toggle + ( + Enum.TryParse(toggleOperator, true, out var @operator) ? @operator : Operator.Any, + ConditionFactory.Create(toggleConditions) + ); + } + } +} \ No newline at end of file diff --git a/src/FeatureOne/Core/Toggle.cs b/src/FeatureOne/Core/Toggle.cs new file mode 100644 index 0000000..4c1a59b --- /dev/null +++ b/src/FeatureOne/Core/Toggle.cs @@ -0,0 +1,24 @@ +namespace FeatureOne.Core +{ + public class Toggle : IToggle + { + public Toggle(Operator @operator, ICondition[] conditions) + { + Operator = @operator; + Conditions = conditions; + } + + public Operator Operator { get; set; } + public ICondition[] Conditions { get; set; } + + public bool Run(IDictionary claims) + { + if (Conditions == null) + return false; + + return Operator == Operator.Any + ? Conditions.Any(x => x.Evaluate(claims)) + : Conditions.All(x => x.Evaluate(claims)); + } + } +} \ No newline at end of file diff --git a/src/FeatureOn/Toggles/Conditions/RegexCondition.cs b/src/FeatureOne/Core/Toggles/Conditions/RegexCondition.cs similarity index 79% rename from src/FeatureOn/Toggles/Conditions/RegexCondition.cs rename to src/FeatureOne/Core/Toggles/Conditions/RegexCondition.cs index 4fa5d6b..9c8a584 100644 --- a/src/FeatureOn/Toggles/Conditions/RegexCondition.cs +++ b/src/FeatureOne/Core/Toggles/Conditions/RegexCondition.cs @@ -1,16 +1,17 @@ -using Ninja.FeatureIt; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; -namespace FeatureOn.Toggle.Conditions +namespace FeatureOne.Core.Toggles.Conditions { - public class RegexCondition : IToggleCondition + public class RegexCondition : ICondition { public string Claim { get; set; } public string Expression { get; set; } + public bool Evaluate(IDictionary claims) { if (claims == null) return false; + if (!claims.Any(x => x.Key != null && x.Key.Equals(Claim))) return false; diff --git a/src/FeatureOn/Toggles/Conditions/SimpleCondition.cs b/src/FeatureOne/Core/Toggles/Conditions/SimpleCondition.cs similarity index 64% rename from src/FeatureOn/Toggles/Conditions/SimpleCondition.cs rename to src/FeatureOne/Core/Toggles/Conditions/SimpleCondition.cs index 11abf36..83e536a 100644 --- a/src/FeatureOn/Toggles/Conditions/SimpleCondition.cs +++ b/src/FeatureOne/Core/Toggles/Conditions/SimpleCondition.cs @@ -1,8 +1,9 @@ -namespace FeatureOn.Toggle.Conditions +namespace FeatureOne.Core.Toggles.Conditions { - public class SimpleCondition : IToggleCondition + public class SimpleCondition : ICondition { public bool IsEnabled { get; set; } + public bool Evaluate(IDictionary claims) { return IsEnabled; diff --git a/src/FeatureOn/Configuration/FeatureConfiguration.cs b/src/FeatureOne/FeatureConfiguration.cs similarity index 80% rename from src/FeatureOn/Configuration/FeatureConfiguration.cs rename to src/FeatureOne/FeatureConfiguration.cs index 004dab7..c019d09 100644 --- a/src/FeatureOn/Configuration/FeatureConfiguration.cs +++ b/src/FeatureOne/FeatureConfiguration.cs @@ -1,6 +1,4 @@ -using FeatureOn.Logger; - -namespace FeatureOn.Configuration +namespace FeatureOne { public class FeatureConfiguration { diff --git a/src/FeatureOn/FeatureName.cs b/src/FeatureOne/FeatureName.cs similarity index 94% rename from src/FeatureOn/FeatureName.cs rename to src/FeatureOne/FeatureName.cs index 6dbcc35..afe6768 100644 --- a/src/FeatureOn/FeatureName.cs +++ b/src/FeatureOne/FeatureName.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Ninja.FeatureIt +namespace FeatureOne { public class FeatureName { @@ -13,8 +13,9 @@ public FeatureName(string name) Value = name; } + public string Value { get; } - + public static implicit operator string(FeatureName s) => s?.Value; } } \ No newline at end of file diff --git a/src/FeatureOn/FeatureOn.csproj b/src/FeatureOne/FeatureOne.csproj similarity index 78% rename from src/FeatureOn/FeatureOn.csproj rename to src/FeatureOne/FeatureOne.csproj index 17be871..3302866 100644 --- a/src/FeatureOn/FeatureOn.csproj +++ b/src/FeatureOne/FeatureOne.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -9,19 +9,18 @@ AssemblyInfo.cs true true - Ninja.FeatureOn - FeatureOn - FeatureOn + FeatureOne + FeatureOne False snupkg - FeatureOn + FeatureOne najaf.shayk Ninja.Sha!4H - FeatureOn + FeatureOne Library to provide feature toggles for projects. 2023 README.md - https://github.com/NinjaRocks/FeatureOn + https://github.com/NinjaRocks/FeatureOne git feature-toggle; feature toggle; .net6.0; featureOn 1.0.0 @@ -29,7 +28,7 @@ - + diff --git a/src/FeatureOn/Features.cs b/src/FeatureOne/Features.cs similarity index 78% rename from src/FeatureOn/Features.cs rename to src/FeatureOne/Features.cs index 01f4c57..1045013 100644 --- a/src/FeatureOn/Features.cs +++ b/src/FeatureOne/Features.cs @@ -1,13 +1,12 @@ using System.Security.Claims; -using FeatureOn.Configuration; -using FeatureOn.Logger; -using FeatureOn.Stores; -namespace Ninja.FeatureIt +namespace FeatureOne { + /// + /// Class to enable checking if a feature is enabled + /// public class Features { - private readonly IFeatureStore featureStore; private readonly FeatureConfiguration Configuration; public static Features Current { get; private set; } @@ -16,42 +15,43 @@ public class Features { UseCache = true, Logger = new NullLogger() - }){ } + }) + { } public Features(IFeatureStore featureStore, FeatureConfiguration configuration) { this.featureStore = featureStore; - this.Configuration = configuration; + this.Configuration = configuration; } - public static void Initialise(Func factory) => Current = factory(); - /// /// Determines whether the feature is enabled for given claims principal. /// - /// feature name + /// feature name /// Claims Principal cliams /// public bool IsEnabled(string name, ClaimsPrincipal principal) { - return IsEnabled(name, principal.Claims.ToDictionary(k => k.Type, v => v.Value)); + return IsEnabled(name, principal.Claims); } + /// /// Determines whether the feature is enabled for given set of user claims /// - /// feature name + /// feature name /// user claims /// public bool IsEnabled(string name, IEnumerable claims) { return IsEnabled(name, claims.ToDictionary(k => k.Type, v => v.Value)); } + /// /// Determines whether the feature is enabled for given set of user claims /// - /// feature name + /// feature name /// user claims /// public bool IsEnabled(string name, IDictionary claims) @@ -59,7 +59,7 @@ public bool IsEnabled(string name, IDictionary claims) try { if (claims == null) - return false; + return false; var featureName = new FeatureName(name); @@ -76,11 +76,9 @@ public bool IsEnabled(string name, IDictionary claims) } catch (Exception ex) { - Configuration.Logger?.Warn($"Action='IsEnabled', Feature='{name}', Exception='{ex}'."); - + Configuration.Logger?.Error($"Action='Features.IsEnabled', Feature='{name}', Exception='{ex}'."); return false; } } - } } \ No newline at end of file diff --git a/src/FeatureOn/IFeature.cs b/src/FeatureOne/IFeature.cs similarity index 60% rename from src/FeatureOn/IFeature.cs rename to src/FeatureOne/IFeature.cs index c610b30..ab825ca 100644 --- a/src/FeatureOn/IFeature.cs +++ b/src/FeatureOne/IFeature.cs @@ -1,11 +1,12 @@ -using FeatureOn.Toggle; +using FeatureOne.Core; -namespace Ninja.FeatureIt +namespace FeatureOne { public interface IFeature { FeatureName Name { get; } - IFeatureToggle Toggle { get; } + IToggle Toggle { get; } + bool IsEnabled(IDictionary claims); } } \ No newline at end of file diff --git a/src/FeatureOn/Logger/IFeatureLogger.cs b/src/FeatureOne/IFeatureLogger.cs similarity index 86% rename from src/FeatureOn/Logger/IFeatureLogger.cs rename to src/FeatureOne/IFeatureLogger.cs index 7d2ef38..b3543c0 100644 --- a/src/FeatureOn/Logger/IFeatureLogger.cs +++ b/src/FeatureOne/IFeatureLogger.cs @@ -1,10 +1,13 @@ -namespace FeatureOn.Logger +namespace FeatureOne { public interface IFeatureLogger { void Debug(string message); + void Error(string message); + void Info(string message); + void Warn(string message); } } \ No newline at end of file diff --git a/src/FeatureOn/Stores/IFeatureStore.cs b/src/FeatureOne/IFeatureStore.cs similarity index 73% rename from src/FeatureOn/Stores/IFeatureStore.cs rename to src/FeatureOne/IFeatureStore.cs index 2457d76..cbaf0ea 100644 --- a/src/FeatureOn/Stores/IFeatureStore.cs +++ b/src/FeatureOne/IFeatureStore.cs @@ -1,10 +1,9 @@ -using Ninja.FeatureIt; - -namespace FeatureOn.Stores +namespace FeatureOne { public interface IFeatureStore { IEnumerable FindStartsWith(string key); + IEnumerable GetAll(); } } \ No newline at end of file diff --git a/src/FeatureOne/LambdaComparer.cs b/src/FeatureOne/LambdaComparer.cs new file mode 100644 index 0000000..181d060 --- /dev/null +++ b/src/FeatureOne/LambdaComparer.cs @@ -0,0 +1,22 @@ +namespace FeatureOne +{ + public class LambdaComparer : IEqualityComparer + { + private Func equalityFunction; + + public LambdaComparer(Func equalityFunction) + { + this.equalityFunction = equalityFunction ?? throw new ArgumentNullException(); + } + + public bool Equals(T x, T y) + { + return equalityFunction(x, y); + } + + public int GetHashCode(T obj) + { + return obj.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/FeatureOne/NullLogger.cs b/src/FeatureOne/NullLogger.cs new file mode 100644 index 0000000..5ad35c5 --- /dev/null +++ b/src/FeatureOne/NullLogger.cs @@ -0,0 +1,17 @@ +namespace FeatureOne +{ + public class NullLogger : IFeatureLogger + { + public void Info(string message) + { } + + public void Debug(string message) + { } + + public void Warn(string message) + { } + + public void Error(string message) + { } + } +} \ No newline at end of file diff --git a/test/FeatureOn.Tests/FeatureNameTest.cs b/test/FeatureOne.Tests/FeatureNameTest.cs similarity index 92% rename from test/FeatureOn.Tests/FeatureNameTest.cs rename to test/FeatureOne.Tests/FeatureNameTest.cs index afe5301..8250888 100644 --- a/test/FeatureOn.Tests/FeatureNameTest.cs +++ b/test/FeatureOne.Tests/FeatureNameTest.cs @@ -1,6 +1,4 @@ -using Ninja.FeatureIt; - -namespace FeatureIt.Test +namespace FeatureOne.Test { [TestFixture] public class FeatureNameTest @@ -25,6 +23,5 @@ public void TestNameForUnsupportedInputs(string input) { Assert.Throws(() => new FeatureName(input)); } - } } \ No newline at end of file diff --git a/test/FeatureOn.Tests/FeatureOn.Tests.csproj b/test/FeatureOne.Tests/FeatureOne.Tests.csproj similarity index 83% rename from test/FeatureOn.Tests/FeatureOn.Tests.csproj rename to test/FeatureOne.Tests/FeatureOne.Tests.csproj index e917144..7a7aa3e 100644 --- a/test/FeatureOn.Tests/FeatureOn.Tests.csproj +++ b/test/FeatureOne.Tests/FeatureOne.Tests.csproj @@ -10,6 +10,7 @@ + @@ -17,7 +18,7 @@ - + diff --git a/test/FeatureOn.Tests/FeatureTest.cs b/test/FeatureOne.Tests/FeatureTest.cs similarity index 74% rename from test/FeatureOn.Tests/FeatureTest.cs rename to test/FeatureOne.Tests/FeatureTest.cs index 3292678..5efb569 100644 --- a/test/FeatureOn.Tests/FeatureTest.cs +++ b/test/FeatureOne.Tests/FeatureTest.cs @@ -1,8 +1,8 @@ -using FeatureOn; -using FeatureOn.Toggle.Conditions; -using FeatureOn.Toggles; +using FeatureOne.Core; +using FeatureOne.Core.Toggles.Conditions; +using Moq; -namespace FeatureIt.Test +namespace FeatureOne.Test { [TestFixture] public sealed class FeatureTest @@ -15,12 +15,11 @@ public void Setup() claims = new Dictionary(); } - [Test] public void TestFeatureForCorrectNameAndToggleCondition() { - var feature = new Feature("feat-01", - new FeatureToggle(FeatureOn.Toggle.ToggleOperator.All, + var feature = new Feature("feat-01", + new Toggle(Operator.All, new[] { new RegexCondition { Claim = "email", Expression = "1234@ioHub.com" } })); Assert.IsInstanceOf(feature.Toggle.Conditions[0]); @@ -31,7 +30,6 @@ public void TestFeatureForCorrectNameAndToggleCondition() [TestCase(false, true, false)] [TestCase(true, false, false)] [TestCase(false, false, false)] - public void TestFeatureForAllOperatorWhenEvaluatingMultileToggleCondition(bool condition1, bool condition2, bool expected) { claims.Clear(); @@ -39,14 +37,14 @@ public void TestFeatureForAllOperatorWhenEvaluatingMultileToggleCondition(bool c claims.Add("User_role", "administration"); var feature = new Feature("feat-01", - new FeatureToggle(FeatureOn.Toggle.ToggleOperator.All, - new[] { + new Toggle(Operator.All, + new[] { new SimpleCondition { IsEnabled = condition1}, new SimpleCondition { IsEnabled = condition2 } })); var isEnabled = feature.IsEnabled(claims); - + Assert.That(feature.Name.Value, Is.EqualTo("feat-01")); Assert.That(isEnabled, Is.EqualTo(expected)); } @@ -62,7 +60,7 @@ public void TestFeatureForAnyOperatorWhenEvaluatingMultileToggleCondition(bool c claims.Add("User_role", "administration"); var feature = new Feature("feat-01", - new FeatureToggle(FeatureOn.Toggle.ToggleOperator.Any, + new Toggle(Operator.Any, new[] { new SimpleCondition { IsEnabled = condition1}, new SimpleCondition { IsEnabled = condition2 } @@ -73,5 +71,23 @@ public void TestFeatureForAnyOperatorWhenEvaluatingMultileToggleCondition(bool c Assert.That(feature.Name.Value, Is.EqualTo("feat-01")); Assert.That(isEnabled, Is.EqualTo(expected)); } + + [Test] + public void TestForToggleRun() + { + var toggle = new Mock(); + + var name = "Ninja"; + var feature = new Feature(name, toggle.Object); + + var claims = new Dictionary(); + + feature.IsEnabled(claims); + + Assert.That(feature.Name.Value, Is.EqualTo(name)); + Assert.That(feature.Toggle, Is.EqualTo(toggle.Object)); + + toggle.Verify(x => x.Run(claims)); + } } -} +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/FeaturesTests.cs b/test/FeatureOne.Tests/FeaturesTests.cs new file mode 100644 index 0000000..16fff81 --- /dev/null +++ b/test/FeatureOne.Tests/FeaturesTests.cs @@ -0,0 +1,87 @@ + +using FeatureOne.Core; +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace FeatureOne.Tests +{ + public class FeaturesTests + { + private Mock store; + private Mock logger; + private Mock feature; + private Features features; + private string featureName; + private ClaimsPrincipal principal; + + [SetUp] + public void Setup() + { + featureName = "Feature-01"; + store = new Mock(); + logger = new Mock(); + feature = new Mock(); + + feature.Setup(x => x.Name).Returns(new FeatureName(featureName)); + feature.Setup(x => x.IsEnabled(It.IsAny>())).Returns(true); + + store.Setup(x => x.FindStartsWith(It.IsAny())).Returns(new[] { feature.Object }); + + features = new Features(store.Object, new FeatureConfiguration + { + Logger = logger.Object, + SlidingExpiry = TimeSpan.FromSeconds(10), + UseCache = true + }); + + principal = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("user", "ninja") + })); + } + + [Test] + public void TestIsEnabledWithClaimsWhenFeatureExistsAsEnabledRetureFeatureIsEnabled() + { + var claims = new List(); + claims.Add(new Claim("user", "ninja")); + + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims)); + var output = features.IsEnabled(featureName, principal); + + Assert.That(output, Is.EqualTo(true)); + + store.Verify(x => x.FindStartsWith(featureName)); + feature.Verify(x => x.IsEnabled(It.IsAny>())); + } + + [Test] + public void TestIsEnabledWithClaimsWhenFeatureDoesNotExistsReturnFalse() + { + featureName = "non-existing-feature"; + var output = features.IsEnabled(featureName, principal); + + Assert.That(output, Is.EqualTo(false)); + + store.Verify(x => x.FindStartsWith(featureName)); + } + + [Test] + public void TestIsEnabledWithExceptionLogErrorReturnFalse() + { + featureName = "errored-feature"; + store.Setup(x => x.FindStartsWith(featureName)).Throws(); + + var output = features.IsEnabled(featureName, principal); + + Assert.That(output, Is.EqualTo(false)); + + logger.Verify(x => x.Error(It.Is(msg=> msg.Contains(featureName)))); + } + } +} diff --git a/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs b/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs new file mode 100644 index 0000000..a483e2c --- /dev/null +++ b/test/FeatureOne.Tests/Stores/FeatureStoreTests.cs @@ -0,0 +1,95 @@ +using FeatureOne.Core; +using FeatureOne.Core.Stores; +using FeatureOne.Core.Toggles.Conditions; +using Moq; +using NUnit.Framework.Internal; + +namespace FeatureOne.Tests.Stores +{ + [TestFixture] + internal class FeatureStoreTests + { + private Mock storeProvider; + private FeatureStore featureStore; + private Mock logger; + + [SetUp] + public void Setup() + { + logger = new Mock(); + storeProvider = new Mock(); + storeProvider.Setup(x => x.Get()) + .Returns(new[] + { + new FeatureRecord("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"), + new FeatureRecord("feature-02", "{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}") + }); + + featureStore = new FeatureStore(storeProvider.Object, new ToggleDeserializer(), new FeatureConfiguration + { + Logger = logger.Object + }); + } + + [Test] + public void TestGetAllToReturnCorrectFeaturesConfiguredStoreInProvider() + { + var features = featureStore.GetAll(); + + Assert.That(features.Count(), Is.EqualTo(2)); + + var feature01 = features.First(x => x.Name.Value == "feature-01"); + Assert.That(feature01.Toggle.Operator, Is.EqualTo(Operator.Any)); + Assert.That(feature01.Toggle.Conditions.Length, Is.EqualTo(1)); + + Assert.Multiple(() => + { + Assert.IsInstanceOf(feature01.Toggle.Conditions[0]); + Assert.That(((SimpleCondition)feature01.Toggle.Conditions[0]).IsEnabled, Is.EqualTo(true)); + }); + + var feature02 = features.First(x => x.Name.Value == "feature-02"); + Assert.That(feature02.Toggle.Operator, Is.EqualTo(Operator.All)); + Assert.That(feature02.Toggle.Conditions.Length, Is.EqualTo(2)); + + Assert.Multiple(() => + { + Assert.IsInstanceOf(feature02.Toggle.Conditions[0]); + Assert.That(((SimpleCondition)feature02.Toggle.Conditions[0]).IsEnabled, Is.EqualTo(false)); + }); + Assert.Multiple(() => + { + Assert.IsInstanceOf(feature02.Toggle.Conditions[1]); + Assert.That(((RegexCondition)feature02.Toggle.Conditions[1]).Claim, Is.EqualTo("email")); + Assert.That(((RegexCondition)feature02.Toggle.Conditions[1]).Expression, Is.EqualTo("*@gbk.com")); + }); + } + + [Test] + public void TestGetAllToReturnAnyDeserializedFeaturesInStoreProvideAndLogErrorsForFailures() + { + storeProvider.Setup(x => x.Get()) + .Returns(new[] + { + new FeatureRecord("feature-01", "{\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": true}]}"), + new FeatureRecord("feature-02", "Invalid Toggle String") + }); + + var features = featureStore.GetAll(); + + Assert.That(features.Count(), Is.EqualTo(1)); + + var feature01 = features.First(x => x.Name.Value == "feature-01"); + Assert.That(feature01.Toggle.Operator, Is.EqualTo(Operator.Any)); + Assert.That(feature01.Toggle.Conditions.Length, Is.EqualTo(1)); + + Assert.Multiple(() => + { + Assert.IsInstanceOf(feature01.Toggle.Conditions[0]); + Assert.That(((SimpleCondition)feature01.Toggle.Conditions[0]).IsEnabled, Is.EqualTo(true)); + }); + + logger.Verify(x => x.Error(It.Is(msg => msg.Contains("feature-02"))), Times.Once()); + } + } +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs b/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs new file mode 100644 index 0000000..181c689 --- /dev/null +++ b/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs @@ -0,0 +1,36 @@ +using FeatureOne.Core; +using FeatureOne.Core.Stores; +using FeatureOne.Core.Toggles.Conditions; + +namespace FeatureOne.Tests.Stores +{ + [TestFixture] + internal class ToggleDeserializerTests + { + [Test] + public void TestDeSerialize() + { + var deserializer = new ToggleDeserializer(); + + var toggle = deserializer + .Deserializer("{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}"); + + + Assert.That(toggle.Operator, Is.EqualTo(Operator.All)); + Assert.That(toggle.Conditions.Length, Is.EqualTo(2)); + + Assert.Multiple(() => + { + Assert.IsInstanceOf(toggle.Conditions[0]); + Assert.That(((SimpleCondition)toggle.Conditions[0]).IsEnabled, Is.EqualTo(false)); + }); + Assert.Multiple(() => + { + Assert.IsInstanceOf(toggle.Conditions[1]); + Assert.That(((RegexCondition)toggle.Conditions[1]).Claim, Is.EqualTo("email")); + Assert.That(((RegexCondition)toggle.Conditions[1]).Expression, Is.EqualTo("*@gbk.com")); + }); + + } + } +} diff --git a/test/FeatureOne.Tests/ToggleTests.cs b/test/FeatureOne.Tests/ToggleTests.cs new file mode 100644 index 0000000..13f1b94 --- /dev/null +++ b/test/FeatureOne.Tests/ToggleTests.cs @@ -0,0 +1,25 @@ +using FeatureOne.Core; +using Moq; + +namespace FeatureOne.Test +{ + [TestFixture] + public class ToggleTests + { + [Test] public void TestToggle() + { + var condition = new Mock(); + var toggle = new Toggle(Operator.All, new[] { condition.Object }); + + var claims = new Dictionary(); + claims.Add("user", "ninja"); + + toggle.Run(claims); + + Assert.That(toggle.Operator, Is.EqualTo(Operator.All)); + Assert.That(toggle.Conditions[0], Is.EqualTo(condition.Object)); + + condition.Verify(x => x.Evaluate(claims)); + } + } +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs b/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs new file mode 100644 index 0000000..17df26c --- /dev/null +++ b/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs @@ -0,0 +1,22 @@ +using FeatureOne.Core; +namespace FeatureOne.Tests.Toggles +{ + [TestFixture] + public class NamePostFixTest + { + [TestCase("Regex", "RegexCondition")] + [TestCase("Simple", "SimpleCondition")] + [TestCase("RegexCondition", "RegexCondition")] + [TestCase("SimpleCondition", "SimpleCondition")] + public void TestNameForSupportedInputs(string input, string output) + { + Assert.That(new NamePostFix(input, "Condition").Name, Is.EqualTo(output)); + } + + [TestCase(null, null)] + public void TestNameForNullInputs(string input, string postfix) + { + Assert.Throws(() => new NamePostFix(input, postfix)); + } + } +} \ No newline at end of file diff --git a/test/FeatureOn.Tests/Toggles/RegexConditionTest.cs b/test/FeatureOne.Tests/Toggles/RegexConditionTest.cs similarity index 86% rename from test/FeatureOn.Tests/Toggles/RegexConditionTest.cs rename to test/FeatureOne.Tests/Toggles/RegexConditionTest.cs index 37e9d1e..e17624d 100644 --- a/test/FeatureOn.Tests/Toggles/RegexConditionTest.cs +++ b/test/FeatureOne.Tests/Toggles/RegexConditionTest.cs @@ -1,6 +1,6 @@ -using FeatureOn.Toggle.Conditions; +using FeatureOne.Core.Toggles.Conditions; -namespace FeatureOn.Test.Toggles +namespace FeatureOne.Test.Toggles { [TestFixture] public sealed class RegexConditionTest @@ -18,7 +18,6 @@ public void EvaluateToggleToFalseWhenNoCliamFound() Assert.IsFalse(condition.Evaluate(claims)); } - public void EvaluateToggleConditionToTrueOnMatchIsHit() { claims.Add("email", "kl12.sha123@ninja.com"); @@ -29,10 +28,9 @@ public void EvaluateToggleConditionToTrueOnMatchIsHit() public void EvaluateToggleConditionToFalseOnMatchIsMiss() { claims.Add("email", "kl12.sha123@yahoo.com"); - var condition = new RegexCondition{ Claim = "email", Expression = GmailDotCom }; + var condition = new RegexCondition { Claim = "email", Expression = GmailDotCom }; Assert.That(condition.Evaluate(claims), Is.Not.EqualTo(false)); } - } -} +} \ No newline at end of file diff --git a/test/FeatureOn.Tests/Toggles/SimpleConditionTest.cs b/test/FeatureOne.Tests/Toggles/SimpleConditionTest.cs similarity index 64% rename from test/FeatureOn.Tests/Toggles/SimpleConditionTest.cs rename to test/FeatureOne.Tests/Toggles/SimpleConditionTest.cs index ef14d82..d4f5a0b 100644 --- a/test/FeatureOn.Tests/Toggles/SimpleConditionTest.cs +++ b/test/FeatureOne.Tests/Toggles/SimpleConditionTest.cs @@ -1,6 +1,6 @@ -using FeatureOn.Toggle.Conditions; +using FeatureOne.Core.Toggles.Conditions; -namespace FeatureOn.Test.Toggles +namespace FeatureOne.Test.Toggles { [TestFixture] public sealed class SimpleConditionTest @@ -9,8 +9,8 @@ public sealed class SimpleConditionTest [TestCase(false)] public void Evaluate_returns_IsEnabled(bool isEnabled) { - var toggle = new SimpleCondition { IsEnabled= isEnabled }; + var toggle = new SimpleCondition { IsEnabled = isEnabled }; Assert.That(toggle.Evaluate(null), Is.EqualTo(isEnabled)); } } -} +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs b/test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs new file mode 100644 index 0000000..e872c45 --- /dev/null +++ b/test/FeatureOne.Tests/Toggles/ToggleConditionFactoryTest.cs @@ -0,0 +1,39 @@ +using FeatureOne.Core.Stores; +using FeatureOne.Core.Toggles.Conditions; +using System.Text.Json.Nodes; + +namespace FeatureOne.Test.Toggles +{ + [TestFixture] + public sealed class ToggleConditionFactoryTest + { + [Test] + public void TestToggleConditionForNUllInput() + { + JsonObject jObj = null; + Assert.Throws(() => ConditionFactory.Create(jObj)); + } + + [Test] + public void TestToggleConditionForCorrectSimpleInstanceType() + { + var json = "{\r\n\t\t\t \"type\":\"Simple\",\r\n\t\t\t \"IsEnabled\":\"true\"\r\n\t\t}"; + + var jobject = JsonObject.Parse(json)?.AsObject(); + var toggleCondition = ConditionFactory.Create(jobject); + + Assert.IsInstanceOf(typeof(SimpleCondition), toggleCondition); + } + + [Test] + public void TestToggleConditionForCorrectRegexInstanceType() + { + var json = "{\r\n\t\t\t \"type\":\"RegexCondition\",\r\n\t\t\t \"claim\":\"email\",\r\n\t\t\t \"expression\":\"*@gbk.com\"\r\n\t\t }"; + + var jobject = JsonObject.Parse(json)?.AsObject(); + var toggleCondition = ConditionFactory.Create(jobject); + + Assert.IsInstanceOf(typeof(RegexCondition), toggleCondition); + } + } +} \ No newline at end of file diff --git a/test/FeatureOn.Tests/Usings.cs b/test/FeatureOne.Tests/Usings.cs similarity index 100% rename from test/FeatureOn.Tests/Usings.cs rename to test/FeatureOne.Tests/Usings.cs From 0fac433d11fe742f1c7f827ddf2b14ac3f60d8a5 Mon Sep 17 00:00:00 2001 From: Ninja Date: Wed, 7 Dec 2022 19:39:56 +0000 Subject: [PATCH 2/2] - Fix lint --- src/FeatureOne/Core/NamePostFix.cs | 2 +- src/FeatureOne/Core/Stores/FeatureStore.cs | 3 +-- src/FeatureOne/Core/Stores/IStoreProvider.cs | 4 ++-- test/FeatureOne.Tests/FeaturesTests.cs | 17 +++++------------ .../Stores/ToggleDeserializerTests.cs | 6 ++---- test/FeatureOne.Tests/ToggleTests.cs | 7 ++++--- .../FeatureOne.Tests/Toggles/NamePostFixTest.cs | 1 + 7 files changed, 16 insertions(+), 24 deletions(-) diff --git a/src/FeatureOne/Core/NamePostFix.cs b/src/FeatureOne/Core/NamePostFix.cs index f2fceab..6197540 100644 --- a/src/FeatureOne/Core/NamePostFix.cs +++ b/src/FeatureOne/Core/NamePostFix.cs @@ -3,7 +3,7 @@ public class NamePostFix { public string Name { get; private set; } - + public NamePostFix(string name, string postFix) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); diff --git a/src/FeatureOne/Core/Stores/FeatureStore.cs b/src/FeatureOne/Core/Stores/FeatureStore.cs index 1109875..7b482d3 100644 --- a/src/FeatureOne/Core/Stores/FeatureStore.cs +++ b/src/FeatureOne/Core/Stores/FeatureStore.cs @@ -1,5 +1,4 @@ - -namespace FeatureOne.Core.Stores +namespace FeatureOne.Core.Stores { public class FeatureStore : IFeatureStore { diff --git a/src/FeatureOne/Core/Stores/IStoreProvider.cs b/src/FeatureOne/Core/Stores/IStoreProvider.cs index 4d0f84c..0fba2bf 100644 --- a/src/FeatureOne/Core/Stores/IStoreProvider.cs +++ b/src/FeatureOne/Core/Stores/IStoreProvider.cs @@ -11,7 +11,7 @@ public interface IStoreProvider /// /// Example: /// Name - Feature-01 - /// Toggle - Json string as + /// Toggle - Json string as /// { /// "operator":"all", /// "conditions":[{ @@ -23,7 +23,7 @@ public interface IStoreProvider /// "type":"RegexCondition", /// "claim":"user_role", /// "expression":"^administrator$" - /// }] + /// }] /// } /// /// diff --git a/test/FeatureOne.Tests/FeaturesTests.cs b/test/FeatureOne.Tests/FeaturesTests.cs index 16fff81..818ede1 100644 --- a/test/FeatureOne.Tests/FeaturesTests.cs +++ b/test/FeatureOne.Tests/FeaturesTests.cs @@ -1,12 +1,5 @@ - -using FeatureOne.Core; -using Moq; -using System; -using System.Collections.Generic; -using System.Linq; +using Moq; using System.Security.Claims; -using System.Text; -using System.Threading.Tasks; namespace FeatureOne.Tests { @@ -28,7 +21,7 @@ public void Setup() feature = new Mock(); feature.Setup(x => x.Name).Returns(new FeatureName(featureName)); - feature.Setup(x => x.IsEnabled(It.IsAny>())).Returns(true); + feature.Setup(x => x.IsEnabled(It.IsAny>())).Returns(true); store.Setup(x => x.FindStartsWith(It.IsAny())).Returns(new[] { feature.Object }); @@ -55,7 +48,7 @@ public void TestIsEnabledWithClaimsWhenFeatureExistsAsEnabledRetureFeatureIsEnab var output = features.IsEnabled(featureName, principal); Assert.That(output, Is.EqualTo(true)); - + store.Verify(x => x.FindStartsWith(featureName)); feature.Verify(x => x.IsEnabled(It.IsAny>())); } @@ -81,7 +74,7 @@ public void TestIsEnabledWithExceptionLogErrorReturnFalse() Assert.That(output, Is.EqualTo(false)); - logger.Verify(x => x.Error(It.Is(msg=> msg.Contains(featureName)))); + logger.Verify(x => x.Error(It.Is(msg => msg.Contains(featureName)))); } } -} +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs b/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs index 181c689..a86e817 100644 --- a/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs +++ b/test/FeatureOne.Tests/Stores/ToggleDeserializerTests.cs @@ -6,7 +6,7 @@ namespace FeatureOne.Tests.Stores { [TestFixture] internal class ToggleDeserializerTests - { + { [Test] public void TestDeSerialize() { @@ -15,7 +15,6 @@ public void TestDeSerialize() var toggle = deserializer .Deserializer("{\"operator\":\"all\",\"conditions\":[{\"type\":\"Simple\",\"isEnabled\": false}, {\"type\":\"RegexCondition\",\"claim\":\"email\",\"expression\":\"*@gbk.com\"}]}"); - Assert.That(toggle.Operator, Is.EqualTo(Operator.All)); Assert.That(toggle.Conditions.Length, Is.EqualTo(2)); @@ -30,7 +29,6 @@ public void TestDeSerialize() Assert.That(((RegexCondition)toggle.Conditions[1]).Claim, Is.EqualTo("email")); Assert.That(((RegexCondition)toggle.Conditions[1]).Expression, Is.EqualTo("*@gbk.com")); }); - } } -} +} \ No newline at end of file diff --git a/test/FeatureOne.Tests/ToggleTests.cs b/test/FeatureOne.Tests/ToggleTests.cs index 13f1b94..6f2bc53 100644 --- a/test/FeatureOne.Tests/ToggleTests.cs +++ b/test/FeatureOne.Tests/ToggleTests.cs @@ -6,14 +6,15 @@ namespace FeatureOne.Test [TestFixture] public class ToggleTests { - [Test] public void TestToggle() + [Test] + public void TestToggle() { var condition = new Mock(); var toggle = new Toggle(Operator.All, new[] { condition.Object }); - + var claims = new Dictionary(); claims.Add("user", "ninja"); - + toggle.Run(claims); Assert.That(toggle.Operator, Is.EqualTo(Operator.All)); diff --git a/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs b/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs index 17df26c..e38f6dd 100644 --- a/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs +++ b/test/FeatureOne.Tests/Toggles/NamePostFixTest.cs @@ -1,4 +1,5 @@ using FeatureOne.Core; + namespace FeatureOne.Tests.Toggles { [TestFixture]