mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
update tests
This commit is contained in:
@@ -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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,34 @@
|
||||
Feature: User Login
|
||||
As a registered user
|
||||
I want to log in to my account
|
||||
So that I receive an authentication token to access authenticated routes
|
||||
As a registered user
|
||||
I want to log in to my account
|
||||
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
|
||||
@@ -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();
|
||||
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", "{}");
|
||||
}
|
||||
}
|
||||
@@ -7,32 +7,22 @@ namespace ServiceCore.Services
|
||||
{
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user