Add user registration bdd tests

This commit is contained in:
Aaron Po
2026-02-10 23:09:00 -05:00
parent 656981003b
commit 8a4b833943
6 changed files with 315 additions and 282 deletions

View File

@@ -1,69 +1,75 @@
Feature: User Registration
As a new user
I want to register an account
So that I can log in and access authenticated routes
@Ignore
Scenario: Successful registration with valid details
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 201
And the response JSON should have "message" equal "User registered successfully."
And the response JSON should have an access token
@Ignore
Scenario: Registration fails with existing username
Given the API is running
And I have an existing account with username "existinguser"
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| existinguser | Existing | User | existing@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 409
And the response JSON should have "message" equal "Username already exists."
@Ignore
Scenario: Registration fails with existing email
Given the API is running
And I have an existing account with email "existing@example.com"
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | existing@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 409
And the response JSON should have "message" equal "Email already in use."
@Ignore
Scenario: Registration fails with missing required fields
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| | New | User | | | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Username is required."
@Ignore
Scenario: Registration fails with invalid email format
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | invalidemail | 1990-01-01 | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Invalid email format."
@Ignore
Scenario: Registration fails with weak password
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Password does not meet complexity requirements."
@Ignore
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| younguser | Young | User | younguser@example.com | | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "You must be at least 19 years old to register."
@Ignore
Scenario: Registration endpoint only accepts POST requests
Given the API is running
When I submit a registration request using a GET request
Then the response has HTTP status 404
And the response JSON should have "message" equal "Not Found."
As a new user
I want to register an account
So that I can log in and access authenticated routes
Scenario: Successful registration with valid details
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 201
And the response JSON should have "message" equal "User registered successfully."
And the response JSON should have an access token
@Ignore
Scenario: Registration fails with existing username
Given the API is running
And I have an existing account with username "existinguser"
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| existinguser | Existing | User | existing@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 409
And the response JSON should have "message" equal "Username already exists."
@Ignore
Scenario: Registration fails with existing email
Given the API is running
And I have an existing account with email "existing@example.com"
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | existing@example.com | 1990-01-01 | Password1! |
Then the response has HTTP status 409
And the response JSON should have "message" equal "Email already in use."
@Ignore
Scenario: Registration fails with missing required fields
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| | New | User | | | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Username is required."
@Ignore
Scenario: Registration fails with invalid email format
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | invalidemail | 1990-01-01 | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Invalid email format."
@Ignore
Scenario: Registration fails with weak password
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
Then the response has HTTP status 400
And the response JSON should have "message" equal "Password does not meet complexity requirements."
@Ignore
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
Given the API is running
When I submit a registration request with values:
| Username | FirstName | LastName | Email | DateOfBirth | Password |
| younguser | Young | User | younguser@example.com | | Password1! |
Then the response has HTTP status 400
And the response JSON should have "message" equal "You must be at least 19 years old to register."
Scenario: Registration endpoint only accepts POST requests
Given the API is running
When I submit a registration request using a GET request
Then the response has HTTP status 404
And the response JSON should have "message" equal "Not Found."

View File

@@ -157,4 +157,64 @@ public class AuthSteps(ScenarioContext scenario)
scenario[ResponseKey] = response;
scenario[ResponseBodyKey] = responseBody;
}
[When("I submit a registration request with values:")]
public async Task WhenISubmitARegistrationRequestWithValues(Table table)
{
var client = GetClient();
var row = table.Rows[0];
var registrationData = new
{
username = row.TryGetValue("Username", out var value) ? value : null,
firstName = row.TryGetValue("FirstName", out var value1) ? value1 : null,
lastName = row.TryGetValue("LastName", out var value2) ? value2 : null,
email = row.TryGetValue("Email", out var value3) ? value3 : null,
dateOfBirth = row.ContainsKey("DateOfBirth") && !string.IsNullOrEmpty(row["DateOfBirth"])
? row["DateOfBirth"]
: null,
password = row.ContainsKey("Password") ? row["Password"] : null
};
var body = JsonSerializer.Serialize(registrationData);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/register")
{
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
};
var response = await client.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsStringAsync();
scenario[ResponseKey] = response;
scenario[ResponseBodyKey] = responseBody;
}
[Given("I have an existing account with username {string}")]
public void GivenIHaveAnExistingAccountWithUsername(string username)
{
}
[Given("I have an existing account with email {string}")]
public void GivenIHaveAnExistingAccountWithEmail(string email)
{
}
[When("I submit a registration request using a GET request")]
public async Task WhenISubmitARegistrationRequestUsingAGetRequest()
{
var client = GetClient();
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/auth/register")
{
Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json")
};
var response = await client.SendAsync(requestMessage);
var responseBody = await response.Content.ReadAsStringAsync();
scenario[ResponseKey] = response;
scenario[ResponseBodyKey] = responseBody;
}
}

View File

@@ -22,10 +22,10 @@ namespace Repository.Core.Repositories.Auth
{
await using var connection = await CreateConnection();
await using var command = connection.CreateCommand();
command.CommandText = "USP_RegisterUser";
command.CommandType = CommandType.StoredProcedure;
// Input parameters
AddParameter(command, "@Username", username);
AddParameter(command, "@FirstName", firstName);
AddParameter(command, "@LastName", lastName);
@@ -33,11 +33,9 @@ namespace Repository.Core.Repositories.Auth
AddParameter(command, "@DateOfBirth", dateOfBirth);
AddParameter(command, "@Hash", passwordHash);
// Execute and retrieve the generated UserAccountId from result set
var result = await command.ExecuteScalarAsync();
var userAccountId = result != null ? (Guid)result : Guid.Empty;
// Return the newly created user account
return new Entities.UserAccount
{
UserAccountId = userAccountId,
@@ -104,12 +102,6 @@ namespace Repository.Core.Repositories.Auth
await command.ExecuteNonQueryAsync();
}
public async Task InvalidateCredentialsByUserAccountIdAsync(Guid userAccountId)
{
throw new NotImplementedException("InvalidateCredentialsByUserAccountIdAsync");
}
/// <summary>
/// Maps a data reader row to a UserAccount entity.
/// </summary>

View File

@@ -55,12 +55,5 @@ namespace Repository.Core.Repositories.Auth
/// <param name="userAccountId">ID of the user account</param>
/// <param name="newPasswordHash">New hashed password</param>
Task RotateCredentialAsync(Guid userAccountId, string newPasswordHash);
/// <summary>
/// Invalidates all credentials for a user account (e.g., for logout or security purposes).
/// Uses stored procedure: USP_InvalidateUserCredential
/// </summary>
/// <param name="userAccountId">ID of the user account</param>
Task InvalidateCredentialsByUserAccountIdAsync(Guid userAccountId);
}
}

View File

@@ -8,225 +8,212 @@ namespace Repository.Tests.Auth;
public class AuthRepositoryTest
{
private static AuthRepository CreateRepo(MockDbConnection conn)
=> new(new TestConnectionFactory(conn));
private static AuthRepository CreateRepo(MockDbConnection conn)
=> new(new TestConnectionFactory(conn));
[Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
{
var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection();
[Fact]
public async Task RegisterUserAsync_CreatesUserWithCredential_ReturnsUserAccount()
{
var expectedUserId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RegisterUser")
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId));
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RegisterUser")
.ReturnsTable(MockTable.WithColumns(("UserAccountId", typeof(Guid)))
.AddRow(expectedUserId));
var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync(
username: "testuser",
firstName: "Test",
lastName: "User",
email: "test@example.com",
dateOfBirth: new DateTime(1990, 1, 1),
passwordHash: "hashedpassword123"
);
var repo = CreateRepo(conn);
var result = await repo.RegisterUserAsync(
username: "testuser",
firstName: "Test",
lastName: "User",
email: "test@example.com",
dateOfBirth: new DateTime(1990, 1, 1),
passwordHash: "hashedpassword123"
);
result.Should().NotBeNull();
result.UserAccountId.Should().Be(expectedUserId);
result.Username.Should().Be("testuser");
result.FirstName.Should().Be("Test");
result.LastName.Should().Be("User");
result.Email.Should().Be("test@example.com");
result.DateOfBirth.Should().Be(new DateTime(1990, 1, 1));
}
result.Should().NotBeNull();
result.UserAccountId.Should().Be(expectedUserId);
result.Username.Should().Be("testuser");
result.FirstName.Should().Be("Test");
result.LastName.Should().Be("User");
result.Email.Should().Be("test@example.com");
result.DateOfBirth.Should().Be(new DateTime(1990, 1, 1));
}
[Fact]
public async Task GetUserByEmailAsync_ReturnsUser_WhenExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
[Fact]
public async Task GetUserByEmailAsync_ReturnsUser_WhenExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
userId,
"emailuser",
"Email",
"User",
"emailuser@example.com",
DateTime.UtcNow,
null,
new DateTime(1990, 5, 15),
null
));
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
userId,
"emailuser",
"Email",
"User",
"emailuser@example.com",
DateTime.UtcNow,
null,
new DateTime(1990, 5, 15),
null
));
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("emailuser@example.com");
result.Should().NotBeNull();
result!.UserAccountId.Should().Be(userId);
result.Username.Should().Be("emailuser");
result.Email.Should().Be("emailuser@example.com");
result.FirstName.Should().Be("Email");
result.LastName.Should().Be("User");
}
result.Should().NotBeNull();
result!.UserAccountId.Should().Be(userId);
result.Username.Should().Be("emailuser");
result.Email.Should().Be("emailuser@example.com");
result.FirstName.Should().Be("Email");
result.LastName.Should().Be("User");
}
[Fact]
public async Task GetUserByEmailAsync_ReturnsNull_WhenNotExists()
{
var conn = new MockDbConnection();
[Fact]
public async Task GetUserByEmailAsync_ReturnsNull_WhenNotExists()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty());
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByEmail")
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("nonexistent@example.com");
var repo = CreateRepo(conn);
var result = await repo.GetUserByEmailAsync("nonexistent@example.com");
result.Should().BeNull();
}
result.Should().BeNull();
}
[Fact]
public async Task GetUserByUsernameAsync_ReturnsUser_WhenExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
[Fact]
public async Task GetUserByUsernameAsync_ReturnsUser_WhenExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
userId,
"usernameuser",
"Username",
"User",
"username@example.com",
DateTime.UtcNow,
null,
new DateTime(1985, 8, 20),
null
));
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.WithColumns(
("UserAccountId", typeof(Guid)),
("Username", typeof(string)),
("FirstName", typeof(string)),
("LastName", typeof(string)),
("Email", typeof(string)),
("CreatedAt", typeof(DateTime)),
("UpdatedAt", typeof(DateTime?)),
("DateOfBirth", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
userId,
"usernameuser",
"Username",
"User",
"username@example.com",
DateTime.UtcNow,
null,
new DateTime(1985, 8, 20),
null
));
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser");
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("usernameuser");
result.Should().NotBeNull();
result!.UserAccountId.Should().Be(userId);
result.Username.Should().Be("usernameuser");
result.Email.Should().Be("username@example.com");
}
result.Should().NotBeNull();
result!.UserAccountId.Should().Be(userId);
result.Username.Should().Be("usernameuser");
result.Email.Should().Be("username@example.com");
}
[Fact]
public async Task GetUserByUsernameAsync_ReturnsNull_WhenNotExists()
{
var conn = new MockDbConnection();
[Fact]
public async Task GetUserByUsernameAsync_ReturnsNull_WhenNotExists()
{
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.Empty());
conn.Mocks
.When(cmd => cmd.CommandText == "usp_GetUserAccountByUsername")
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("nonexistent");
var repo = CreateRepo(conn);
var result = await repo.GetUserByUsernameAsync("nonexistent");
result.Should().BeNull();
}
result.Should().BeNull();
}
[Fact]
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsCredential_WhenExists()
{
var userId = Guid.NewGuid();
var credentialId = Guid.NewGuid();
var conn = new MockDbConnection();
[Fact]
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsCredential_WhenExists()
{
var userId = Guid.NewGuid();
var credentialId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
.ReturnsTable(MockTable.WithColumns(
("UserCredentialId", typeof(Guid)),
("UserAccountId", typeof(Guid)),
("Hash", typeof(string)),
("CreatedAt", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
credentialId,
userId,
"hashed_password_value",
DateTime.UtcNow,
null
));
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
.ReturnsTable(MockTable.WithColumns(
("UserCredentialId", typeof(Guid)),
("UserAccountId", typeof(Guid)),
("Hash", typeof(string)),
("CreatedAt", typeof(DateTime)),
("Timer", typeof(byte[]))
).AddRow(
credentialId,
userId,
"hashed_password_value",
DateTime.UtcNow,
null
));
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
result.Should().NotBeNull();
result!.UserCredentialId.Should().Be(credentialId);
result.UserAccountId.Should().Be(userId);
result.Hash.Should().Be("hashed_password_value");
}
result.Should().NotBeNull();
result!.UserCredentialId.Should().Be(credentialId);
result.UserAccountId.Should().Be(userId);
result.Hash.Should().Be("hashed_password_value");
}
[Fact]
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsNull_WhenNotExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
[Fact]
public async Task GetActiveCredentialByUserAccountIdAsync_ReturnsNull_WhenNotExists()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
.ReturnsTable(MockTable.Empty());
conn.Mocks
.When(cmd => cmd.CommandText == "USP_GetActiveUserCredentialByUserAccountId")
.ReturnsTable(MockTable.Empty());
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
var repo = CreateRepo(conn);
var result = await repo.GetActiveCredentialByUserAccountIdAsync(userId);
result.Should().BeNull();
}
result.Should().BeNull();
}
[Fact]
public async Task RotateCredentialAsync_ExecutesSuccessfully()
{
var userId = Guid.NewGuid();
var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection();
[Fact]
public async Task RotateCredentialAsync_ExecutesSuccessfully()
{
var userId = Guid.NewGuid();
var newPasswordHash = "new_hashed_password";
var conn = new MockDbConnection();
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1);
conn.Mocks
.When(cmd => cmd.CommandText == "USP_RotateUserCredential")
.ReturnsScalar(1);
var repo = CreateRepo(conn);
var repo = CreateRepo(conn);
// Should not throw
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync();
}
[Fact]
public async Task InvalidateCredentialsByUserAccountIdAsync_ExecutesSuccessfully()
{
var userId = Guid.NewGuid();
var conn = new MockDbConnection();
var repo = CreateRepo(conn);
// Should complete without error
var act = async () => await repo.InvalidateCredentialsByUserAccountIdAsync(userId);
await act.Should().NotThrowAsync();
}
// Should not throw
var act = async () => await repo.RotateCredentialAsync(userId, newPasswordHash);
await act.Should().NotThrowAsync();
}
}

View File

@@ -9,13 +9,13 @@ public class AuthService(
IPasswordService passwordService
) : IAuthService
{
public async Task<UserAccount?> RegisterAsync(UserAccount userAccount, string password)
public async Task<UserAccount> RegisterAsync(UserAccount userAccount, string password)
{
// Check if user already exists
var user = await authRepo.GetUserByUsernameAsync(userAccount.Username);
if (user is not null)
{
return null;
return null!;
}
// password hashing
@@ -45,9 +45,4 @@ public class AuthService(
if (activeCred is null) return null;
return !passwordService.Verify(password, activeCred.Hash) ? null : user;
}
public async Task InvalidateAsync(Guid userAccountId)
{
await authRepo.InvalidateCredentialsByUserAccountIdAsync(userAccountId);
}
}