Make CT required in IBackChannelLogoutHttpClient, IJwtRequestUriHttpClient, IUiLocalesService, IBackchannelAuthenticationUserNotificationService, IUserCodeService, IUserCodeGenerator, IIntrospectionResponseGenerator, and IDiscoveryResponseGenerator, flow through implementations, callers, and tests

This commit is contained in:
Damian Hickey 2026-02-20 23:02:49 +01:00
parent f7d6f09c4e
commit 944920ff30
32 changed files with 91 additions and 65 deletions

View file

@ -25,20 +25,20 @@ internal abstract class BaseDiscoveryEndpoint(
var distributedCache = context.RequestServices.GetRequiredService<IDistributedCache>();
if (distributedCache is not null)
{
return await GetCachedDiscoveryDocument(distributedCache, baseUrl, issuerUri);
return await GetCachedDiscoveryDocument(distributedCache, baseUrl, issuerUri, context.RequestAborted);
}
// fall through to default implementation if there is no cache provider registered
}
var response = await ResponseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);
var response = await ResponseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri, context.RequestAborted);
return new DiscoveryDocumentResult(response, Options.Discovery.ResponseCacheInterval);
}
private async Task<IEndpointResult> GetCachedDiscoveryDocument(IDistributedCache cache, string baseUrl,
string issuerUri)
string issuerUri, CT ct)
{
var key = $"discoveryDocument/{baseUrl}/{issuerUri}";
var json = await cache.GetStringAsync(key);
var json = await cache.GetStringAsync(key, ct);
if (json is not null)
{
@ -49,7 +49,7 @@ internal abstract class BaseDiscoveryEndpoint(
}
var entries =
await ResponseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);
await ResponseGenerator.CreateDiscoveryDocumentAsync(baseUrl, issuerUri, ct);
var expirationFromNow = Options.Preview.DiscoveryDocumentCacheDuration;
@ -62,7 +62,7 @@ internal abstract class BaseDiscoveryEndpoint(
await cache.SetStringAsync(key, result.Json, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expirationFromNow,
});
}, ct);
return result;
}

View file

@ -53,7 +53,7 @@ internal class DiscoveryKeyEndpoint : IEndpointHandler
// generate response
_logger.LogTrace("Calling into discovery response generator: {type}", _responseGenerator.GetType().FullName);
var response = await _responseGenerator.CreateJwkDocumentAsync();
var response = await _responseGenerator.CreateJwkDocumentAsync(context.RequestAborted);
return new JsonWebKeysResult(response, _options.Discovery.ResponseCacheInterval);
}

View file

@ -152,7 +152,7 @@ internal class IntrospectionEndpoint : IEndpointHandler
// response generation
_logger.LogTrace("Calling into introspection response generator: {type}", _responseGenerator.GetType().FullName);
var response = await _responseGenerator.ProcessAsync(validationResult);
var response = await _responseGenerator.ProcessAsync(validationResult, context.RequestAborted);
// render result
LogSuccess(validationResult.IsActive, callerName);

View file

@ -122,7 +122,7 @@ internal class AuthorizeInteractionPageHttpWriter : IHttpResponseWriter<Authoriz
else
{
// if we're redirecting to a local URL, ensure we persist the UI locales in a way .net's localization will pick them up
await _localesService.StoreUiLocalesForRedirectAsync(result.Request.UiLocales);
await _localesService.StoreUiLocalesForRedirectAsync(result.Request.UiLocales, context.RequestAborted);
}
url = url.AddQueryString(result.ReturnUrlParameterName, returnUrl);

View file

@ -221,7 +221,7 @@ public class AuthorizeHttpWriter : IHttpResponseWriter<AuthorizeResult>
var uiLocalesService = context.RequestServices.GetService<IUiLocalesService>();
if (uiLocalesService != null)
{
await uiLocalesService.StoreUiLocalesForRedirectAsync(response.Request?.UiLocales);
await uiLocalesService.StoreUiLocalesForRedirectAsync(response.Request?.UiLocales, context.RequestAborted);
}
var errorModel = await CreateErrorMessage(response, context);

View file

@ -75,7 +75,7 @@ internal class EndSessionHttpWriter : IHttpResponseWriter<EndSessionResult>
if (redirect.IsLocalUrl())
{
redirect = _urls.GetIdentityServerRelativeUrl(redirect);
await _localesService.StoreUiLocalesForRedirectAsync(result.Result.ValidatedRequest?.UiLocales);
await _localesService.StoreUiLocalesForRedirectAsync(result.Result.ValidatedRequest?.UiLocales, context.RequestAborted);
}
if (id != null)

View file

@ -118,7 +118,7 @@ public class BackchannelAuthenticationResponseGenerator : IBackchannelAuthentica
Tenant = validationResult.ValidatedRequest.Tenant,
IdP = validationResult.ValidatedRequest.IdP,
Properties = validationResult.ValidatedRequest.Properties,
});
}, ct);
return response;
}

View file

@ -82,13 +82,13 @@ public class DeviceAuthorizationResponseGenerator : IDeviceAuthorizationResponse
// generate user_code
var userCodeGenerator = await UserCodeService.GetGenerator(
validationResult.ValidatedRequest.Client.UserCodeType ??
Options.DeviceFlow.DefaultUserCodeType);
Options.DeviceFlow.DefaultUserCodeType, ct);
var retryCount = 0;
while (retryCount < userCodeGenerator.RetryLimit)
{
var userCode = await userCodeGenerator.GenerateAsync();
var userCode = await userCodeGenerator.GenerateAsync(ct);
var deviceCode = await DeviceFlowCodeService.FindByUserCodeAsync(userCode, ct);
if (deviceCode == null)

View file

@ -92,7 +92,8 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator
/// </summary>
/// <param name="baseUrl">The base URL.</param>
/// <param name="issuerUri">The issuer URI.</param>
public virtual async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(string baseUrl, string issuerUri)
/// <param name="ct"></param>
public virtual async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(string baseUrl, string issuerUri, CT ct)
{
using var activity = Tracing.BasicActivitySource.StartActivity("DiscoveryResponseGenerator.CreateDiscoveryDocument");
@ -106,7 +107,7 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator
// jwks
if (Options.Discovery.ShowKeySet)
{
if ((await Keys.GetValidationKeysAsync(default)).Any())
if ((await Keys.GetValidationKeysAsync(ct)).Any())
{
entries.Add(OidcConstants.Discovery.JwksUri, baseUrl + ProtocolRoutePaths.DiscoveryWebKeys);
}
@ -236,7 +237,7 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator
Options.Discovery.ShowApiScopes ||
Options.Discovery.ShowClaims)
{
var resources = await ResourceStore.GetAllEnabledResourcesAsync(default);
var resources = await ResourceStore.GetAllEnabledResourcesAsync(ct);
var scopes = new List<string>();
// scopes
@ -342,7 +343,7 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator
AddSigningAlgorithmsForEndpointIfNeeded(OidcConstants.Discovery.IntrospectionEndpointAuthSigningAlgorithmsSupported, entries, supportedAuthMethods);
}
var signingCredentials = await Keys.GetAllSigningCredentialsAsync(default);
var signingCredentials = await Keys.GetAllSigningCredentialsAsync(ct);
if (signingCredentials.Any())
{
var signingAlgorithms = signingCredentials.Select(c => c.Algorithm).Distinct();
@ -458,13 +459,14 @@ public class DiscoveryResponseGenerator : IDiscoveryResponseGenerator
/// <summary>
/// Creates the JWK document.
/// </summary>
public virtual async Task<IEnumerable<Models.JsonWebKey>> CreateJwkDocumentAsync()
/// <param name="ct"></param>
public virtual async Task<IEnumerable<Models.JsonWebKey>> CreateJwkDocumentAsync(CT ct)
{
using var activity = Tracing.BasicActivitySource.StartActivity("DiscoveryResponseGenerator.CreateJwkDocument");
var webKeys = new List<Models.JsonWebKey>();
foreach (var key in await Keys.GetValidationKeysAsync(default))
foreach (var key in await Keys.GetValidationKeysAsync(ct))
{
if (key.Key is X509SecurityKey x509Key)
{

View file

@ -45,8 +45,9 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
/// Processes the response.
/// </summary>
/// <param name="validationResult">The validation result.</param>
/// <param name="ct"></param>
/// <returns></returns>
public virtual async Task<Dictionary<string, object>> ProcessAsync(IntrospectionRequestValidationResult validationResult)
public virtual async Task<Dictionary<string, object>> ProcessAsync(IntrospectionRequestValidationResult validationResult, CT ct)
{
using var activity = Tracing.BasicActivitySource.StartActivity("IntrospectionResponseGenerator.Process");
@ -65,7 +66,7 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
{
Logger.LogDebug("Creating introspection response for inactive token.");
Telemetry.Metrics.Introspection(callerName, false);
await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult), default);
await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult), ct);
return response;
}
@ -76,7 +77,7 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
if (validationResult.Api != null)
{
// expected scope not present
if (await AreExpectedScopesPresentAsync(validationResult) == false)
if (await AreExpectedScopesPresentAsync(validationResult, ct) == false)
{
return response;
}
@ -98,7 +99,7 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
response.Add("scope", scopes.ToSpaceSeparatedString());
Telemetry.Metrics.Introspection(callerName, true);
await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult), default);
await Events.RaiseAsync(new TokenIntrospectionSuccessEvent(validationResult), ct);
return response;
}
@ -106,8 +107,9 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
/// Checks if the API resource is allowed to introspect the scopes.
/// </summary>
/// <param name="validationResult">The validation result.</param>
/// <param name="ct"></param>
/// <returns></returns>
protected virtual async Task<bool> AreExpectedScopesPresentAsync(IntrospectionRequestValidationResult validationResult)
protected virtual async Task<bool> AreExpectedScopesPresentAsync(IntrospectionRequestValidationResult validationResult, CT ct)
{
var apiScopes = validationResult.Api.Scopes;
var tokenScopes = validationResult.Claims.Where(c => c.Type == JwtClaimTypes.Scope);
@ -129,7 +131,7 @@ public class IntrospectionResponseGenerator : IIntrospectionResponseGenerator
const string errorMessage = "Expected scopes are missing";
var callerName = validationResult.Api?.Name ?? validationResult.Client.ClientId;
Telemetry.Metrics.IntrospectionFailure(callerName, errorMessage);
await Events.RaiseAsync(new TokenIntrospectionFailureEvent(validationResult.Api.Name, errorMessage, validationResult.Token, apiScopes, tokenScopes.Select(s => s.Value)), default);
await Events.RaiseAsync(new TokenIntrospectionFailureEvent(validationResult.Api.Name, errorMessage, validationResult.Token, apiScopes, tokenScopes.Select(s => s.Value)), ct);
}
return result;

View file

@ -16,10 +16,12 @@ public interface IDiscoveryResponseGenerator
/// </summary>
/// <param name="baseUrl">The base URL.</param>
/// <param name="issuerUri">The issuer URI.</param>
Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(string baseUrl, string issuerUri);
/// <param name="ct"></param>
Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(string baseUrl, string issuerUri, CT ct);
/// <summary>
/// Creates the JWK document.
/// </summary>
Task<IEnumerable<JsonWebKey>> CreateJwkDocumentAsync();
/// <param name="ct"></param>
Task<IEnumerable<JsonWebKey>> CreateJwkDocumentAsync(CT ct);
}

View file

@ -15,6 +15,7 @@ public interface IIntrospectionResponseGenerator
/// Processes the response.
/// </summary>
/// <param name="validationResult">The validation result.</param>
/// <param name="ct"></param>
/// <returns></returns>
Task<Dictionary<string, object>> ProcessAsync(IntrospectionRequestValidationResult validationResult);
Task<Dictionary<string, object>> ProcessAsync(IntrospectionRequestValidationResult validationResult, CT ct);
}

View file

@ -34,15 +34,16 @@ public class DefaultBackChannelLogoutHttpClient : IBackChannelLogoutHttpClient
/// </summary>
/// <param name="url"></param>
/// <param name="payload"></param>
/// <param name="ct"></param>
/// <returns></returns>
public async Task PostAsync(string url, Dictionary<string, string> payload)
public async Task PostAsync(string url, Dictionary<string, string> payload, CT ct)
{
using var activity = Tracing.ServiceActivitySource.StartActivity("DefaultBackChannelLogoutHttpClient.Post");
try
{
using var formEncodedContent = new FormUrlEncodedContent(payload);
var response = await _client.PostAsync(url, formEncodedContent, _cancellationTokenProvider.CancellationToken);
var response = await _client.PostAsync(url, formEncodedContent, ct);
if (response.IsSuccessStatusCode)
{
_logger.LogDebug("Response from back-channel logout endpoint: {url} status code: {status}", url, (int)response.StatusCode);
@ -51,7 +52,7 @@ public class DefaultBackChannelLogoutHttpClient : IBackChannelLogoutHttpClient
{
BackChannelError err = null;
var errorjson = await response.Content.ReadAsStringAsync();
var errorjson = await response.Content.ReadAsStringAsync(ct);
try
{
err = JsonSerializer.Deserialize<BackChannelError>(errorjson);

View file

@ -82,7 +82,7 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService
var backChannelRequests = await LogoutNotificationService.GetBackChannelLogoutNotificationsAsync(context, ct);
if (backChannelRequests.Any())
{
await SendLogoutNotificationsAsync(backChannelRequests);
await SendLogoutNotificationsAsync(backChannelRequests, ct);
}
}
@ -90,8 +90,9 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService
/// Sends the logout notifications for the collection of clients.
/// </summary>
/// <param name="requests"></param>
/// <param name="ct"></param>
/// <returns></returns>
protected virtual async Task SendLogoutNotificationsAsync(IEnumerable<BackChannelLogoutRequest> requests)
protected virtual async Task SendLogoutNotificationsAsync(IEnumerable<BackChannelLogoutRequest> requests, CT ct)
{
requests ??= [];
var logoutRequestsWithPayload = new List<(BackChannelLogoutRequest, Dictionary<string, string>)>();
@ -106,7 +107,7 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService
logoutRequestsWithPayload.Add((backChannelLogoutRequest, payload));
}
var logoutRequests = logoutRequestsWithPayload.Select(request => PostLogoutJwt(request.Item1, request.Item2)).ToArray();
var logoutRequests = logoutRequestsWithPayload.Select(request => PostLogoutJwt(request.Item1, request.Item2, ct)).ToArray();
await Task.WhenAll(logoutRequests);
}
@ -115,8 +116,9 @@ public class DefaultBackChannelLogoutService : IBackChannelLogoutService
/// </summary>
/// <param name="client"></param>
/// <param name="data"></param>
/// <param name="ct"></param>
/// <returns></returns>
protected virtual Task PostLogoutJwt(BackChannelLogoutRequest client, Dictionary<string, string> data) => HttpClient.PostAsync(client.LogoutUri, data);
protected virtual Task PostLogoutJwt(BackChannelLogoutRequest client, Dictionary<string, string> data, CT ct) => HttpClient.PostAsync(client.LogoutUri, data, ct);
/// <summary>
/// Creates the form-url-encoded payload (as a dictionary) to send to the client.

View file

@ -38,14 +38,14 @@ public class DefaultJwtRequestUriHttpClient : IJwtRequestUriHttpClient
/// <inheritdoc />
public async Task<string> GetJwtAsync(string url, Client client)
public async Task<string> GetJwtAsync(string url, Client client, CT ct)
{
using var activity = Tracing.ServiceActivitySource.StartActivity("DefaultJwtRequestUriHttpClient.GetJwt");
using var req = new HttpRequestMessage(HttpMethod.Get, url);
req.Options.TryAdd(IdentityServerConstants.JwtRequestClientKey, client);
var response = await _client.SendAsync(req, _cancellationTokenProvider.CancellationToken);
var response = await _client.SendAsync(req, ct);
if (response.StatusCode == System.Net.HttpStatusCode.OK)
{
if (_options.StrictJarValidation)
@ -61,7 +61,7 @@ public class DefaultJwtRequestUriHttpClient : IJwtRequestUriHttpClient
_sanitizedLogger.LogDebug("Success http response from jwt url {url}", url);
var json = await response.Content.ReadAsStringAsync();
var json = await response.Content.ReadAsStringAsync(ct);
return json;
}

View file

@ -13,7 +13,7 @@ namespace Duende.IdentityServer.Services.Default;
public class DefaultUiLocalesService(IHttpContextAccessor httpContextAccessor, IOptions<RequestLocalizationOptions> requestLocalizationOptions, ILogger<DefaultUiLocalesService> logger) : IUiLocalesService
{
public virtual Task StoreUiLocalesForRedirectAsync(string? uiLocales)
public virtual Task StoreUiLocalesForRedirectAsync(string? uiLocales, CT ct)
{
if (httpContextAccessor.HttpContext is null)
{

View file

@ -23,7 +23,8 @@ public class DefaultUserCodeService : IUserCodeService
/// Gets the user code generator.
/// </summary>
/// <param name="userCodeType">Type of user code.</param>
/// <param name="ct"></param>
/// <returns></returns>
public Task<IUserCodeGenerator> GetGenerator(string userCodeType) =>
public Task<IUserCodeGenerator> GetGenerator(string userCodeType, CT ct) =>
Task.FromResult(_generators.FirstOrDefault(x => x.UserCodeType == userCodeType));
}

View file

@ -26,9 +26,9 @@ public class NopBackchannelAuthenticationUserNotificationService : IBackchannelA
}
/// <inheritdoc/>
public async Task SendLoginRequestAsync(BackchannelUserLoginRequest request)
public async Task SendLoginRequestAsync(BackchannelUserLoginRequest request, CT ct)
{
var url = await _issuerNameService.GetCurrentAsync(default);
var url = await _issuerNameService.GetCurrentAsync(ct);
url += "/ciba?id=" + request.InternalId;
_sanitizedLogger.LogWarning("IBackchannelAuthenticationUserNotificationService not implemented. But for testing, visit {url} to simulate what a user might need to do to complete the request.", url);
}

View file

@ -32,8 +32,9 @@ public class NumericUserCodeGenerator : IUserCodeGenerator
/// <summary>
/// Generates the user code.
/// </summary>
/// <param name="ct"></param>
/// <returns></returns>
public Task<string> GenerateAsync()
public Task<string> GenerateAsync(CT ct)
{
var next = RandomNumberGenerator.GetInt32(100000000, 1000000000);
return Task.FromResult(next.ToString(CultureInfo.InvariantCulture));

View file

@ -16,6 +16,7 @@ public interface IBackChannelLogoutHttpClient
/// </summary>
/// <param name="url"></param>
/// <param name="payload"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task PostAsync(string url, Dictionary<string, string> payload);
Task PostAsync(string url, Dictionary<string, string> payload, CT ct);
}

View file

@ -16,5 +16,7 @@ public interface IBackchannelAuthenticationUserNotificationService
/// <summary>
/// Sends a notification for the user to login.
/// </summary>
Task SendLoginRequestAsync(BackchannelUserLoginRequest request);
/// <param name="request"></param>
/// <param name="ct"></param>
Task SendLoginRequestAsync(BackchannelUserLoginRequest request, CT ct);
}

View file

@ -18,6 +18,7 @@ public interface IJwtRequestUriHttpClient
/// </summary>
/// <param name="url"></param>
/// <param name="client"></param>
/// <param name="ct"></param>
/// <returns></returns>
Task<string?> GetJwtAsync(string url, Client client);
Task<string?> GetJwtAsync(string url, Client client, CT ct);
}

View file

@ -6,5 +6,10 @@ namespace Duende.IdentityServer.Services;
public interface IUiLocalesService
{
Task StoreUiLocalesForRedirectAsync(string? uiLocales);
/// <summary>
/// Stores the UI locales for redirect.
/// </summary>
/// <param name="uiLocales"></param>
/// <param name="ct"></param>
Task StoreUiLocalesForRedirectAsync(string? uiLocales, CT ct);
}

View file

@ -30,6 +30,7 @@ public interface IUserCodeGenerator
/// <summary>
/// Generates the user code.
/// </summary>
/// <param name="ct"></param>
/// <returns></returns>
Task<string> GenerateAsync();
Task<string> GenerateAsync(CT ct);
}

View file

@ -15,6 +15,7 @@ public interface IUserCodeService
/// Gets the user code generator.
/// </summary>
/// <param name="userCodeType">Type of user code.</param>
/// <param name="ct"></param>
/// <returns></returns>
Task<IUserCodeGenerator?> GetGenerator(string userCodeType);
Task<IUserCodeGenerator?> GetGenerator(string userCodeType, CT ct);
}

View file

@ -82,7 +82,7 @@ internal class RequestObjectValidator : IRequestObjectValidator
return Invalid(request, error: OidcConstants.AuthorizeErrors.InvalidRequestUri, description: "request_uri is too long");
}
var jwt = await _jwtRequestUriHttpClient.GetJwtAsync(requestUri, request.Client);
var jwt = await _jwtRequestUriHttpClient.GetJwtAsync(requestUri, request.Client, ct);
if (jwt.IsMissing())
{
LogError("no value returned from request_uri", request);

View file

@ -11,7 +11,7 @@ internal class MockCibaUserNotificationService : IBackchannelAuthenticationUserN
{
public BackchannelUserLoginRequest LoginRequest { get; set; }
public Task SendLoginRequestAsync(BackchannelUserLoginRequest request)
public Task SendLoginRequestAsync(BackchannelUserLoginRequest request, CT ct)
{
LoginRequest = request;
return Task.CompletedTask;

View file

@ -11,5 +11,5 @@ public class MockJwtRequestUriHttpClient : IJwtRequestUriHttpClient
{
public string Jwt { get; set; }
public Task<string> GetJwtAsync(string url, Client client) => Task.FromResult(Jwt);
public Task<string> GetJwtAsync(string url, Client client, CT ct) => Task.FromResult(Jwt);
}

View file

@ -8,5 +8,5 @@ namespace UnitTests.Common;
public class MockUiLocaleService : IUiLocalesService
{
public Task StoreUiLocalesForRedirectAsync(string? uiLocales) => Task.CompletedTask;
public Task StoreUiLocalesForRedirectAsync(string? uiLocales, CT ct) => Task.CompletedTask;
}

View file

@ -193,7 +193,7 @@ internal class FakeUserCodeGenerator : IUserCodeGenerator
set => retryLimit = value;
}
public Task<string> GenerateAsync()
public Task<string> GenerateAsync(CT ct)
{
if (tryCount == 0)
{

View file

@ -15,6 +15,7 @@ namespace UnitTests.Services.Default;
public class DefaultUiLocalesServiceTests
{
private readonly CT _ct = TestContext.Current.CancellationToken;
private readonly DefaultHttpContext _httpContext;
private readonly HttpContextAccessor _httpContextAccessor;
private readonly RequestLocalizationOptions _requestLocalizationOptions;
@ -34,7 +35,7 @@ public class DefaultUiLocalesServiceTests
{
_httpContextAccessor.HttpContext = null;
await _subject.StoreUiLocalesForRedirectAsync("en-US");
await _subject.StoreUiLocalesForRedirectAsync("en-US", _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -45,7 +46,7 @@ public class DefaultUiLocalesServiceTests
{
_requestLocalizationOptions.RequestCultureProviders.Clear();
await _subject.StoreUiLocalesForRedirectAsync("en-US");
await _subject.StoreUiLocalesForRedirectAsync("en-US", _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -56,7 +57,7 @@ public class DefaultUiLocalesServiceTests
{
_requestLocalizationOptions.SupportedUICultures = new List<CultureInfo> { new("fr-FR") };
await _subject.StoreUiLocalesForRedirectAsync("en-US");
await _subject.StoreUiLocalesForRedirectAsync("en-US", _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -67,7 +68,7 @@ public class DefaultUiLocalesServiceTests
{
_requestLocalizationOptions.SupportedUICultures = new List<CultureInfo> { new("fr-FR") };
await _subject.StoreUiLocalesForRedirectAsync("en-US nb-NO");
await _subject.StoreUiLocalesForRedirectAsync("en-US nb-NO", _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -79,7 +80,7 @@ public class DefaultUiLocalesServiceTests
[InlineData(" ")]
public async Task StoreUiLocalesForRedirectAsync_NullOrWhitespaceUiLocales_DoesNothing(string? uiLocales)
{
await _subject.StoreUiLocalesForRedirectAsync(uiLocales);
await _subject.StoreUiLocalesForRedirectAsync(uiLocales, _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -90,7 +91,7 @@ public class DefaultUiLocalesServiceTests
{
_requestLocalizationOptions.SupportedUICultures = new List<CultureInfo>();
await _subject.StoreUiLocalesForRedirectAsync("en-US");
await _subject.StoreUiLocalesForRedirectAsync("en-US", _ct);
var setCookieHeader = _httpContext.Response.Headers.Where(x => x.Key == "Set-Cookie");
setCookieHeader.ShouldBeEmpty();
@ -102,7 +103,7 @@ public class DefaultUiLocalesServiceTests
var expectedSetCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("en-US")));
_requestLocalizationOptions.SupportedUICultures = new List<CultureInfo> { new("en-US") };
await _subject.StoreUiLocalesForRedirectAsync("en-US");
await _subject.StoreUiLocalesForRedirectAsync("en-US", _ct);
var cookieContainer = new CookieContainer();
var cookies = _httpContext.HttpContext.Response.Headers.Where(x => x.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)).Select(x => x.Value);
@ -122,7 +123,7 @@ public class DefaultUiLocalesServiceTests
new("de-DE")
};
await _subject.StoreUiLocalesForRedirectAsync("en-US fr-FR");
await _subject.StoreUiLocalesForRedirectAsync("en-US fr-FR", _ct);
var cookieContainer = new CookieContainer();
var cookies = _httpContext.HttpContext.Response.Headers.Where(x => x.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)).Select(x => x.Value);
@ -142,7 +143,7 @@ public class DefaultUiLocalesServiceTests
new("de-DE")
};
await _subject.StoreUiLocalesForRedirectAsync("fr-FR en-US");
await _subject.StoreUiLocalesForRedirectAsync("fr-FR en-US", _ct);
var cookieContainer = new CookieContainer();
var cookies = _httpContext.HttpContext.Response.Headers.Where(x => x.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)).Select(x => x.Value);

View file

@ -8,12 +8,14 @@ namespace UnitTests.Services.Default;
public class NumericUserCodeGeneratorTests
{
private readonly CT _ct = TestContext.Current.CancellationToken;
[Fact]
public async Task GenerateAsync_should_return_expected_code()
{
var sut = new NumericUserCodeGenerator();
var userCode = await sut.GenerateAsync();
var userCode = await sut.GenerateAsync(_ct);
var userCodeInt = int.Parse(userCode);
userCodeInt.ShouldBeGreaterThanOrEqualTo(100000000);