diff --git a/src/Core/API/API.Specs/Features/Auth.feature b/src/Core/API/API.Specs/Features/Auth.feature index b85109f..0adad13 100644 --- a/src/Core/API/API.Specs/Features/Auth.feature +++ b/src/Core/API/API.Specs/Features/Auth.feature @@ -1,7 +1,7 @@ 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 @@ -31,4 +31,9 @@ Feature: User Login 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 \ No newline at end of file + Then the response has HTTP status 400 + + Scenario: Login endpoint only accepts POST requests + Given the API is running + When I submit a login request using a GET request + Then the response has HTTP status 404 \ No newline at end of file diff --git a/src/Core/API/API.Specs/Steps/ApiGeneralSteps.cs b/src/Core/API/API.Specs/Steps/ApiGeneralSteps.cs new file mode 100644 index 0000000..eb8d411 --- /dev/null +++ b/src/Core/API/API.Specs/Steps/ApiGeneralSteps.cs @@ -0,0 +1,99 @@ +using System.Text.Json; +using Reqnroll; +using FluentAssertions; +using API.Specs; + +namespace API.Specs.Steps; + +[Binding] +public class ApiGeneralSteps(ScenarioContext scenario) +{ + private const string ClientKey = "client"; + private const string FactoryKey = "factory"; + private const string ResponseKey = "response"; + private const string ResponseBodyKey = "responseBody"; + + private HttpClient GetClient() + { + if (scenario.TryGetValue(ClientKey, out var client)) + { + return client; + } + + var factory = scenario.TryGetValue(FactoryKey, out var f) ? f : new TestApiFactory(); + scenario[FactoryKey] = factory; + + client = factory.CreateClient(); + scenario[ClientKey] = client; + return client; + } + + [Given("the API is running")] + public void GivenTheApiIsRunning() + { + GetClient(); + } + + [When("I send an HTTP request {string} to {string} with body:")] + public async Task WhenISendAnHttpRequestStringToStringWithBody(string method, string url, string jsonBody) + { + var client = GetClient(); + + var requestMessage = new HttpRequestMessage(new HttpMethod(method), url) + { + Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json") + }; + + var response = await client.SendAsync(requestMessage); + var responseBody = await response.Content.ReadAsStringAsync(); + + scenario[ResponseKey] = response; + scenario[ResponseBodyKey] = responseBody; + } + + [When("I send an HTTP request {string} to {string}")] + public async Task WhenISendAnHttpRequestStringToString(string method, string url) + { + var client = GetClient(); + var requestMessage = new HttpRequestMessage(new HttpMethod(method), url); + var response = await client.SendAsync(requestMessage); + var responseBody = await response.Content.ReadAsStringAsync(); + + scenario[ResponseKey] = response; + scenario[ResponseBodyKey] = responseBody; + } + + [Then("the response status code should be {int}")] + public void ThenTheResponseStatusCodeShouldBeInt(int expected) + { + scenario.TryGetValue(ResponseKey, out var response).Should().BeTrue(); + ((int)response!.StatusCode).Should().Be(expected); + } + + [Then("the response has HTTP status {int}")] + public void ThenTheResponseHasHttpStatusInt(int expectedCode) + { + scenario.TryGetValue(ResponseKey, out var response).Should().BeTrue("No response was received from the API"); + ((int)response!.StatusCode).Should().Be(expectedCode); + } + + [Then("the response JSON should have {string} equal {string}")] + public void ThenTheResponseJsonShouldHaveStringEqualString(string field, string expected) + { + scenario.TryGetValue(ResponseKey, out var response).Should().BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out var responseBody).Should().BeTrue(); + + 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); + } +} diff --git a/src/Core/API/API.Specs/Steps/ApiSteps.cs b/src/Core/API/API.Specs/Steps/ApiSteps.cs deleted file mode 100644 index 5446997..0000000 --- a/src/Core/API/API.Specs/Steps/ApiSteps.cs +++ /dev/null @@ -1,165 +0,0 @@ -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; - -[Binding] -public class ApiSteps -{ - private readonly TestApiFactory _factory = new(); - private HttpClient? _client; - - private HttpResponseMessage? _response; - private string? _responseBody; - - private (string username, string password) _testUser; - - [Given("the API is running")] - public void GivenTheApiIsRunning() - { - _client = _factory.CreateClient(); - } - - - [Then("the response status code should be {int}")] - public void ThenStatusCodeShouldBe(int expected) - { - _response.Should().NotBeNull(); - ((int)_response!.StatusCode).Should().Be(expected); - } - - [Then("the response JSON should have {string} equal {string}")] - public void ThenTheResponseJsonShouldHaveStringEqualString(string field, string expected) - { - _response.Should().NotBeNull(); - _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 WhenISendAnHttpRequestStringToStringWithBody(string method, string url, string jsonBody) - { - _client.Should().NotBeNull(); - - var requestMessage = new HttpRequestMessage(new HttpMethod(method), url) - { - // Convert the string body into JSON content - 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}")] - public async Task WhenISendAnHttpRequestTo(string method, string url) - { - 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}")] - public void ThenTheResponseHasHttpStatusInt(int expectedCode) - { - _response.Should().NotBeNull("No response was received from the API"); - - ((int)_response!.StatusCode).Should().Be(expectedCode); - } - - [Given("I have an existing account")] - public void GivenIHaveAnExistingAccount() - { - _testUser = ("test.user", "password"); - } - - [When("I submit a login request with a username and password")] - public async Task WhenISubmitALoginRequestWithAUsernameAndPassword() - { - await WhenISendAnHttpRequestStringToStringWithBody("POST", "/api/auth/login", $@" - {{ - ""username"": ""{_testUser.username}"", - ""password"": ""{_testUser.password}"" - }}"); - } - - - [Then("the response JSON should have an access token")] - public void ThenTheResponseJsonShouldHaveAnAccessToken() - { - _response.Should().NotBeNull(); - _responseBody.Should().NotBeNull(); - - 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", "{}"); - } -} \ No newline at end of file diff --git a/src/Core/API/API.Specs/Steps/AuthSteps.cs b/src/Core/API/API.Specs/Steps/AuthSteps.cs new file mode 100644 index 0000000..92ae4c7 --- /dev/null +++ b/src/Core/API/API.Specs/Steps/AuthSteps.cs @@ -0,0 +1,160 @@ +using System.Text.Json; +using Reqnroll; +using FluentAssertions; +using API.Specs; + +namespace API.Specs.Steps; + +[Binding] +public class AuthSteps(ScenarioContext scenario) +{ + private const string ClientKey = "client"; + private const string FactoryKey = "factory"; + private const string ResponseKey = "response"; + private const string ResponseBodyKey = "responseBody"; + private const string TestUserKey = "testUser"; + + private HttpClient GetClient() + { + if (scenario.TryGetValue(ClientKey, out var client)) + { + return client; + } + + var factory = scenario.TryGetValue(FactoryKey, out var f) ? f : new TestApiFactory(); + scenario[FactoryKey] = factory; + + client = factory.CreateClient(); + scenario[ClientKey] = client; + return client; + } + + [Given("I have an existing account")] + public void GivenIHaveAnExistingAccount() + { + scenario[TestUserKey] = ("test.user", "password"); + } + + [Given("I do not have an existing account")] + public void GivenIDoNotHaveAnExistingAccount() + { + scenario[TestUserKey] = ("Failing", "User"); + } + + [When("I submit a login request with a username and password")] + public async Task WhenISubmitALoginRequestWithAUsernameAndPassword() + { + var client = GetClient(); + var (username, password) = scenario.TryGetValue<(string username, string password)>(TestUserKey, out var user) + ? user + : ("test.user", "password"); + + var body = JsonSerializer.Serialize(new { username, password }); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login") + { + 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; + } + + [When("I submit a login request with a missing username")] + public async Task WhenISubmitALoginRequestWithAMissingUsername() + { + var client = GetClient(); + var body = JsonSerializer.Serialize(new { password = "test" }); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login") + { + 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; + } + + [When("I submit a login request with a missing password")] + public async Task WhenISubmitALoginRequestWithAMissingPassword() + { + var client = GetClient(); + var body = JsonSerializer.Serialize(new { username = "test" }); + + var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login") + { + 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; + } + + [When("I submit a login request with both username and password missing")] + public async Task WhenISubmitALoginRequestWithBothUsernameAndPasswordMissing() + { + var client = GetClient(); + var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login") + { + 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; + } + + [Then("the response JSON should have an access token")] + public void ThenTheResponseJsonShouldHaveAnAccessToken() + { + scenario.TryGetValue(ResponseKey, out var response).Should().BeTrue(); + scenario.TryGetValue(ResponseBodyKey, out var responseBody).Should().BeTrue(); + + var doc = JsonDocument.Parse(responseBody!); + var root = doc.RootElement; + JsonElement tokenElem = default; + var hasToken = false; + + + 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(); + token.Should().NotBeNullOrEmpty(); + } + + + [When("I submit a login request using a GET request")] + public async Task WhenISubmitALoginRequestUsingAgetRequest() + { + var client = GetClient(); + // testing GET + var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/auth/login") + { + 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; + } +} \ No newline at end of file