mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
Make confirmation idempotent and add re-confirmation coverage
This commit is contained in:
@@ -10,6 +10,15 @@ Feature: User Account Confirmation
|
|||||||
Then the response has HTTP status 200
|
Then the response has HTTP status 200
|
||||||
And the response JSON should have "message" containing "is confirmed"
|
And the response JSON should have "message" containing "is confirmed"
|
||||||
|
|
||||||
|
Scenario: Re-confirming an already verified account remains successful
|
||||||
|
Given the API is running
|
||||||
|
And I have registered a new account
|
||||||
|
And I have a valid confirmation token for my account
|
||||||
|
When I submit a confirmation request with the valid token
|
||||||
|
And I submit the same confirmation request again
|
||||||
|
Then the response has HTTP status 200
|
||||||
|
And the response JSON should have "message" containing "is confirmed"
|
||||||
|
|
||||||
Scenario: Confirmation fails with invalid token
|
Scenario: Confirmation fails with invalid token
|
||||||
Given the API is running
|
Given the API is running
|
||||||
When I submit a confirmation request with an invalid token
|
When I submit a confirmation request with an invalid token
|
||||||
|
|||||||
@@ -599,6 +599,25 @@ public class AuthSteps(ScenarioContext scenario)
|
|||||||
scenario[ResponseBodyKey] = responseBody;
|
scenario[ResponseBodyKey] = responseBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[When("I submit the same confirmation request again")]
|
||||||
|
public async Task WhenISubmitTheSameConfirmationRequestAgain()
|
||||||
|
{
|
||||||
|
var client = GetClient();
|
||||||
|
var token = scenario.TryGetValue<string>("confirmationToken", out var t)
|
||||||
|
? t
|
||||||
|
: "valid-token";
|
||||||
|
|
||||||
|
var requestMessage = new HttpRequestMessage(
|
||||||
|
HttpMethod.Post,
|
||||||
|
$"/api/auth/confirm?token={Uri.EscapeDataString(token)}"
|
||||||
|
);
|
||||||
|
|
||||||
|
var response = await client.SendAsync(requestMessage);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
|
scenario[ResponseKey] = response;
|
||||||
|
scenario[ResponseBodyKey] = responseBody;
|
||||||
|
}
|
||||||
|
|
||||||
[When("I submit a confirmation request with a malformed token")]
|
[When("I submit a confirmation request with a malformed token")]
|
||||||
public async Task WhenISubmitAConfirmationRequestWithAMalformedToken()
|
public async Task WhenISubmitAConfirmationRequestWithAMalformedToken()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Data;
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Domain.Entities;
|
using Domain.Entities;
|
||||||
using Infrastructure.Repository.Sql;
|
using Infrastructure.Repository.Sql;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
namespace Infrastructure.Repository.Auth;
|
namespace Infrastructure.Repository.Auth;
|
||||||
|
|
||||||
@@ -132,6 +133,12 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idempotency: if already verified, treat as successful confirmation.
|
||||||
|
if (await IsUserVerifiedAsync(userAccountId))
|
||||||
|
{
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
await using var connection = await CreateConnection();
|
await using var connection = await CreateConnection();
|
||||||
await using var command = connection.CreateCommand();
|
await using var command = connection.CreateCommand();
|
||||||
command.CommandText = "USP_CreateUserVerification";
|
command.CommandText = "USP_CreateUserVerification";
|
||||||
@@ -139,12 +146,39 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
|
|||||||
|
|
||||||
AddParameter(command, "@UserAccountID_", userAccountId);
|
AddParameter(command, "@UserAccountID_", userAccountId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
catch (SqlException ex) when (IsDuplicateVerificationViolation(ex))
|
||||||
|
{
|
||||||
|
// A concurrent request verified this user first. Keep behavior idempotent.
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch and return the updated user
|
// Fetch and return the updated user
|
||||||
return await GetUserByIdAsync(userAccountId);
|
return await GetUserByIdAsync(userAccountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsUserVerifiedAsync(Guid userAccountId)
|
||||||
|
{
|
||||||
|
await using var connection = await CreateConnection();
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText =
|
||||||
|
"SELECT TOP 1 1 FROM dbo.UserVerification WHERE UserAccountID = @UserAccountID";
|
||||||
|
command.CommandType = CommandType.Text;
|
||||||
|
|
||||||
|
AddParameter(command, "@UserAccountID", userAccountId);
|
||||||
|
|
||||||
|
var result = await command.ExecuteScalarAsync();
|
||||||
|
return result != null && result != DBNull.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDuplicateVerificationViolation(SqlException ex)
|
||||||
|
{
|
||||||
|
// 2601/2627 are duplicate key violations in SQL Server.
|
||||||
|
return ex.Number == 2601 || ex.Number == 2627;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maps a data reader row to a UserAccount entity.
|
/// Maps a data reader row to a UserAccount entity.
|
||||||
|
|||||||
Reference in New Issue
Block a user