Fix code analysis warnings

This commit is contained in:
Aaron Po
2026-08-21 23:53:18 -04:00
parent 7bcadc5ae4
commit f015ac8025
19 changed files with 9209 additions and 101 deletions

View File

@@ -8,6 +8,13 @@
"csharpier" "csharpier"
], ],
"rollForward": false "rollForward": false
},
"jetbrains.resharper.globaltools": {
"version": "2026.2.1",
"commands": [
"jb"
],
"rollForward": false
} }
} }
} }

View File

@@ -83,4 +83,4 @@ public class JwtAuthenticationHandler(
/// <summary> /// <summary>
/// Options for the <c>JWT</c> authentication scheme handled by <see cref="JwtAuthenticationHandler" />. /// Options for the <c>JWT</c> authentication scheme handled by <see cref="JwtAuthenticationHandler" />.
/// </summary> /// </summary>
public class JwtAuthenticationOptions : AuthenticationSchemeOptions { } public class JwtAuthenticationOptions : AuthenticationSchemeOptions;

View File

@@ -413,7 +413,7 @@ public class AuthSteps(ScenarioContext scenario)
[Given("I have a valid access token for my account")] [Given("I have a valid access token for my account")]
public void GivenIHaveAValidAccessTokenForMyAccount() public void GivenIHaveAValidAccessTokenForMyAccount()
{ {
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: throw new InvalidOperationException("registered user ID not found in scenario"); : throw new InvalidOperationException("registered user ID not found in scenario");
string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user) string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user)
@@ -432,7 +432,7 @@ public class AuthSteps(ScenarioContext scenario)
[Given("I have a valid confirmation token for my account")] [Given("I have a valid confirmation token for my account")]
public void GivenIHaveAValidConfirmationTokenForMyAccount() public void GivenIHaveAValidConfirmationTokenForMyAccount()
{ {
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: throw new InvalidOperationException("registered user ID not found in scenario"); : throw new InvalidOperationException("registered user ID not found in scenario");
string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user) string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user)
@@ -451,7 +451,7 @@ public class AuthSteps(ScenarioContext scenario)
[Given("I have an expired confirmation token for my account")] [Given("I have an expired confirmation token for my account")]
public void GivenIHaveAnExpiredConfirmationTokenForMyAccount() public void GivenIHaveAnExpiredConfirmationTokenForMyAccount()
{ {
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: throw new InvalidOperationException("registered user ID not found in scenario"); : throw new InvalidOperationException("registered user ID not found in scenario");
string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user) string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user)
@@ -470,7 +470,7 @@ public class AuthSteps(ScenarioContext scenario)
[Given("I have a confirmation token signed with the wrong secret")] [Given("I have a confirmation token signed with the wrong secret")]
public void GivenIHaveAConfirmationTokenSignedWithTheWrongSecret() public void GivenIHaveAConfirmationTokenSignedWithTheWrongSecret()
{ {
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: throw new InvalidOperationException("registered user ID not found in scenario"); : throw new InvalidOperationException("registered user ID not found in scenario");
string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user) string? username = scenario.TryGetValue<string>(RegisteredUsernameKey, out string? user)
@@ -668,7 +668,7 @@ public class AuthSteps(ScenarioContext scenario)
} }
[When("I submit a refresh token request using a GET request")] [When("I submit a refresh token request using a GET request")]
public async Task WhenISubmitARefreshTokenRequestUsingAGETRequest() public async Task WhenISubmitARefreshTokenRequestUsingAGetRequest()
{ {
HttpClient client = GetClient(); HttpClient client = GetClient();
HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/auth/refresh") HttpRequestMessage requestMessage = new(HttpMethod.Get, "/api/auth/refresh")
@@ -979,7 +979,7 @@ public class AuthSteps(ScenarioContext scenario)
public async Task WhenISubmitAResendConfirmationRequestForMyAccount() public async Task WhenISubmitAResendConfirmationRequestForMyAccount()
{ {
HttpClient client = GetClient(); HttpClient client = GetClient();
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: throw new InvalidOperationException("registered user ID not found"); : throw new InvalidOperationException("registered user ID not found");
string? accessToken = scenario.TryGetValue<string>("accessToken", out string? at) string? accessToken = scenario.TryGetValue<string>("accessToken", out string? at)
@@ -1025,7 +1025,7 @@ public class AuthSteps(ScenarioContext scenario)
public async Task WhenISubmitAResendConfirmationRequestWithoutAnAccessToken() public async Task WhenISubmitAResendConfirmationRequestWithoutAnAccessToken()
{ {
HttpClient client = GetClient(); HttpClient client = GetClient();
Guid userId = scenario.TryGetValue<Guid>(RegisteredUserIdKey, out Guid id) Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id)
? id ? id
: Guid.NewGuid(); : Guid.NewGuid();

View File

@@ -3,7 +3,6 @@ using Database.Seed.PipelineData;
using Database.Seed.Sqlite; using Database.Seed.Sqlite;
using Domain.Entities; using Domain.Entities;
using Features.Auth.DependencyInjection; using Features.Auth.DependencyInjection;
using Features.Auth.Dtos;
using Features.Auth.Repository; using Features.Auth.Repository;
using Features.Breweries.DependencyInjection; using Features.Breweries.DependencyInjection;
using Features.Breweries.Repository; using Features.Breweries.Repository;
@@ -255,7 +254,7 @@ static Table BuildUserTable(IReadOnlyList<UserRecord> users)
user.User.FirstName, user.User.FirstName,
user.User.LastName, user.User.LastName,
user.Email, user.Email,
user.DateOfBirth.ToString() user.DateOfBirth
); );
} }

View File

@@ -13,8 +13,8 @@ public sealed class BeerPost
// Attributes // Attributes
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
public decimal ABV { get; set; } public decimal Abv { get; set; }
public int IBU { get; set; } public int Ibu { get; set; }
// Audit Fields // Audit Fields
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

View File

@@ -8,7 +8,7 @@ public sealed class Country
// Attributes // Attributes
public string CountryName { get; set; } = string.Empty; public string CountryName { get; set; } = string.Empty;
public string ISO3166_1 { get; set; } = string.Empty; // this is the iso 3166-1 alpha-2 code for the country, e.g. "US" for United States, "CA" for Canada, etc. public string Iso31661 { get; set; } = string.Empty; // this is the iso 3166-1 alpha-2 code for the country, e.g. "US" for United States, "CA" for Canada, etc.
// Audit Fields // Audit Fields
public byte[]? RowVersion { get; set; } public byte[]? RowVersion { get; set; }

View File

@@ -13,7 +13,7 @@ public sealed class StateProvince
// this is the iso 3166-2 code for the state/province, e.g. "CA" for California, "ON" for Ontario, etc. // this is the iso 3166-2 code for the state/province, e.g. "CA" for California, "ON" for Ontario, etc.
// and is rendered like US-CA for California, CA-ON for Ontario, etc. // and is rendered like US-CA for California, CA-ON for Ontario, etc.
public string ISO3166_2 { get; set; } = string.Empty; public string Iso31662 { get; set; } = string.Empty;
// Audit Fields // Audit Fields
public byte[]? RowVersion { get; set; } public byte[]? RowVersion { get; set; }

View File

@@ -11,14 +11,12 @@ namespace Features.Auth.Tests.Services;
public class TokenServiceValidationTests public class TokenServiceValidationTests
{ {
private readonly Mock<IAuthRepository> _authRepositoryMock;
private readonly Mock<ITokenInfrastructure> _tokenInfraMock; private readonly Mock<ITokenInfrastructure> _tokenInfraMock;
private readonly TokenService _tokenService; private readonly TokenService _tokenService;
public TokenServiceValidationTests() public TokenServiceValidationTests()
{ {
_tokenInfraMock = new Mock<ITokenInfrastructure>(); _tokenInfraMock = new Mock<ITokenInfrastructure>();
_authRepositoryMock = new Mock<IAuthRepository>();
// Set environment variables for tokens // Set environment variables for tokens
Environment.SetEnvironmentVariable( Environment.SetEnvironmentVariable(
@@ -34,7 +32,7 @@ public class TokenServiceValidationTests
"test-confirmation-secret-that-is-very-long-1234567890" "test-confirmation-secret-that-is-very-long-1234567890"
); );
_tokenService = new TokenService(_tokenInfraMock.Object, _authRepositoryMock.Object); _tokenService = new TokenService(_tokenInfraMock.Object, new Mock<IAuthRepository>().Object);
} }
[Fact] [Fact]

View File

@@ -2,7 +2,6 @@ using System.Data.Common;
using Dapper; using Dapper;
using Domain.Entities; using Domain.Entities;
using Domain.Exceptions; using Domain.Exceptions;
using Features.Auth.Dtos;
using Infrastructure.Sql; using Infrastructure.Sql;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
@@ -46,7 +45,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
INSERT INTO dbo.UserCredential (UserAccountId, Hash) INSERT INTO dbo.UserCredential (UserAccountId, Hash)
VALUES (@UserAccountId, @Hash); VALUES (@UserAccountId, @Hash);
""", """,
new { UserAccountId = userAccountId, Hash = ua.UserCredential?.Hash }, new { UserAccountId = userAccountId, ua.UserCredential?.Hash },
transaction transaction
) )
); );

View File

@@ -1,5 +1,4 @@
using Domain.Entities; using Domain.Entities;
using Features.Auth.Dtos;
namespace Features.Auth.Repository; namespace Features.Auth.Repository;

View File

@@ -62,10 +62,6 @@ public interface ITokenService
/// </summary> /// </summary>
string GenerateConfirmationToken(UserAccount user); string GenerateConfirmationToken(UserAccount user);
/// <summary>Generates a token of the type specified by <typeparamref name="T" />.</summary>
/// <typeparam name="T">Must be <see cref="TokenType" />.</typeparam>
string GenerateToken<T>(UserAccount user)
where T : struct, Enum;
/// <summary>Validates an access token's signature and expiration and extracts the caller's identity.</summary> /// <summary>Validates an access token's signature and expiration and extracts the caller's identity.</summary>
/// <exception cref="Domain.Exceptions.UnauthorizedException"> /// <exception cref="Domain.Exceptions.UnauthorizedException">

View File

@@ -88,30 +88,6 @@ public class TokenService : ITokenService
); );
} }
/// <inheritdoc />
/// <exception cref="InvalidOperationException">
/// Thrown when <typeparamref name="T" /> is not <see cref="TokenType" /> or does not resolve to a known token type.
/// </exception>
public string GenerateToken<T>(UserAccount user)
where T : struct, Enum
{
if (typeof(T) != typeof(TokenType))
throw new InvalidOperationException("Invalid token type");
string tokenTypeName = typeof(T).Name;
if (!Enum.TryParse(typeof(TokenType), tokenTypeName, out object? parsed))
throw new InvalidOperationException("Invalid token type");
TokenType tokenType = (TokenType)parsed;
return tokenType switch
{
TokenType.AccessToken => GenerateAccessToken(user),
TokenType.RefreshToken => GenerateRefreshToken(user),
TokenType.ConfirmationToken => GenerateConfirmationToken(user),
_ => throw new InvalidOperationException("Invalid token type"),
};
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<ValidatedToken> ValidateAccessTokenAsync(string token) public async Task<ValidatedToken> ValidateAccessTokenAsync(string token)
{ {

View File

@@ -29,7 +29,7 @@ public class GetBreweryByIdHandlerTests
); );
result.Should().NotBeNull(); result.Should().NotBeNull();
result!.BreweryPostId.Should().Be(brewery.BreweryPostId); result.BreweryPostId.Should().Be(brewery.BreweryPostId);
} }
[Fact] [Fact]

View File

@@ -25,7 +25,6 @@ public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
RuleFor(x => x.Location.CityId) RuleFor(x => x.Location.CityId)
.NotEmpty() .NotEmpty()
.When(x => x.Location is not null)
.WithMessage("CityId is required."); .WithMessage("CityId is required.");
RuleFor(x => x.Location.AddressLine1) RuleFor(x => x.Location.AddressLine1)
@@ -33,15 +32,12 @@ public class CreateBreweryValidator : AbstractValidator<CreateBreweryCommand>
.When(x => x.Location is not null) .When(x => x.Location is not null)
.WithMessage("Address line 1 is required.") .WithMessage("Address line 1 is required.")
.MaximumLength(256) .MaximumLength(256)
.When(x => x.Location is not null)
.WithMessage("Address line 1 cannot exceed 256 characters."); .WithMessage("Address line 1 cannot exceed 256 characters.");
RuleFor(x => x.Location.PostalCode) RuleFor(x => x.Location.PostalCode)
.NotEmpty() .NotEmpty()
.When(x => x.Location is not null)
.WithMessage("Postal code is required.") .WithMessage("Postal code is required.")
.MaximumLength(20) .MaximumLength(20)
.When(x => x.Location is not null)
.WithMessage("Postal code cannot exceed 20 characters."); .WithMessage("Postal code cannot exceed 20 characters.");
} }
} }

View File

@@ -27,11 +27,11 @@ public static class BreweryDtoMapper
CityName: brewery.Location.City?.CityName ?? string.Empty, CityName: brewery.Location.City?.CityName ?? string.Empty,
StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName StateProvinceName: brewery.Location.City?.StateProvince?.StateProvinceName
?? string.Empty, ?? string.Empty,
StateProvinceCode: brewery.Location.City?.StateProvince?.ISO3166_2 StateProvinceCode: brewery.Location.City?.StateProvince?.Iso31662
?? string.Empty, ?? string.Empty,
CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName CountryName: brewery.Location.City?.StateProvince?.Country?.CountryName
?? string.Empty, ?? string.Empty,
CountryCode: brewery.Location.City?.StateProvince?.Country?.ISO3166_1 CountryCode: brewery.Location.City?.StateProvince?.Country?.Iso31661
?? string.Empty, ?? string.Empty,
AddressLine1: brewery.Location.AddressLine1, AddressLine1: brewery.Location.AddressLine1,
AddressLine2: brewery.Location.AddressLine2, AddressLine2: brewery.Location.AddressLine2,

View File

@@ -36,7 +36,7 @@ public class EmailDispatcher(
confirmationLink confirmationLink
); );
await emailProvider.SendAsync(email, "Welcome to The Biergarten App!", emailHtml, true); await emailProvider.SendAsync(email, "Welcome to The Biergarten App!", emailHtml);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -59,7 +59,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
""", """,
new new
{ {
UserAccountId = userAccount.UserAccountId, userAccount.UserAccountId,
userAccount.Username, userAccount.Username,
userAccount.FirstName, userAccount.FirstName,
userAccount.LastName, userAccount.LastName,

View File

@@ -1,44 +1 @@
namespace Infrastructure.Sql; namespace Infrastructure.Sql;
/// <summary>
/// Custom SQL Server error numbers (50000+) that a stored procedure could use to signal
/// domain-level failures via <c>THROW</c>, for a repository to catch as
/// <see cref="Microsoft.Data.SqlClient.SqlException" /> and translate via <c>ex.Number</c> into
/// the corresponding <c>Domain.Exceptions</c> type.
/// </summary>
/// <remarks>
/// Currently unused: the schema has no stored procedures, and no repository throws or catches on
/// these numbers. Repositories instead run their own existence/concurrency checks and throw
/// <c>Domain.Exceptions</c> directly (see <c>docs/website/database.md</c> for the current
/// approach). This type is kept in case a future stored procedure reintroduces the pattern; each
/// number would
/// identify a category of error shared by every procedure that raises it, not a single call site.
/// </remarks>
public static class SqlErrorCodes
{
/// <summary>
/// A stored procedure's own input validation failed (e.g. a required parameter was <c>NULL</c>).
/// Maps to HTTP 400 Bad Request.
/// </summary>
public const int Validation = 50400;
/// <summary>
/// A stored procedure could not find a row it required, by primary key or foreign key. Maps to
/// HTTP 404 Not Found.
/// </summary>
public const int NotFound = 50404;
/// <summary>
/// A stored procedure detected a conflict: either a duplicate/already-exists row, or a stale
/// optimistic-concurrency token (row-version mismatch). Maps to HTTP 409 Conflict.
/// </summary>
public const int Conflict = 50409;
/// <summary>
/// A defensive check inside a stored procedure failed under conditions that should be
/// unreachable in normal operation (e.g. an <c>INSERT</c> that should always affect one row
/// affected zero). Left uncaught by repositories so it falls through to the generic
/// <see cref="Microsoft.Data.SqlClient.SqlException" /> handler. Maps to HTTP 500/503.
/// </summary>
public const int InternalFailure = 50500;
}

9181
web/backend/report.sarif Normal file

File diff suppressed because it is too large Load Diff