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 DataAccessLayer.Entities;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using ServiceCore.Services; using ServiceCore.Services;
@@ -17,7 +18,13 @@ namespace WebAPI.Controllers
string Password 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")] [HttpPost("register")]
public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req) public async Task<ActionResult<UserAccount>> Register([FromBody] RegisterRequest req)
@@ -39,15 +46,15 @@ namespace WebAPI.Controllers
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest req) 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) 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); 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

@@ -1,12 +1,34 @@
Feature: User Login Feature: User Login
As a registered user As a registered user
I want to log in to my account I want to log in to my account
So that I receive an authentication token to access authenticated routes So that I receive an authentication token to access authenticated routes
Scenario: Successful login with valid credentials Scenario: Successful login with valid credentials
Given the API is running Given the API is running
And I have an existing account 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 Then the response has HTTP status 200
And the response JSON should have "message" equal "Logged in successfully." 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;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json;
using Reqnroll; using Reqnroll;
using FluentAssertions; using FluentAssertions;
using System.IdentityModel.Tokens.Jwt;
namespace API.Specs.Steps; namespace API.Specs.Steps;
@@ -10,9 +12,11 @@ public class ApiSteps
{ {
private readonly TestApiFactory _factory = new(); private readonly TestApiFactory _factory = new();
private HttpClient? _client; 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")] [Given("the API is running")]
public void GivenTheApiIsRunning() public void GivenTheApiIsRunning()
@@ -29,17 +33,27 @@ public class ApiSteps
} }
[Then("the response JSON should have {string} equal {string}")] [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(); _response.Should().NotBeNull();
var dict = await _response!.Content.ReadFromJsonAsync<Dictionary<string, object>>(); _responseBody.Should().NotBeNull();
dict.Should().NotBeNull();
dict!.TryGetValue(field, out var value).Should().BeTrue(); using var doc = JsonDocument.Parse(_responseBody!);
(value?.ToString()).Should().Be(expected); 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:")] [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(); _client.Should().NotBeNull();
@@ -49,7 +63,10 @@ public class ApiSteps
Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json") Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json")
}; };
_response = await _client!.SendAsync(requestMessage); _response = await _client!.SendAsync(requestMessage);
_responseBody = await _response.Content.ReadAsStringAsync();
} }
[When("I send an HTTP request {string} to {string}")] [When("I send an HTTP request {string} to {string}")]
@@ -57,6 +74,7 @@ public class ApiSteps
{ {
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url); var requestMessage = new HttpRequestMessage(new HttpMethod(method), url);
_response = await _client!.SendAsync(requestMessage); _response = await _client!.SendAsync(requestMessage);
_responseBody = await _response.Content.ReadAsStringAsync();
} }
[Then("the response has HTTP status {int}")] [Then("the response has HTTP status {int}")]
@@ -70,30 +88,78 @@ public class ApiSteps
[Given("I have an existing account")] [Given("I have an existing account")]
public void GivenIHaveAnExistingAccount() public void GivenIHaveAnExistingAccount()
{ {
testUser = ("test.user", "password"); _testUser = ("test.user", "password");
} }
[Given("I submit a login request with a valid username and password")] [When("I submit a login request with a username and password")]
public async Task GivenISubmitALoginRequestWithAValidUsernameAndPassword() public async Task WhenISubmitALoginRequestWithAUsernameAndPassword()
{ {
await WhenISendAnHttpRequestToWithBody("POST", "/api/v1/account/login", $@" await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", $@"
{{ {{
""username"": ""{testUser.username}"", ""username"": ""{_testUser.username}"",
""password"": ""{testUser.password}"" ""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>>(); _response.Should().NotBeNull();
dict.Should().NotBeNull(); _responseBody.Should().NotBeNull();
dict!.TryGetValue("AccessToken", out var value).Should().BeTrue(); using var doc = JsonDocument.Parse(_responseBody!);
var root = doc.RootElement;
JsonElement tokenElem;
var hasToken = root.TryGetProperty("accessToken", out tokenElem)
|| root.TryGetProperty("AccessToken", out tokenElem);
var messageStr = value!.ToString(); if (!hasToken)
{
if (root.TryGetProperty("payload", out var payloadElem) && payloadElem.ValueKind == JsonValueKind.Object)
{
hasToken = payloadElem.TryGetProperty("accessToken", out tokenElem)
|| payloadElem.TryGetProperty("AccessToken", out tokenElem);
}
}
Console.WriteLine(messageStr); 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

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

View File

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