From 8e96e7b81b5dd44b71029b0fd326746479c14c59 Mon Sep 17 00:00:00 2001 From: Kirill Polishchuk Date: Tue, 1 Jul 2025 16:43:56 +1200 Subject: [PATCH 1/2] Added new methods --- README.md | 29 +++++++ .../PasswordGeneratorTests.cs | 85 +++++++++++++++++++ src/KPasswordGenerator/PasswordGenerator.cs | 26 ++++++ 3 files changed, 140 insertions(+) diff --git a/README.md b/README.md index ad18632..d8d9547 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,35 @@ string password = generator.Generate(16); Console.WriteLine(password); // Example output: kAj79uV@E?m7_8eS ``` +## Additional Methods + +### `GenerateRandomLength(int minPasswordLength, int maxPasswordLength)` + +Generates a password of **random length within a specified range**. + +- Ensures the generated password meets all defined character requirements. +- Uses secure randomness to choose the length. + +**Example:** +```csharp +string password = generator.GenerateRandomLength(12, 20); +``` + +--- + +### `Validate(string password)` + +Validates whether a password meets the current `PasswordSettings` policy. + +- Checks overall length. +- Verifies that the required number of characters from each character pool are present. +- Returns `true` if all rules pass, otherwise `false`. + +- **Example:** +```csharp +bool isValid = generator.Validate("abcD123!"); +``` + ## Installation Install via NuGet: diff --git a/src/KPasswordGenerator.Tests/PasswordGeneratorTests.cs b/src/KPasswordGenerator.Tests/PasswordGeneratorTests.cs index d171147..f15a33a 100644 --- a/src/KPasswordGenerator.Tests/PasswordGeneratorTests.cs +++ b/src/KPasswordGenerator.Tests/PasswordGeneratorTests.cs @@ -60,4 +60,89 @@ public void Generate_DoesNotReturnSamePasswordEveryTime() Assert.NotEqual(password1, password2); // May rarely fail on pure randomness } + + [Theory] + [InlineData(6, 12)] + [InlineData(10, 10)] // exact length + [InlineData(4, 5)] + public void GenerateRandomLength_ReturnsPasswordWithinRange(int min, int max) + { + var generator = CreateDefaultGenerator(); + + string password = generator.GenerateRandomLength(min, max); + + Assert.InRange(password.Length, min, max); + } + + [Fact] + public void GenerateRandomLength_Throws_WhenMinGreaterThanMax() + { + var generator = CreateDefaultGenerator(); ; + + Assert.Throws(() => + generator.GenerateRandomLength(10, 5)); + } + + [Fact] + public void GenerateRandomLength_Throws_WhenMinLessThanRequired() + { + var generator = CreateDefaultGenerator(); + int tooSmall = 3; + + Assert.Throws(() => + generator.GenerateRandomLength(tooSmall, 10)); + } + + [Fact] + public void Validate_ReturnsTrue_WhenPasswordMeetsAllRequirements() + { + var generator = CreateDefaultGenerator(); + + // 'a', 'b' (from "abc"), '1' (from "123"), '!' (from "!@#") + string validPassword = "ab1!xyz"; + + Assert.True(generator.Validate(validPassword)); + } + + [Fact] + public void Validate_ReturnsFalse_WhenPasswordTooShort() + { + var generator = CreateDefaultGenerator(); + + string shortPassword = "a1!"; + + Assert.False(generator.Validate(shortPassword)); + } + + [Fact] + public void Validate_ReturnsFalse_WhenRequirementNotMet() + { + var generator = CreateDefaultGenerator(); + + // Missing digits + string invalidPassword = "ab!!xyz"; + + Assert.False(generator.Validate(invalidPassword)); + } + + [Fact] + public void Validate_Throws_WhenPasswordIsNull() + { + var generator = CreateDefaultGenerator(); + + Assert.Throws(() => + generator.Validate(null!)); + } + + private static PasswordGenerator CreateDefaultGenerator() + { + List requirements = + [ + new CharacterRequirement(2, "abc"), + new CharacterRequirement(1, "123"), + new CharacterRequirement(1, "!@#") + ]; + + return new PasswordGenerator(new PasswordSettings(requirements)); + } } \ No newline at end of file diff --git a/src/KPasswordGenerator/PasswordGenerator.cs b/src/KPasswordGenerator/PasswordGenerator.cs index 05be9ad..0feb73c 100644 --- a/src/KPasswordGenerator/PasswordGenerator.cs +++ b/src/KPasswordGenerator/PasswordGenerator.cs @@ -34,4 +34,30 @@ public string Generate(int passwordLength) return buffer.ToString(); } + + public string GenerateRandomLength(int minPasswordLength, int maxPasswordLength) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(minPasswordLength, maxPasswordLength); + ArgumentOutOfRangeException.ThrowIfLessThan(minPasswordLength, _passwordSettings.MinimumPasswordLength); + + int length = RandomNumberGenerator.GetInt32(minPasswordLength, maxPasswordLength + 1); + + return Generate(length); + } + + public bool Validate(string password) + { + ArgumentNullException.ThrowIfNull(password); + + if (password.Length < _passwordSettings.MinimumPasswordLength) return false; + + foreach (var requirement in _passwordSettings.CharacterRequirements) + { + int count = password.Count(requirement.CharacterPool.Contains); + + if (count < requirement.MinRequired) return false; + } + + return true; + } } \ No newline at end of file From 3d3335cdfa6aaa05cb8d6a6b4e0ea3dc7e5ea87b Mon Sep 17 00:00:00 2001 From: Kirill Polishchuk Date: Wed, 2 Jul 2025 11:47:48 +1200 Subject: [PATCH 2/2] Added default methods --- README.md | 9 ++++ .../PasswordSettingsTests.cs | 46 +++++++++++++++++++ src/KPasswordGenerator/CharacterPools.cs | 2 + src/KPasswordGenerator/PasswordGenerator.cs | 12 ++++- src/KPasswordGenerator/PasswordSettings.cs | 19 ++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 src/KPasswordGenerator.Tests/PasswordSettingsTests.cs diff --git a/README.md b/README.md index d8d9547..326d4a5 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,15 @@ string password = generator.Generate(16); Console.WriteLine(password); // Example output: kAj79uV@E?m7_8eS ``` +### Using WithDefaults +Quickly generate a password using default pools: +```csharp +PasswordSettings settings = PasswordSettings.WithDefaults(minLower: 2, minUpper: 2, minDigits: 2, minSpecial: 1); +PasswordGenerator generator = new(settings); + +string password = generator.Generate(12); +``` + ## Additional Methods ### `GenerateRandomLength(int minPasswordLength, int maxPasswordLength)` diff --git a/src/KPasswordGenerator.Tests/PasswordSettingsTests.cs b/src/KPasswordGenerator.Tests/PasswordSettingsTests.cs new file mode 100644 index 0000000..506c87b --- /dev/null +++ b/src/KPasswordGenerator.Tests/PasswordSettingsTests.cs @@ -0,0 +1,46 @@ +namespace KPasswordGenerator.Tests; + +public class PasswordSettingsTests +{ + [Fact] + public void WithDefaults_UsesExpectedDefaults() + { + var settings = PasswordSettings.WithDefaults(); + + Assert.Equal(4, settings.CharacterRequirements.Count); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == 1 && r.CharacterPool == CharacterPools.LowerCase); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == 1 && r.CharacterPool == CharacterPools.UpperCase); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == 1 && r.CharacterPool == CharacterPools.Digits); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == 1 && r.CharacterPool == CharacterPools.Special); + } + + [Fact] + public void WithDefaults_AllZero_ReturnsEmptyRequirements() + { + var settings = PasswordSettings.WithDefaults(0, 0, 0, 0); + + Assert.Empty(settings.CharacterRequirements); + } + + [Fact] + public void WithDefaults_CreatesRequirementsWithCorrectValues() + { + int lower = 2, upper = 3, digits = 4, special = 5; + + var settings = PasswordSettings.WithDefaults(lower, upper, digits, special); + + Assert.Equal(4, settings.CharacterRequirements.Count); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == lower && r.CharacterPool == CharacterPools.LowerCase); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == upper && r.CharacterPool == CharacterPools.UpperCase); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == digits && r.CharacterPool == CharacterPools.Digits); + Assert.Contains(settings.CharacterRequirements, r => + r.MinRequired == special && r.CharacterPool == CharacterPools.Special); + } +} \ No newline at end of file diff --git a/src/KPasswordGenerator/CharacterPools.cs b/src/KPasswordGenerator/CharacterPools.cs index afcb516..5413055 100644 --- a/src/KPasswordGenerator/CharacterPools.cs +++ b/src/KPasswordGenerator/CharacterPools.cs @@ -6,5 +6,7 @@ public static class CharacterPools public const string UpperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + public const string Digits = "0123456789"; + public const string Special = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; } \ No newline at end of file diff --git a/src/KPasswordGenerator/PasswordGenerator.cs b/src/KPasswordGenerator/PasswordGenerator.cs index 0feb73c..797ccca 100644 --- a/src/KPasswordGenerator/PasswordGenerator.cs +++ b/src/KPasswordGenerator/PasswordGenerator.cs @@ -53,9 +53,17 @@ public bool Validate(string password) foreach (var requirement in _passwordSettings.CharacterRequirements) { - int count = password.Count(requirement.CharacterPool.Contains); + int count = 0; - if (count < requirement.MinRequired) return false; + for (int i = 0; i < password.Length && count < requirement.MinRequired; i++) + { + if (requirement.CharacterPool.Contains(password[i])) + { + count++; + } + } + + if (count != requirement.MinRequired) return false; } return true; diff --git a/src/KPasswordGenerator/PasswordSettings.cs b/src/KPasswordGenerator/PasswordSettings.cs index 8a26fc1..5c0fc5f 100644 --- a/src/KPasswordGenerator/PasswordSettings.cs +++ b/src/KPasswordGenerator/PasswordSettings.cs @@ -16,4 +16,23 @@ public PasswordSettings(ICollection characterRequirements) } public ICollection CharacterRequirements { get; } + + public static PasswordSettings WithDefaults(int minLower = 1, int minUpper = 1, int minDigits = 1, int minSpecial = 1) + { + List requirements = []; + + if (minLower > 0) + requirements.Add(new CharacterRequirement(minLower, CharacterPools.LowerCase)); + + if (minUpper > 0) + requirements.Add(new CharacterRequirement(minUpper, CharacterPools.UpperCase)); + + if (minDigits > 0) + requirements.Add(new CharacterRequirement(minDigits, CharacterPools.Digits)); + + if (minSpecial > 0) + requirements.Add(new CharacterRequirement(minSpecial, CharacterPools.Special)); + + return new PasswordSettings(requirements); + } } \ No newline at end of file