diff --git a/web/backend/API/API.Specs/Features/Registration.feature b/web/backend/API/API.Specs/Features/Registration.feature
index edc33671..7af06b8b 100644
--- a/web/backend/API/API.Specs/Features/Registration.feature
+++ b/web/backend/API/API.Specs/Features/Registration.feature
@@ -14,6 +14,7 @@ Scenario: Successful registration with valid details
Scenario: Registration fails with existing username
Given the API is running
+ And I have an existing account
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
@@ -21,6 +22,7 @@ Scenario: Registration fails with existing username
Scenario: Registration fails with existing email
Given the API is running
+ And I have an existing account
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
diff --git a/web/backend/API/API.Specs/Steps/AuthSteps.cs b/web/backend/API/API.Specs/Steps/AuthSteps.cs
index 605859c9..0f4c045e 100644
--- a/web/backend/API/API.Specs/Steps/AuthSteps.cs
+++ b/web/backend/API/API.Specs/Steps/AuthSteps.cs
@@ -83,10 +83,59 @@ public class AuthSteps(ScenarioContext scenario)
);
}
+ private const string FixtureUsername = "test.user";
+ private const string FixturePassword = "Password1!";
+ private const string FixtureEmail = "test.user@thebiergarten.app";
+
+ // Scenarios run in parallel across feature classes, and this fixture is shared by name.
+ // Serializing provisioning avoids two scenarios racing to INSERT the same username/email,
+ // which would otherwise surface as a raw SQL unique-constraint error instead of a clean 409.
+ private static readonly SemaphoreSlim FixtureProvisioningLock = new(1, 1);
+
[Given("I have an existing account")]
- public void GivenIHaveAnExistingAccount()
+ public async Task GivenIHaveAnExistingAccount()
{
- scenario[TestUserKey] = ("test.user", "password");
+ scenario[TestUserKey] = (FixtureUsername, FixturePassword);
+
+ HttpClient client = GetClient();
+ var registrationData = new
+ {
+ username = FixtureUsername,
+ firstName = "Test",
+ lastName = "User",
+ email = FixtureEmail,
+ dateOfBirth = "1990-01-01",
+ password = FixturePassword,
+ };
+
+ HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/register")
+ {
+ Content = new StringContent(
+ JsonSerializer.Serialize(registrationData),
+ Encoding.UTF8,
+ "application/json"
+ ),
+ };
+
+ // Registration is used (rather than relying on seeded data) so the fixture account is
+ // guaranteed to exist regardless of run order. A 409 means another scenario in this run
+ // already provisioned it, which is equally fine.
+ await FixtureProvisioningLock.WaitAsync();
+ HttpResponseMessage response;
+ try
+ {
+ response = await client.SendAsync(requestMessage);
+ }
+ finally
+ {
+ FixtureProvisioningLock.Release();
+ }
+
+ if ((int)response.StatusCode is not (201 or 409))
+ throw new InvalidOperationException(
+ $"Failed to provision fixture account '{FixtureUsername}': "
+ + $"{(int)response.StatusCode} {response.StatusCode}"
+ );
}
[Given("I do not have an existing account")]
@@ -320,7 +369,7 @@ public class AuthSteps(ScenarioContext scenario)
public async Task GivenIAmLoggedIn()
{
HttpClient client = GetClient();
- var loginData = new { username = "test.user", password = "password" };
+ var loginData = new { username = FixtureUsername, password = FixturePassword };
string body = JsonSerializer.Serialize(loginData);
HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/login")
diff --git a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
index 252c9a9e..c5b99ac3 100644
--- a/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
+++ b/web/backend/Features/Features.Auth.Tests/Repository/AuthRepositoryTests.cs
@@ -45,7 +45,7 @@ public class AuthRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
expectedUserId,
@@ -99,7 +99,7 @@ public class AuthRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
userId,
@@ -157,7 +157,7 @@ public class AuthRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
userId,
@@ -210,7 +210,7 @@ public class AuthRepositoryTests
("UserAccountId", typeof(Guid)),
("Hash", typeof(string)),
("CreatedAt", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(credentialId, userId, "hashed_password_value", DateTime.UtcNow, null)
);
diff --git a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
index 2b145b3d..ae861637 100644
--- a/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
+++ b/web/backend/Features/Features.Auth/Repository/AuthRepository.cs
@@ -17,7 +17,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
IAuthRepository
{
private const string SelectColumns =
- "UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, Timer";
+ "UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion";
///
/// The account and credential are inserted within a single database transaction.
@@ -92,7 +92,7 @@ public class AuthRepository(ISqlConnectionFactory connectionFactory)
await using DbConnection connection = await CreateConnection();
return await connection.QueryFirstOrDefaultAsync(
"""
- SELECT UserCredentialId, UserAccountId, Hash, CreatedAt, Timer
+ SELECT UserCredentialId, UserAccountId, Hash, CreatedAt, RowVersion
FROM dbo.UserCredential
WHERE UserAccountId = @UserAccountId AND IsRevoked = 0
""",
diff --git a/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs b/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs
index de217dfa..54fb2d50 100644
--- a/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs
+++ b/web/backend/Features/Features.UserManagement.Tests/Repository/UserAccountRepositoryTests.cs
@@ -29,7 +29,7 @@ public class UserAccountRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
@@ -70,7 +70,7 @@ public class UserAccountRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
Guid.NewGuid(),
@@ -118,7 +118,7 @@ public class UserAccountRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
Guid.NewGuid(),
@@ -155,7 +155,7 @@ public class UserAccountRepositoryTests
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
- ("Timer", typeof(byte[]))
+ ("RowVersion", typeof(byte[]))
)
.AddRow(
Guid.NewGuid(),
diff --git a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs
index c911007e..13c3b26b 100644
--- a/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs
+++ b/web/backend/Features/Features.UserManagement/Repository/UserAccountRepository.cs
@@ -14,7 +14,7 @@ public class UserAccountRepository(ISqlConnectionFactory connectionFactory)
IUserAccountRepository
{
private const string SelectColumns =
- "UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, Timer";
+ "UserAccountID, Username, FirstName, LastName, Email, CreatedAt, UpdatedAt, DateOfBirth, RowVersion";
///
public async Task GetByIdAsync(Guid id)