Skip to content
Open
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
16 changes: 16 additions & 0 deletions access-token-management/src/AccessTokenManagement/RefreshToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,24 @@ namespace Duende.AccessTokenManagement;
public readonly record struct RefreshToken : IStronglyTypedValue<RefreshToken>
{
public const int MaxLength = 4 * 1024;

public override string ToString() => Value;

/// <summary>
/// Changes the maximum length for refresh tokens.
/// </summary>
/// <param name="maxLength">The new maximum length. Must be a strictly positive value.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="maxLength"/> is zero or a negative value.</exception>
/// <remarks>
/// Note that this change is applied statically and will affect all instances of <see cref="RefreshToken"/> across the entire application.
/// </remarks>
public static void SetMaxLength(int maxLength)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxLength);

Validators[0] = ValidationRules.MaxLength(maxLength);
}
Comment on lines +25 to +30
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetMaxLength updates Validators[0], which couples the API to the current ordering/shape of the Validators array. If additional validation rules are added later (or the order changes), this method could silently stop updating the max-length rule. Consider refactoring so the max-length check is implemented via a dedicated validator method that reads a static int (e.g., Volatile.Read/Write) or by rebuilding the validators array in a way that doesn't rely on a hard-coded index.

Copilot uses AI. Check for mistakes.

private static readonly ValidationRule<string>[] Validators = [
// Officially, there's no max length refresh tokens, but 4k is a good limit
ValidationRules.MaxLength(MaxLength)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@
public RefreshToken() { }
public override string ToString() { }
public static Duende.AccessTokenManagement.RefreshToken Parse(string value) { }
public static void SetMaxLength(int maxLength) { }
public static bool TryParse(string value, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out Duende.AccessTokenManagement.RefreshToken? parsed, out string[] errors) { }
public static string op_Implicit(Duende.AccessTokenManagement.RefreshToken value) { }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace Duende.AccessTokenManagement.Types;

/// <summary>
/// Tests for <see cref="RefreshToken.SetMaxLength"/>. These are isolated into a separate
/// non-parallel collection because <see cref="RefreshToken.SetMaxLength"/> mutates static state
/// that would affect other tests running concurrently.
/// </summary>
[Collection(nameof(RefreshTokenTests))]
public class RefreshTokenSetMaxLengthTests : IDisposable
Comment on lines +6 to +12
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: [Collection(nameof(RefreshTokenTests))] does not make the collection itself non-parallel vs other collections. If these tests rely on global static state being isolated, introduce a CollectionDefinition with DisableParallelization = true (or an equivalent mechanism) instead of only documenting it.

Copilot uses AI. Check for mistakes.
{
public void Dispose() =>
// Reset to the default max length after each test so static state doesn't leak.
RefreshToken.SetMaxLength(RefreshToken.MaxLength);

[Fact]
public void SetMaxLength_AllowsLargerTokens()
{
// Arrange
var largeMaxLength = 16 * 1024; // 16 KB, e.g. for ADFS tokens
var largeValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds default, within new limit
RefreshToken.SetMaxLength(largeMaxLength);

// Act
var result = RefreshToken.Parse(largeValue);

// Assert
result.ToString().ShouldBe(largeValue);
}

[Fact]
public void SetMaxLength_StillRejectsTokensExceedingNewLimit()
{
// Arrange
var newMaxLength = 8 * 1024;
RefreshToken.SetMaxLength(newMaxLength);
var tooLargeValue = new string('a', newMaxLength + 1);

// Act & Assert
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(tooLargeValue));
exception.Message.ShouldContain("exceeds maximum length");
}

[Fact]
public void SetMaxLength_TryParse_AllowsLargerTokens()
{
// Arrange
var largeMaxLength = 16 * 1024;
var largeValue = new string('a', RefreshToken.MaxLength + 1);
RefreshToken.SetMaxLength(largeMaxLength);

// Act
var success = RefreshToken.TryParse(largeValue, out var result, out var errors);

// Assert
success.ShouldBeTrue();
result.ShouldNotBeNull();
result.ToString().ShouldBe(largeValue);
errors.ShouldBeEmpty();
}

[Fact]
public void SetMaxLength_TryParse_StillRejectsTokensExceedingNewLimit()
{
// Arrange
var newMaxLength = 8 * 1024;
RefreshToken.SetMaxLength(newMaxLength);
var tooLargeValue = new string('a', newMaxLength + 1);

// Act
var success = RefreshToken.TryParse(tooLargeValue, out var result, out var errors);

// Assert
success.ShouldBeFalse();
result.ShouldBeNull();
errors.ShouldNotBeEmpty();
errors[0].ShouldContain("exceeds maximum length");
}

[Fact]
public void SetMaxLength_CanReduceLimit()
{
// Arrange
var smallerMaxLength = 100;
RefreshToken.SetMaxLength(smallerMaxLength);
var value = new string('a', smallerMaxLength + 1);

// Act & Assert
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(value));
exception.Message.ShouldContain("exceeds maximum length");
}

[Fact]
public void SetMaxLength_AtExactNewLimit_Succeeds()
{
// Arrange
var newMaxLength = 8 * 1024;
RefreshToken.SetMaxLength(newMaxLength);
var value = new string('a', newMaxLength);

// Act
var result = RefreshToken.Parse(value);

// Assert
result.ToString().ShouldBe(value);
}

[Fact]
public void SetMaxLength_Zero_ThrowsArgumentOutOfRangeException() =>
// Act & Assert
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(0));

[Fact]
public void SetMaxLength_Negative_ThrowsArgumentOutOfRangeException() =>
// Act & Assert
Should.Throw<ArgumentOutOfRangeException>(() => RefreshToken.SetMaxLength(-1));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright (c) Duende Software. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.

namespace Duende.AccessTokenManagement.Types;

/// <summary>
/// Both the <see cref="RefreshTokenTests"/> and <see cref="RefreshTokenSetMaxLengthTests"/> classes share a collection to prevent parallel execution,
/// since <see cref="RefreshToken.SetMaxLength"/> mutates static state.
/// </summary>
[Collection(nameof(RefreshTokenTests))]
public class RefreshTokenTests
Comment on lines +6 to +11
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The XML doc says these tests are placed in a collection to “prevent parallel execution”, but [Collection(...)] only serializes tests within the same collection; the collection can still run in parallel with other collections. If the goal is to avoid static-state interference, add a [CollectionDefinition(..., DisableParallelization = true)] for this collection (or otherwise disable parallelization for refresh-token tests) so no other test collection can run concurrently while RefreshToken.SetMaxLength mutates global state.

Copilot uses AI. Check for mistakes.
{
[Fact]
public void Parse_ValidValue_ReturnsRefreshToken()
{
// Arrange
var validValue = "valid_refresh_token";

// Act
var result = RefreshToken.Parse(validValue);

// Assert
result.ToString().ShouldBe(validValue);
}

[Fact]
public void Parse_InvalidValue_ThrowsException()
{
// Arrange
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length

// Act & Assert
var exception = Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(invalidValue));
exception.Message.ShouldContain("exceeds maximum length");
}

[Fact]
public void TryParse_ValidValue_ReturnsTrueAndRefreshToken()
{
// Arrange
var validValue = "valid_refresh_token";

// Act
var success = RefreshToken.TryParse(validValue, out var result, out var errors);

// Assert
success.ShouldBeTrue();
result.ShouldNotBeNull();
result.ToString().ShouldBe(validValue);
errors.ShouldBeEmpty();
}

[Fact]
public void TryParse_InvalidValue_ReturnsFalseAndErrors()
{
// Arrange
var invalidValue = new string('a', RefreshToken.MaxLength + 1); // Exceeds max length

// Act
var success = RefreshToken.TryParse(invalidValue, out var result, out var errors);

// Assert
success.ShouldBeFalse();
result.ShouldBeNull();
errors.ShouldNotBeEmpty();
errors[0].ShouldContain("exceeds maximum length");
}

[Fact]
public void Parse_AtExactMaxLength_ReturnsRefreshToken()
{
// Arrange
var value = new string('a', RefreshToken.MaxLength);

// Act
var result = RefreshToken.Parse(value);

// Assert
result.ToString().ShouldBe(value);
}

[Fact]
public void Parse_NullValue_ThrowsException() =>
// Act & Assert
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(null!));

[Fact]
public void Parse_EmptyValue_ThrowsException() =>
// Act & Assert
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(string.Empty));

[Fact]
public void Parse_WhitespaceValue_ThrowsException() =>
// Act & Assert
Should.Throw<InvalidOperationException>(() => RefreshToken.Parse(" "));

[Fact]
public void ParameterlessConstructor_ThrowsException()
{
// Act & Assert
var exception = Should.Throw<InvalidOperationException>(() => new RefreshToken());
exception.Message.ShouldContain("Can't create null value");
}

[Fact]
public void ImplicitStringConversion_ReturnsValue()
{
// Arrange
var value = "my_refresh_token";
var token = RefreshToken.Parse(value);

// Act
string converted = token;

// Assert
converted.ShouldBe(value);
}
}
Loading