mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 01:54:00 +00:00
Move dotnet api into new directory
This commit is contained in:
46
web/backend/API/API.Specs/API.Specs.csproj
Normal file
46
web/backend/API/API.Specs/API.Specs.csproj
Normal file
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>API.Specs</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="dbup" Version="5.0.41" />
|
||||
|
||||
<!-- Reqnroll core, xUnit adapter and code-behind generator -->
|
||||
<PackageReference Include="Reqnroll" Version="3.3.3" />
|
||||
<PackageReference Include="Reqnroll.xUnit" Version="3.3.3" />
|
||||
<PackageReference
|
||||
Include="Reqnroll.Tools.MsBuild.Generation"
|
||||
Version="3.3.3"
|
||||
PrivateAssets="all"
|
||||
/>
|
||||
|
||||
<!-- ASP.NET Core integration testing -->
|
||||
<PackageReference
|
||||
Include="Microsoft.AspNetCore.Mvc.Testing"
|
||||
Version="9.0.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Ensure feature files are included in the project -->
|
||||
<None Include="Features\**\*.feature" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
25
web/backend/API/API.Specs/Dockerfile
Normal file
25
web/backend/API/API.Specs/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"]
|
||||
COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"]
|
||||
COPY ["Domain/Domain.Entities/Domain.Entities.csproj", "Domain.Entities/"]
|
||||
COPY ["Domain/Domain.Exceptions/Domain.Exceptions.csproj", "Domain.Exceptions/"]
|
||||
COPY ["Infrastructure/Infrastructure.Repository/Infrastructure.Repository.csproj", "Infrastructure/Infrastructure.Repository/"]
|
||||
COPY ["Infrastructure/Infrastructure.Jwt/Infrastructure.Jwt.csproj", "Infrastructure/Infrastructure.Jwt/"]
|
||||
COPY ["Infrastructure/Infrastructure.PasswordHashing/Infrastructure.PasswordHashing.csproj", "Infrastructure/Infrastructure.PasswordHashing/"]
|
||||
COPY ["Infrastructure/Infrastructure.Email/Infrastructure.Email.csproj", "Infrastructure/Infrastructure.Email/"]
|
||||
COPY ["Service/Service.Auth/Service.Auth.csproj", "Service/Service.Auth/"]
|
||||
COPY ["Service/Service.UserManagement/Service.UserManagement.csproj", "Service/Service.UserManagement/"]
|
||||
RUN dotnet restore "API/API.Specs/API.Specs.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/API/API.Specs"
|
||||
RUN dotnet build "./API.Specs.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS final
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN mkdir -p /app/test-results/api-specs
|
||||
WORKDIR /src/API/API.Specs
|
||||
ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/api-specs/results.trx"]
|
||||
@@ -0,0 +1,51 @@
|
||||
Feature: Protected Endpoint Access Token Validation
|
||||
As a backend developer
|
||||
I want protected endpoints to validate access tokens
|
||||
So that unauthorized requests are rejected
|
||||
|
||||
Scenario: Protected endpoint accepts valid access token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
And I am logged in
|
||||
When I submit a request to a protected endpoint with a valid access token
|
||||
Then the response has HTTP status 200
|
||||
|
||||
Scenario: Protected endpoint rejects missing access token
|
||||
Given the API is running
|
||||
When I submit a request to a protected endpoint without an access token
|
||||
Then the response has HTTP status 401
|
||||
|
||||
Scenario: Protected endpoint rejects invalid access token
|
||||
Given the API is running
|
||||
When I submit a request to a protected endpoint with an invalid access token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects expired access token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
And I am logged in with an immediately-expiring access token
|
||||
When I submit a request to a protected endpoint with the expired token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects token signed with wrong secret
|
||||
Given the API is running
|
||||
And I have an access token signed with the wrong secret
|
||||
When I submit a request to a protected endpoint with the tampered token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Unauthorized"
|
||||
|
||||
Scenario: Protected endpoint rejects refresh token as access token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
And I am logged in
|
||||
When I submit a request to a protected endpoint with my refresh token instead of access token
|
||||
Then the response has HTTP status 401
|
||||
|
||||
Scenario: Protected endpoint rejects confirmation token as access token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token
|
||||
When I submit a request to a protected endpoint with my confirmation token instead of access token
|
||||
Then the response has HTTP status 401
|
||||
76
web/backend/API/API.Specs/Features/Confirmation.feature
Normal file
76
web/backend/API/API.Specs/Features/Confirmation.feature
Normal file
@@ -0,0 +1,76 @@
|
||||
Feature: User Account Confirmation
|
||||
As a newly registered user
|
||||
I want to confirm my email address via a validation token
|
||||
So that my account is fully activated
|
||||
Scenario: Successful confirmation with valid token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token for my account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with the valid token
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "is confirmed"
|
||||
|
||||
Scenario: Re-confirming an already verified account remains successful
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token for my account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with the valid token
|
||||
And I submit the same confirmation request again
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "is confirmed"
|
||||
|
||||
Scenario: Confirmation fails with invalid token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with an invalid token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Confirmation fails with expired token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have an expired confirmation token for my account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with the expired token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Confirmation fails with tampered token (wrong secret)
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a confirmation token signed with the wrong secret
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with the tampered token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Confirmation fails when token is missing
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with a missing token
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Confirmation endpoint only accepts POST requests
|
||||
Given the API is running
|
||||
And I have a valid confirmation token
|
||||
When I submit a confirmation request using an invalid HTTP method
|
||||
Then the response has HTTP status 404
|
||||
|
||||
Scenario: Confirmation fails with malformed token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid access token for my account
|
||||
When I submit a confirmation request with a malformed token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Confirmation fails without an access token
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token for my account
|
||||
When I submit a confirmation request with the valid token without an access token
|
||||
Then the response has HTTP status 401
|
||||
39
web/backend/API/API.Specs/Features/Login.feature
Normal file
39
web/backend/API/API.Specs/Features/Login.feature
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
|
||||
Scenario: Successful login with valid credentials
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
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 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
|
||||
|
||||
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
|
||||
10
web/backend/API/API.Specs/Features/NotFound.feature
Normal file
10
web/backend/API/API.Specs/Features/NotFound.feature
Normal file
@@ -0,0 +1,10 @@
|
||||
Feature: NotFound Responses
|
||||
As a client of the API
|
||||
I want consistent 404 responses
|
||||
So that consumers can gracefully handle missing routes
|
||||
|
||||
Scenario: GET request to an invalid route returns 404
|
||||
Given the API is running
|
||||
When I send an HTTP request "GET" to "/invalid-route"
|
||||
Then the response has HTTP status 404
|
||||
And the response JSON should have "message" equal "Route not found."
|
||||
60
web/backend/API/API.Specs/Features/Registration.feature
Normal file
60
web/backend/API/API.Specs/Features/Registration.feature
Normal file
@@ -0,0 +1,60 @@
|
||||
Feature: User Registration
|
||||
As a new user
|
||||
I want to register an account
|
||||
So that I can log in and access authenticated routes
|
||||
|
||||
Scenario: Successful registration with valid details
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| newuser | New | User | newuser@example.com | 1990-01-01 | Password1! |
|
||||
Then the response has HTTP status 201
|
||||
And the response JSON should have "message" equal "User registered successfully."
|
||||
And the response JSON should have an access token
|
||||
|
||||
Scenario: Registration fails with existing username
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| test.user | Test | User | example@example.com | 2001-11-11 | Password1! |
|
||||
Then the response has HTTP status 409
|
||||
|
||||
Scenario: Registration fails with existing email
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| newuser | New | User | test.user@thebiergarten.app | 1990-01-01 | Password1! |
|
||||
Then the response has HTTP status 409
|
||||
|
||||
Scenario: Registration fails with missing required fields
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| | New | User | | | Password1! |
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Registration fails with invalid email format
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| newuser | New | User | invalidemail | 1990-01-01 | Password1! |
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Registration fails with weak password
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| newuser | New | User | newuser@example.com | 1990-01-01 | weakpass |
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Cannot register a user younger than 19 years of age (regulatory requirement)
|
||||
Given the API is running
|
||||
When I submit a registration request with values:
|
||||
| Username | FirstName | LastName | Email | DateOfBirth | Password |
|
||||
| younguser | Young | User | younguser@example.com | {underage_date} | Password1! |
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Registration endpoint only accepts POST requests
|
||||
Given the API is running
|
||||
When I submit a registration request using a GET request
|
||||
Then the response has HTTP status 404
|
||||
@@ -0,0 +1,36 @@
|
||||
Feature: Resend Confirmation Email
|
||||
As a user who did not receive the confirmation email
|
||||
I want to request a resend of the confirmation email
|
||||
So that I can obtain a working confirmation link while preventing abuse
|
||||
|
||||
Scenario: Legitimate resend for an unconfirmed user
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid access token for my account
|
||||
When I submit a resend confirmation request for my account
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "confirmation email has been resent"
|
||||
|
||||
Scenario: Resend is a no-op for an already confirmed user
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid confirmation token for my account
|
||||
And I have a valid access token for my account
|
||||
And I have confirmed my account
|
||||
When I submit a resend confirmation request for my account
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "confirmation email has been resent"
|
||||
|
||||
Scenario: Resend is a no-op for a non-existent user
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
And I have a valid access token for my account
|
||||
When I submit a resend confirmation request for a non-existent user
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" containing "confirmation email has been resent"
|
||||
|
||||
Scenario: Resend requires authentication
|
||||
Given the API is running
|
||||
And I have registered a new account
|
||||
When I submit a resend confirmation request without an access token
|
||||
Then the response has HTTP status 401
|
||||
39
web/backend/API/API.Specs/Features/TokenRefresh.feature
Normal file
39
web/backend/API/API.Specs/Features/TokenRefresh.feature
Normal file
@@ -0,0 +1,39 @@
|
||||
Feature: Token Refresh
|
||||
As an authenticated user
|
||||
I want to refresh my access token using my refresh token
|
||||
So that I can maintain my session without logging in again
|
||||
|
||||
Scenario: Successful token refresh with valid refresh token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
And I am logged in
|
||||
When I submit a refresh token request with a valid refresh token
|
||||
Then the response has HTTP status 200
|
||||
And the response JSON should have "message" equal "Token refreshed successfully."
|
||||
And the response JSON should have a new access token
|
||||
And the response JSON should have a new refresh token
|
||||
|
||||
Scenario: Token refresh fails with invalid refresh token
|
||||
Given the API is running
|
||||
When I submit a refresh token request with an invalid refresh token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid"
|
||||
|
||||
Scenario: Token refresh fails with expired refresh token
|
||||
Given the API is running
|
||||
And I have an existing account
|
||||
And I am logged in with an immediately-expiring refresh token
|
||||
When I submit a refresh token request with the expired refresh token
|
||||
Then the response has HTTP status 401
|
||||
And the response JSON should have "message" containing "Invalid token"
|
||||
|
||||
Scenario: Token refresh fails when refresh token is missing
|
||||
Given the API is running
|
||||
When I submit a refresh token request with a missing refresh token
|
||||
Then the response has HTTP status 400
|
||||
|
||||
Scenario: Token refresh endpoint only accepts POST requests
|
||||
Given the API is running
|
||||
And I have a valid refresh token
|
||||
When I submit a refresh token request using a GET request
|
||||
Then the response has HTTP status 404
|
||||
68
web/backend/API/API.Specs/Mocks/MockEmailProvider.cs
Normal file
68
web/backend/API/API.Specs/Mocks/MockEmailProvider.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Infrastructure.Email;
|
||||
|
||||
namespace API.Specs.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Mock email provider for testing that doesn't actually send emails.
|
||||
/// Tracks sent emails for verification in tests if needed.
|
||||
/// </summary>
|
||||
public class MockEmailProvider : IEmailProvider
|
||||
{
|
||||
public List<SentEmail> SentEmails { get; } = new();
|
||||
|
||||
public Task SendAsync(
|
||||
string to,
|
||||
string subject,
|
||||
string body,
|
||||
bool isHtml = true
|
||||
)
|
||||
{
|
||||
SentEmails.Add(
|
||||
new SentEmail
|
||||
{
|
||||
To = [to],
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendAsync(
|
||||
IEnumerable<string> to,
|
||||
string subject,
|
||||
string body,
|
||||
bool isHtml = true
|
||||
)
|
||||
{
|
||||
SentEmails.Add(
|
||||
new SentEmail
|
||||
{
|
||||
To = to.ToList(),
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SentEmails.Clear();
|
||||
}
|
||||
|
||||
public class SentEmail
|
||||
{
|
||||
public List<string> To { get; init; } = new();
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string Body { get; init; } = string.Empty;
|
||||
public bool IsHtml { get; init; }
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
}
|
||||
65
web/backend/API/API.Specs/Mocks/MockEmailService.cs
Normal file
65
web/backend/API/API.Specs/Mocks/MockEmailService.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Domain.Entities;
|
||||
using Service.Emails;
|
||||
|
||||
namespace API.Specs.Mocks;
|
||||
|
||||
public class MockEmailService : IEmailService
|
||||
{
|
||||
public List<RegistrationEmail> SentRegistrationEmails { get; } = new();
|
||||
|
||||
public List<ResendConfirmationEmail> SentResendConfirmationEmails { get; } = new();
|
||||
|
||||
public Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
)
|
||||
{
|
||||
SentRegistrationEmails.Add(
|
||||
new RegistrationEmail
|
||||
{
|
||||
UserAccount = createdUser,
|
||||
ConfirmationToken = confirmationToken,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendResendConfirmationEmailAsync(
|
||||
UserAccount user,
|
||||
string confirmationToken
|
||||
)
|
||||
{
|
||||
SentResendConfirmationEmails.Add(
|
||||
new ResendConfirmationEmail
|
||||
{
|
||||
UserAccount = user,
|
||||
ConfirmationToken = confirmationToken,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SentRegistrationEmails.Clear();
|
||||
SentResendConfirmationEmails.Clear();
|
||||
}
|
||||
|
||||
public class RegistrationEmail
|
||||
{
|
||||
public UserAccount UserAccount { get; init; } = null!;
|
||||
public string ConfirmationToken { get; init; } = string.Empty;
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
|
||||
public class ResendConfirmationEmail
|
||||
{
|
||||
public UserAccount UserAccount { get; init; } = null!;
|
||||
public string ConfirmationToken { get; init; } = string.Empty;
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
}
|
||||
209
web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs
Normal file
209
web/backend/API/API.Specs/Steps/ApiGeneralSteps.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System.Text.Json;
|
||||
using API.Specs;
|
||||
using FluentAssertions;
|
||||
using Reqnroll;
|
||||
|
||||
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<HttpClient>(ClientKey, out var client))
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
var factory = scenario.TryGetValue<TestApiFactory>(
|
||||
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<HttpResponseMessage>(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<HttpResponseMessage>(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<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
scenario
|
||||
.TryGetValue<string>(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);
|
||||
}
|
||||
|
||||
[Then("the response JSON should have {string} containing {string}")]
|
||||
public void ThenTheResponseJsonShouldHaveStringContainingString(
|
||||
string field,
|
||||
string expectedSubstring
|
||||
)
|
||||
{
|
||||
scenario
|
||||
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
scenario
|
||||
.TryGetValue<string>(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
|
||||
);
|
||||
var actualValue = value.GetString();
|
||||
actualValue
|
||||
.Should()
|
||||
.Contain(
|
||||
expectedSubstring,
|
||||
"Expected field '{0}' to contain '{1}' but was '{2}'",
|
||||
field,
|
||||
expectedSubstring,
|
||||
actualValue
|
||||
);
|
||||
}
|
||||
}
|
||||
1214
web/backend/API/API.Specs/Steps/AuthSteps.cs
Normal file
1214
web/backend/API/API.Specs/Steps/AuthSteps.cs
Normal file
File diff suppressed because it is too large
Load Diff
46
web/backend/API/API.Specs/TestApiFactory.cs
Normal file
46
web/backend/API/API.Specs/TestApiFactory.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Generic;
|
||||
using API.Specs.Mocks;
|
||||
using Infrastructure.Email;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Service.Emails;
|
||||
|
||||
namespace API.Specs
|
||||
{
|
||||
public class TestApiFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
builder.UseEnvironment("Testing");
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Replace the real email provider with mock for testing
|
||||
var emailProviderDescriptor = services.SingleOrDefault(d =>
|
||||
d.ServiceType == typeof(IEmailProvider)
|
||||
);
|
||||
|
||||
if (emailProviderDescriptor != null)
|
||||
{
|
||||
services.Remove(emailProviderDescriptor);
|
||||
}
|
||||
|
||||
services.AddScoped<IEmailProvider, MockEmailProvider>();
|
||||
|
||||
// Replace the real email service with mock for testing
|
||||
var emailServiceDescriptor = services.SingleOrDefault(d =>
|
||||
d.ServiceType == typeof(IEmailService)
|
||||
);
|
||||
|
||||
if (emailServiceDescriptor != null)
|
||||
{
|
||||
services.Remove(emailServiceDescriptor);
|
||||
}
|
||||
|
||||
services.AddScoped<IEmailService, MockEmailService>();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
15
web/backend/API/API.Specs/reqnroll.json
Normal file
15
web/backend/API/API.Specs/reqnroll.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/reqnroll/Reqnroll/main/Reqnroll.Configuration/reqnroll.schema.json",
|
||||
"language": {
|
||||
"feature": "en-US"
|
||||
},
|
||||
"bindingCulture": {
|
||||
"name": "en-US"
|
||||
},
|
||||
"trace": {
|
||||
"level": "Verbose"
|
||||
},
|
||||
"runtime": {
|
||||
"stopAtFirstError": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user