mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 20:13:49 +00:00
update tests
This commit is contained in:
@@ -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 }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -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", "{}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user