update tests

This commit is contained in:
Aaron Po
2026-01-31 15:41:00 -05:00
parent 9474fb7811
commit 954e224c34
5 changed files with 140 additions and 55 deletions

View File

@@ -1,3 +1,4 @@
using System.Net;
using DataAccessLayer.Entities;
using Microsoft.AspNetCore.Mvc;
using ServiceCore.Services;
@@ -17,7 +18,13 @@ namespace WebAPI.Controllers
string Password
);
public record LoginRequest(string UsernameOrEmail, string Password);
public record LoginRequest
{
public string Username { get; init; } = default!;
public string Password { get; init; } = default!;
}
private record ResponseBody(string Message, object? Payload);
[HttpPost("register")]
public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req)
@@ -39,15 +46,15 @@ namespace WebAPI.Controllers
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest req)
{
var userAccount = await auth.LoginAsync(req.UsernameOrEmail, req.Password);
var userAccount = await auth.LoginAsync(req.Username, req.Password);
if (userAccount is null)
{
return Unauthorized();
return Unauthorized(new ResponseBody("Invalid username or password.", null));
}
var jwt = jwtService.GenerateJwt(userAccount.UserAccountId, userAccount.Username, userAccount.DateOfBirth);
return Ok(new { AccessToken = jwt, Message = "Logged in successfully." });
return Ok(new ResponseBody("Logged in successfully.", new { AccessToken = jwt }));
}
}
}

View File

@@ -6,7 +6,29 @@ So that I receive an authentication token to access authenticated routes
Scenario: Successful login with valid credentials
Given the API is running
And I have an existing account
And I submit a login request with a valid username and password
When I submit a login request with a username and password
Then the response has HTTP status 200
And the response JSON should have "message" equal "Logged in successfully."
And the response JSON should have a valid access token.
And the response JSON should have an access token
Scenario: Login fails with invalid credentials
Given the API is running
And I do not have an existing account
When I submit a login request with a username and password
Then the response has HTTP status 401
And the response JSON should have "message" equal "Invalid username or password."
Scenario: Login fails when required missing username
Given the API is running
When I submit a login request with a missing username
Then the response has HTTP status 400
Scenario: Login fails when required missing password
Given the API is running
When I submit a login request with a missing password
Then the response has HTTP status 400
Scenario: Login fails when both username and password are missing
Given the API is running
When I submit a login request with both username and password missing
Then the response has HTTP status 400

View File

@@ -1,7 +1,9 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Reqnroll;
using FluentAssertions;
using System.IdentityModel.Tokens.Jwt;
namespace API.Specs.Steps;
@@ -10,9 +12,11 @@ public class ApiSteps
{
private readonly TestApiFactory _factory = new();
private HttpClient? _client;
private HttpResponseMessage? _response;
private (string username, string password) testUser;
private HttpResponseMessage? _response;
private string? _responseBody;
private (string username, string password) _testUser;
[Given("the API is running")]
public void GivenTheApiIsRunning()
@@ -29,17 +33,27 @@ public class ApiSteps
}
[Then("the response JSON should have {string} equal {string}")]
public async Task ThenResponseJsonShouldHaveFieldEqual(string field, string expected)
public void ThenTheResponseJsonShouldHaveStringEqualString(string field, string expected)
{
_response.Should().NotBeNull();
var dict = await _response!.Content.ReadFromJsonAsync<Dictionary<string, object>>();
dict.Should().NotBeNull();
dict!.TryGetValue(field, out var value).Should().BeTrue();
(value?.ToString()).Should().Be(expected);
_responseBody.Should().NotBeNull();
using var doc = JsonDocument.Parse(_responseBody!);
var root = doc.RootElement;
if (!root.TryGetProperty(field, out var value))
{
root.TryGetProperty("payload", out var payloadElem).Should().BeTrue("Expected field '{0}' to be present either at the root or inside 'payload'", field);
payloadElem.ValueKind.Should().Be(JsonValueKind.Object, "payload must be an object");
payloadElem.TryGetProperty(field, out value).Should().BeTrue("Expected field '{0}' to be present inside 'payload'", field);
}
value.ValueKind.Should().Be(JsonValueKind.String, "Expected field '{0}' to be a string", field);
value.GetString().Should().Be(expected);
}
[When("I send an HTTP request {string} to {string} with body:")]
public async Task WhenISendAnHttpRequestToWithBody(string method, string url, string jsonBody)
public async Task WhenISendAnHttpRequestStringToStringWithBody(string method, string url, string jsonBody)
{
_client.Should().NotBeNull();
@@ -49,7 +63,10 @@ public class ApiSteps
Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json")
};
_response = await _client!.SendAsync(requestMessage);
_responseBody = await _response.Content.ReadAsStringAsync();
}
[When("I send an HTTP request {string} to {string}")]
@@ -57,6 +74,7 @@ public class ApiSteps
{
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url);
_response = await _client!.SendAsync(requestMessage);
_responseBody = await _response.Content.ReadAsStringAsync();
}
[Then("the response has HTTP status {int}")]
@@ -70,30 +88,78 @@ public class ApiSteps
[Given("I have an existing account")]
public void GivenIHaveAnExistingAccount()
{
testUser = ("test.user", "password");
_testUser = ("test.user", "password");
}
[Given("I submit a login request with a valid username and password")]
public async Task GivenISubmitALoginRequestWithAValidUsernameAndPassword()
[When("I submit a login request with a username and password")]
public async Task WhenISubmitALoginRequestWithAUsernameAndPassword()
{
await WhenISendAnHttpRequestToWithBody("POST", "/api/v1/account/login", $@"
await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", $@"
{{
""username"": ""{testUser.username}"",
""password"": ""{testUser.password}""
""username"": ""{_testUser.username}"",
""password"": ""{_testUser.password}""
}}");
}
[Then("the response JSON should have a valid access token.")]
public async Task ThenTheResponseJsonShouldHaveAValidAccessToken()
[Then("the response JSON should have an access token")]
public void ThenTheResponseJsonShouldHaveAnAccessToken()
{
var dict = await _response!.Content.ReadFromJsonAsync<Dictionary<string, object>>();
dict.Should().NotBeNull();
_response.Should().NotBeNull();
_responseBody.Should().NotBeNull();
dict!.TryGetValue("AccessToken", out var value).Should().BeTrue();
var messageStr = value!.ToString();
Console.WriteLine(messageStr);
using var doc = JsonDocument.Parse(_responseBody!);
var root = doc.RootElement;
JsonElement tokenElem;
var hasToken = root.TryGetProperty("accessToken", out tokenElem)
|| root.TryGetProperty("AccessToken", out tokenElem);
if (!hasToken)
{
if (root.TryGetProperty("payload", out var payloadElem) && payloadElem.ValueKind == JsonValueKind.Object)
{
hasToken = payloadElem.TryGetProperty("accessToken", out tokenElem)
|| payloadElem.TryGetProperty("AccessToken", out tokenElem);
}
}
hasToken.Should().BeTrue("Expected an access token either at the root or inside 'payload'");
var token = tokenElem.GetString();
// @todo validate the token
token.Should().NotBeNullOrEmpty();
}
[Given("I do not have an existing account")]
public void GivenIDoNotHaveAnExistingAccount()
{
_testUser = ("Failing", "User");
}
[When("I submit a login request with a missing username")]
public async Task WhenISubmitALoginRequestWithAMissingUsername()
{
await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", $@"
{{
""password"": ""test""
}}");
}
[When("I submit a login request with a missing password")]
public async Task WhenISubmitALoginRequestWithAMissingPassword()
{
await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", $@"
{{
""username"": ""test""
}}");
}
[When("I submit a login request with both username and password missing")]
public async Task WhenISubmitALoginRequestWithBothUsernameAndPasswordMissing()
{
await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", "{}");
}
}

View File

@@ -10,29 +10,19 @@ namespace ServiceCore.Services
throw new NotImplementedException();
}
public async Task<UserAccount?> LoginAsync(string usernameOrEmail, string password)
public async Task<UserAccount?> LoginAsync(string username, string password)
{
// Attempt lookup by username, then email
var user = await userRepo.GetByUsernameAsync(usernameOrEmail)
?? await userRepo.GetByEmailAsync(usernameOrEmail);
// the user was not found
if (user is null)
{
return null;
}
// Attempt lookup by username
var user = await userRepo.GetByUsernameAsync(username);
// the user was not found
if (user is null) return null;
// they don't have an active credential
// @todo handle expired passwords
var activeCred = await credRepo.GetActiveCredentialByUserAccountIdAsync(user.UserAccountId);
if (activeCred is null)
{
return null;
}
if (!PasswordHasher.Verify(password, activeCred.Hash))
{
return null;
}
if (activeCred is null) return null;
if (!PasswordHasher.Verify(password, activeCred.Hash)) return null;
return user;
}

View File

@@ -5,6 +5,6 @@ namespace ServiceCore.Services
public interface IAuthService
{
Task<UserAccount> RegisterAsync(UserAccount userAccount, string password);
Task<UserAccount?> LoginAsync(string usernameOrEmail, string password);
Task<UserAccount?> LoginAsync(string username, string password);
}
}