Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
Expand Down Expand Up @@ -58,7 +58,7 @@ jobs:

- name: Upload raw artifacts
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: benchmark-results-${{ github.run_number }}
path: PasswordGenerator.Benchmarks/BenchmarkDotNet.Artifacts/results/
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0 # full history so SourceLink can resolve the commit

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
Expand All @@ -39,7 +39,7 @@ jobs:

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-results
path: ./test-results/*.trx
Expand All @@ -48,7 +48,7 @@ jobs:
run: dotnet pack PasswordGenerator/PasswordGenerator.csproj -c Release --no-build -o artifacts

- name: Upload package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nuget
path: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0 # full history so SourceLink can resolve the commit

- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
Expand Down
4 changes: 2 additions & 2 deletions PasswordGenerator.Benchmarks/Benchmarks/AsyncBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ public void Cleanup()
}

[Benchmark]
public Task<string> NextAsync()
public ValueTask<string> NextAsync()
{
return _password.NextAsync();
}

[Benchmark]
public Task<IReadOnlyList<string>> GenerateAsync()
public ValueTask<IReadOnlyList<string>> GenerateAsync()
{
return _password.GenerateAsync(Count);
}
Expand Down
24 changes: 24 additions & 0 deletions PasswordGenerator.Tests/Phase3Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,30 @@ public void NextAsync_WithAlreadyCancelledToken_Throws()
Throws.InstanceOf<OperationCanceledException>());
}

[Test]
public void NextAsync_WithCancelledToken_SurfacesCancellationThroughTask()
{
var pwd = new Password(20);
var cts = new CancellationTokenSource();
cts.Cancel();

// Cancellation must come back through the returned task, not as a synchronous throw,
// so the result composes correctly when not awaited immediately.
var task = pwd.NextAsync(cts.Token);
Assert.That(task.IsCanceled, Is.True);
}

[Test]
public void GenerateAsync_WithCancelledToken_SurfacesCancellationThroughTask()
{
var pwd = new Password(16);
var cts = new CancellationTokenSource();
cts.Cancel();

var task = pwd.GenerateAsync(10, cts.Token);
Assert.That(task.IsCanceled, Is.True);
}

[Test]
public void Generate_ReturnsRequestedCount()
{
Expand Down
7 changes: 7 additions & 0 deletions PasswordGenerator/CharacterClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@ namespace PasswordGenerator
/// </summary>
public enum CharacterClass
{
/// <summary>Lowercase letters (<c>a–z</c>).</summary>
Lowercase,

/// <summary>Uppercase letters (<c>A–Z</c>).</summary>
Uppercase,

/// <summary>Digits (<c>0–9</c>).</summary>
Numeric,

/// <summary>Special / symbol characters.</summary>
Special
}
}
9 changes: 9 additions & 0 deletions PasswordGenerator/CryptoRandomSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ namespace PasswordGenerator
/// </summary>
public sealed class CryptoRandomSource : IRandomSource
{
/// <summary>
/// Returns a uniformly distributed, non-negative random integer that is less than
/// <paramref name="maxExclusive" />.
/// </summary>
/// <param name="maxExclusive">The exclusive upper bound; must be positive.</param>
/// <returns>A random integer in the range <c>[0, maxExclusive)</c>.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="maxExclusive" /> is zero or negative.
/// </exception>
public int NextInt(int maxExclusive)
{
if (maxExclusive <= 0)
Expand Down
35 changes: 35 additions & 0 deletions PasswordGenerator/IPassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,31 @@

namespace PasswordGenerator
{
/// <summary>
/// Fluent builder for configuring and generating passwords. Each configuration method returns the
/// same instance so calls can be chained.
/// </summary>
public interface IPassword
{
/// <summary>Includes lowercase letters in the pool.</summary>
/// <returns>The same builder, for chaining.</returns>
IPassword IncludeLowercase();

/// <summary>Includes uppercase letters in the pool.</summary>
/// <returns>The same builder, for chaining.</returns>
IPassword IncludeUppercase();

/// <summary>Includes digits in the pool.</summary>
/// <returns>The same builder, for chaining.</returns>
IPassword IncludeNumeric();

/// <summary>Includes the default special characters in the pool.</summary>
/// <returns>The same builder, for chaining.</returns>
IPassword IncludeSpecial();

/// <summary>Includes the given special characters in the pool.</summary>
/// <param name="specialCharactersToInclude">The special characters to add to the pool.</param>
/// <returns>The same builder, for chaining.</returns>
IPassword IncludeSpecial(string specialCharactersToInclude);

/// <summary>Replaces the pool with an explicit set of characters (no forced composition).</summary>
Expand All @@ -22,9 +41,25 @@ public interface IPassword
/// <summary>Requires at least <paramref name="count" /> characters from the given class.</summary>
IPassword RequireAtLeast(CharacterClass characterClass, int count);

/// <summary>Sets the required password length.</summary>
/// <param name="passwordLength">The number of characters the generated password should contain.</param>
/// <returns>The same builder, for chaining.</returns>
IPassword LengthRequired(int passwordLength);

/// <summary>Generates a single password using the current settings.</summary>
/// <returns>The generated password.</returns>
string Next();

/// <summary>Attempts to generate a single password without throwing on invalid settings.</summary>
/// <param name="password">
/// When this method returns <see langword="true" />, the generated password; otherwise <see langword="null" />.
/// </param>
/// <returns><see langword="true" /> if a password was generated; otherwise <see langword="false" />.</returns>
bool TryNext(out string? password);

/// <summary>Generates a sequence of passwords using the current settings.</summary>
/// <param name="numberOfPasswordsToGenerate">How many passwords to generate.</param>
/// <returns>The generated passwords.</returns>
IEnumerable<string> NextGroup(int numberOfPasswordsToGenerate);
}
}
12 changes: 7 additions & 5 deletions PasswordGenerator/IPasswordGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ public interface IPasswordGenerator
bool TryNext(out string? password);

/// <summary>
/// Generates a single password. Generation is CPU-bound; this overload exists for ergonomics
/// and to honour cancellation, not to offload work to another thread.
/// Generates a single password. Generation is CPU-bound and completes synchronously; this overload
/// exists for ergonomics and to honour cancellation, not to offload work to another thread. A
/// <see cref="ValueTask{TResult}" /> is used because the result is always available synchronously.
/// If <paramref name="cancellationToken" /> is already cancelled, the returned task is cancelled.
/// </summary>
Task<string> NextAsync(CancellationToken cancellationToken = default);
ValueTask<string> NextAsync(CancellationToken cancellationToken = default);

/// <summary>Generates the default number of passwords (configurable; one unless overridden).</summary>
IReadOnlyList<string> Generate();
Expand All @@ -29,9 +31,9 @@ public interface IPasswordGenerator
IReadOnlyList<string> Generate(int count);

/// <summary>Generates the default number of passwords, observing <paramref name="cancellationToken" />.</summary>
Task<IReadOnlyList<string>> GenerateAsync(CancellationToken cancellationToken = default);
ValueTask<IReadOnlyList<string>> GenerateAsync(CancellationToken cancellationToken = default);

/// <summary>Generates <paramref name="count" /> passwords, observing <paramref name="cancellationToken" />.</summary>
Task<IReadOnlyList<string>> GenerateAsync(int count, CancellationToken cancellationToken = default);
ValueTask<IReadOnlyList<string>> GenerateAsync(int count, CancellationToken cancellationToken = default);
}
}
34 changes: 34 additions & 0 deletions PasswordGenerator/IPasswordSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ namespace PasswordGenerator
/// </summary>
public interface IPasswordSettings
{
/// <summary>Whether lowercase letters are included in the pool.</summary>
bool IncludeLowercase { get; }

/// <summary>Whether uppercase letters are included in the pool.</summary>
bool IncludeUppercase { get; }

/// <summary>Whether digits are included in the pool.</summary>
bool IncludeNumeric { get; }

/// <summary>Whether special characters are included in the pool.</summary>
bool IncludeSpecial { get; }

/// <summary>The number of characters the generated password should contain.</summary>
int PasswordLength { get; set; }

/// <summary>The full set of characters the password is drawn from, after applying all settings.</summary>
string CharacterSet { get; }

/// <summary>True when a custom pool (e.g. <see cref="UseCharacters" />) replaces the per-class sets.</summary>
Expand All @@ -28,13 +39,35 @@ public interface IPasswordSettings
/// guarantee at least one character from each required class is present in the output.
/// </summary>
IReadOnlyList<string> CharacterGroups { get; }

/// <summary>The maximum number of attempts allowed when generating a valid password.</summary>
int MaximumAttempts { get; }

/// <summary>The smallest allowed password length.</summary>
int MinimumLength { get; }

/// <summary>The largest allowed password length.</summary>
int MaximumLength { get; }

/// <summary>Enables lowercase letters in the pool.</summary>
/// <returns>The same settings, for chaining.</returns>
IPasswordSettings AddLowercase();

/// <summary>Enables uppercase letters in the pool.</summary>
/// <returns>The same settings, for chaining.</returns>
IPasswordSettings AddUppercase();

/// <summary>Enables digits in the pool.</summary>
/// <returns>The same settings, for chaining.</returns>
IPasswordSettings AddNumeric();

/// <summary>Enables the default special characters in the pool.</summary>
/// <returns>The same settings, for chaining.</returns>
IPasswordSettings AddSpecial();

/// <summary>Enables the given special characters in the pool.</summary>
/// <param name="specialCharactersToAdd">The special characters to add to the pool.</param>
/// <returns>The same settings, for chaining.</returns>
IPasswordSettings AddSpecial(string specialCharactersToAdd);

/// <summary>Replaces the entire pool with an explicit set of characters (no forced composition).</summary>
Expand All @@ -49,6 +82,7 @@ public interface IPasswordSettings
/// <summary>Requires at least <paramref name="count" /> characters from the given class, enabling it if needed.</summary>
IPasswordSettings RequireAtLeast(CharacterClass characterClass, int count);

/// <summary>The special characters used when special characters are included.</summary>
string SpecialCharacters { get; set; }
}
}
Loading