Files
step3/api/QuestionsApi.Tests/AuthTests.cs
T

139 lines
5.4 KiB
C#

using Microsoft.Extensions.Configuration;
using QuestionsApi.Models;
using QuestionsApi.Services;
using System.IdentityModel.Tokens.Jwt;
using Xunit;
namespace QuestionsApi.Tests;
public sealed class AuthTests
{
[Theory]
[InlineData("valid_user", "[email protected]", "password1", null)]
[InlineData("ab", "[email protected]", "password1", "Username")]
[InlineData("invalid-user", "[email protected]", "password1", "Username")]
[InlineData("valid_user", "not-an-email", "password1", "email")]
[InlineData("valid_user", "[email protected]", "short", "Password")]
public void NewAccountValidationRejectsInvalidInput(
string username, string email, string password, string? expectedError)
{
var result = AuthValidation.ValidateNewAccount(username, email, password);
if (expectedError is null) Assert.Null(result);
else Assert.Contains(expectedError, result!, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void PasswordResetCodesAreSixDigitsAndBoundToTheirRequest()
{
var service = new PasswordResetCodeService(Configuration(new Dictionary<string, string?>
{
["PasswordReset:HmacKey"] = "A-test-only-key-that-is-longer-than-thirty-two-bytes"
}));
var code = service.GenerateCode();
var hash = service.Hash("request-one", code);
Assert.Matches("^[0-9]{6}$", code);
Assert.True(service.Verify("request-one", code, hash));
Assert.False(service.Verify("request-two", code, hash));
Assert.False(service.Verify("request-one", "000000", hash));
}
[Fact]
public void PasswordResetCanRemainUnconfiguredWithoutBreakingAuthentication()
{
var service = new PasswordResetCodeService(Configuration([]));
Assert.False(service.IsConfigured);
}
[Fact]
public void JwtContainsCompatibleIdentityRoleAndAuthVersionClaims()
{
var service = new JwtTokenService(Configuration(new Dictionary<string, string?>
{
["Jwt:Key"] = "A-test-only-jwt-key-that-is-longer-than-thirty-two-bytes",
["Jwt:Issuer"] = "promiscuity",
["Jwt:Audience"] = "promiscuity-auth-api"
}));
var user = new AuthUser
{
Id = "507f1f77bcf86cd799439011",
Username = "doctor",
Role = "SUPER",
AuthVersion = 4
};
var generated = service.Generate(user);
var token = new JwtSecurityTokenHandler().ReadJwtToken(generated.Token);
Assert.Equal("promiscuity", token.Issuer);
Assert.Contains("promiscuity-auth-api", token.Audiences);
Assert.Equal("4", token.Claims.Single(claim => claim.Type == "auth_ver").Value);
Assert.Equal(generated.Jti, token.Id);
}
[Fact]
public void LegacyUsersDefaultToAuthVersionZero()
{
Assert.Equal(0, new AuthUser().AuthVersion);
}
[Fact]
public void SteamClaimedIdExtractionRejectsUnexpectedHostsAndLengths()
{
Assert.Equal("76561198000000000",
SteamOpenIdService.ExtractSteamId("https://steamcommunity.com/openid/id/76561198000000000"));
Assert.Null(SteamOpenIdService.ExtractSteamId("https://example.com/openid/id/76561198000000000"));
Assert.Null(SteamOpenIdService.ExtractSteamId("https://steamcommunity.com/openid/id/123"));
}
[Fact]
public void GoogleProviderIsDisabledUntilAllSecureConfigurationExists()
{
var incomplete = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
{
["Google:ClientId"] = "client-id",
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
}));
var complete = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
{
["Google:ClientId"] = "client-id",
["Google:ClientSecret"] = "client-secret",
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
}));
Assert.False(incomplete.IsConfigured);
Assert.True(complete.IsConfigured);
}
[Fact]
public void GoogleAuthorizationUsesStateNonceAndPkce()
{
var provider = new GoogleExternalAuthProvider(new TestHttpClientFactory(), Configuration(new Dictionary<string, string?>
{
["Google:ClientId"] = "client-id",
["Google:ClientSecret"] = "client-secret",
["Google:ReturnUrl"] = "https://example.test/api/Auth/external/google/callback"
}));
var url = provider.BuildAuthorizationUrl(new ExternalAuthTransaction
{
Id = "transaction-state",
Nonce = "transaction-nonce",
CodeVerifier = "a-long-test-code-verifier"
});
Assert.Contains("state=transaction-state", url);
Assert.Contains("nonce=transaction-nonce", url);
Assert.Contains("code_challenge_method=S256", url);
Assert.Contains("scope=openid%20email%20profile", url);
Assert.DoesNotContain("client-secret", url);
}
private static IConfiguration Configuration(Dictionary<string, string?> values) =>
new ConfigurationBuilder().AddInMemoryCollection(values).Build();
private sealed class TestHttpClientFactory : IHttpClientFactory
{
public HttpClient CreateClient(string name) => new();
}
}