using System.Text; using System.Text.Json; using FluentAssertions; using Infrastructure.Jwt; using Reqnroll; 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 const string RegisteredUserIdKey = "registeredUserId"; private const string RegisteredUsernameKey = "registeredUsername"; private const string PreviousAccessTokenKey = "previousAccessToken"; private const string PreviousRefreshTokenKey = "previousRefreshToken"; private HttpClient GetClient() { if (scenario.TryGetValue(ClientKey, out HttpClient? client)) return client; TestApiFactory? factory = scenario.TryGetValue( FactoryKey, out TestApiFactory? f ) ? f : new TestApiFactory(); scenario[FactoryKey] = factory; client = factory.CreateClient(); scenario[ClientKey] = client; return client; } private static string GetRequiredEnvVar(string name) { return Environment.GetEnvironmentVariable(name) ?? throw new InvalidOperationException( $"{name} environment variable is not set" ); } private static string GenerateJwtToken( Guid userId, string username, string secret, DateTime expiry ) { JwtInfrastructure infra = new(); return infra.GenerateJwt(userId, username, expiry, secret); } private static Guid ParseRegisteredUserId(JsonElement root) { return root .GetProperty("payload") .GetProperty("userAccountId") .GetGuid(); } private static string ParseRegisteredUsername(JsonElement root) { return root .GetProperty("payload") .GetProperty("username") .GetString() ?? throw new InvalidOperationException( "username missing from registration payload" ); } private static string ParseTokenFromPayload( JsonElement payload, string camelCaseName, string pascalCaseName ) { if ( payload.TryGetProperty(camelCaseName, out JsonElement tokenElem) || payload.TryGetProperty(pascalCaseName, out tokenElem) ) return tokenElem.GetString() ?? throw new InvalidOperationException( $"{camelCaseName} is null" ); throw new InvalidOperationException( $"Could not find token field '{camelCaseName}' in payload" ); } [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() { HttpClient client = GetClient(); (string username, string password) = scenario.TryGetValue<( string username, string password )>(TestUserKey, out (string username, string password) user) ? user : ("test.user", "password"); string body = JsonSerializer.Serialize(new { username, password }); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/login" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string 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() { HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { password = "test" }); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/login" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string 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() { HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { username = "test" }); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/login" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string 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() { HttpClient client = GetClient(); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/login" ) { Content = new StringContent( "{}", Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string 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 HttpResponseMessage? response) .Should() .BeTrue(); scenario .TryGetValue(ResponseBodyKey, out string? responseBody) .Should() .BeTrue(); JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement root = doc.RootElement; JsonElement tokenElem = default; bool hasToken = false; if ( root.TryGetProperty("payload", out JsonElement 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'" ); string? token = tokenElem.GetString(); token.Should().NotBeNullOrEmpty(); } [When("I submit a login request using a GET request")] public async Task WhenISubmitALoginRequestUsingAgetRequest() { HttpClient client = GetClient(); // testing GET HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/auth/login" ) { Content = new StringContent( "{}", Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a registration request with values:")] public async Task WhenISubmitARegistrationRequestWithValues(Table table) { HttpClient client = GetClient(); DataTableRow? row = table.Rows[0]; string username = row["Username"] ?? ""; string firstName = row["FirstName"] ?? ""; string lastName = row["LastName"] ?? ""; string email = row["Email"] ?? ""; string dateOfBirth = row["DateOfBirth"] ?? ""; if (dateOfBirth == "{underage_date}") dateOfBirth = DateTime.UtcNow.AddYears(-18).ToString("yyyy-MM-dd"); // Keep default registration fixture values unique across repeated runs. if (email == "newuser@example.com") { string suffix = Guid.NewGuid().ToString("N")[..8]; email = $"newuser-{suffix}@example.com"; if (username == "newuser") username = $"newuser-{suffix}"; } string? password = row["Password"]; var registrationData = new { username, firstName, lastName, email, dateOfBirth, password }; string body = JsonSerializer.Serialize(registrationData); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/register" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a registration request using a GET request")] public async Task WhenISubmitARegistrationRequestUsingAGetRequest() { HttpClient client = GetClient(); HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/auth/register" ) { Content = new StringContent( "{}", Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [Given("I have registered a new account")] public async Task GivenIHaveRegisteredANewAccount() { HttpClient client = GetClient(); string suffix = Guid.NewGuid().ToString("N")[..8]; var registrationData = new { username = $"newuser-{suffix}", firstName = "New", lastName = "User", email = $"newuser-{suffix}@example.com", dateOfBirth = "1990-01-01", password = "Password1!" }; string body = JsonSerializer.Serialize(registrationData); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/register" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; using JsonDocument doc = JsonDocument.Parse(responseBody); JsonElement root = doc.RootElement; scenario[RegisteredUserIdKey] = ParseRegisteredUserId(root); scenario[RegisteredUsernameKey] = ParseRegisteredUsername(root); } [Given("I am logged in")] public async Task GivenIAmLoggedIn() { HttpClient client = GetClient(); var loginData = new { username = "test.user", password = "password" }; string body = JsonSerializer.Serialize(loginData); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/login" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); JsonDocument doc = JsonDocument.Parse(responseBody); JsonElement root = doc.RootElement; if (root.TryGetProperty("payload", out JsonElement payloadElem)) { if ( payloadElem.TryGetProperty("accessToken", out JsonElement tokenElem) || payloadElem.TryGetProperty("AccessToken", out tokenElem) ) scenario["accessToken"] = tokenElem.GetString(); if ( payloadElem.TryGetProperty("refreshToken", out JsonElement refreshElem) || payloadElem.TryGetProperty("RefreshToken", out refreshElem) ) scenario["refreshToken"] = refreshElem.GetString(); } } [Given("I have a valid refresh token")] public async Task GivenIHaveAValidRefreshToken() { await GivenIAmLoggedIn(); } [Given("I am logged in with an immediately-expiring refresh token")] public async Task GivenIAmLoggedInWithAnImmediatelyExpiringRefreshToken() { // For now, create a normal login; in production this would generate an expiring token await GivenIAmLoggedIn(); } [Given("I have a valid access token for my account")] public void GivenIHaveAValidAccessTokenForMyAccount() { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); string? username = scenario.TryGetValue( RegisteredUsernameKey, out string? user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); string secret = GetRequiredEnvVar("ACCESS_TOKEN_SECRET"); scenario["accessToken"] = GenerateJwtToken( userId, username, secret, DateTime.UtcNow.AddMinutes(60) ); } [Given("I have a valid confirmation token for my account")] public void GivenIHaveAValidConfirmationTokenForMyAccount() { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); string? username = scenario.TryGetValue( RegisteredUsernameKey, out string? user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); string secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET"); scenario["confirmationToken"] = GenerateJwtToken( userId, username, secret, DateTime.UtcNow.AddMinutes(5) ); } [Given("I have an expired confirmation token for my account")] public void GivenIHaveAnExpiredConfirmationTokenForMyAccount() { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); string? username = scenario.TryGetValue( RegisteredUsernameKey, out string? user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); string secret = GetRequiredEnvVar("CONFIRMATION_TOKEN_SECRET"); scenario["confirmationToken"] = GenerateJwtToken( userId, username, secret, DateTime.UtcNow.AddMinutes(-5) ); } [Given("I have a confirmation token signed with the wrong secret")] public void GivenIHaveAConfirmationTokenSignedWithTheWrongSecret() { Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); string? username = scenario.TryGetValue( RegisteredUsernameKey, out string? user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); const string wrongSecret = "wrong-confirmation-secret-that-is-very-long-1234567890"; scenario["confirmationToken"] = GenerateJwtToken( userId, username, wrongSecret, DateTime.UtcNow.AddMinutes(5) ); } [When( "I submit a request to a protected endpoint with a valid access token" )] public async Task WhenISubmitARequestToAProtectedEndpointWithAValidAccessToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("accessToken", out string? t) ? t : "invalid-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When( "I submit a request to a protected endpoint with an invalid access token" )] public async Task WhenISubmitARequestToAProtectedEndpointWithAnInvalidAccessToken() { HttpClient client = GetClient(); HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", "Bearer invalid-token-format" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with the valid token")] public async Task WhenISubmitAConfirmationRequestWithTheValidToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "valid-token"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit the same confirmation request again")] public async Task WhenISubmitTheSameConfirmationRequestAgain() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "valid-token"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with a malformed token")] public async Task WhenISubmitAConfirmationRequestWithAMalformedToken() { HttpClient client = GetClient(); const string token = "malformed-token-not-jwt"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a refresh token request with a valid refresh token")] public async Task WhenISubmitARefreshTokenRequestWithTheValidRefreshToken() { HttpClient client = GetClient(); if (scenario.TryGetValue("accessToken", out string? oldAccessToken)) scenario[PreviousAccessTokenKey] = oldAccessToken; if (scenario.TryGetValue("refreshToken", out string? oldRefreshToken)) scenario[PreviousRefreshTokenKey] = oldRefreshToken; string? token = scenario.TryGetValue("refreshToken", out string? t) ? t : "valid-refresh-token"; string body = JsonSerializer.Serialize(new { refreshToken = token }); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/refresh" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a refresh token request with an invalid refresh token")] public async Task WhenISubmitARefreshTokenRequestWithAnInvalidRefreshToken() { HttpClient client = GetClient(); string body = JsonSerializer.Serialize( new { refreshToken = "invalid-refresh-token" } ); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/refresh" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a refresh token request with the expired refresh token")] public async Task WhenISubmitARefreshTokenRequestWithTheExpiredRefreshToken() { HttpClient client = GetClient(); // Use an expired token string body = JsonSerializer.Serialize( new { refreshToken = "expired-refresh-token" } ); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/refresh" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a refresh token request with a missing refresh token")] public async Task WhenISubmitARefreshTokenRequestWithAMissingRefreshToken() { HttpClient client = GetClient(); string body = JsonSerializer.Serialize(new { }); HttpRequestMessage requestMessage = new( HttpMethod.Post, "/api/auth/refresh" ) { Content = new StringContent( body, Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a refresh token request using a GET request")] public async Task WhenISubmitARefreshTokenRequestUsingAGETRequest() { HttpClient client = GetClient(); HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/auth/refresh" ) { Content = new StringContent( "{}", Encoding.UTF8, "application/json" ) }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } // Protected Endpoint Steps [When("I submit a request to a protected endpoint without an access token")] public async Task WhenISubmitARequestToAProtectedEndpointWithoutAnAccessToken() { HttpClient client = GetClient(); HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [Given("I am logged in with an immediately-expiring access token")] public Task GivenIAmLoggedInWithAnImmediatelyExpiringAccessToken() { // Simulate an expired access token for auth rejection behavior. scenario["accessToken"] = "expired-access-token"; return Task.CompletedTask; } [Given("I have an access token signed with the wrong secret")] public void GivenIHaveAnAccessTokenSignedWithTheWrongSecret() { // Create a token with a different secret scenario["accessToken"] = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; } [When("I submit a request to a protected endpoint with the expired token")] public async Task WhenISubmitARequestToAProtectedEndpointWithTheExpiredToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("accessToken", out string? t) ? t : "expired-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a request to a protected endpoint with the tampered token")] public async Task WhenISubmitARequestToAProtectedEndpointWithTheTamperedToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("accessToken", out string? t) ? t : "tampered-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When( "I submit a request to a protected endpoint with my refresh token instead of access token" )] public async Task WhenISubmitARequestToAProtectedEndpointWithMyRefreshTokenInsteadOfAccessToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("refreshToken", out string? t) ? t : "refresh-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [Given("I have a valid confirmation token")] public void GivenIHaveAValidConfirmationToken() { scenario["confirmationToken"] = "valid-confirmation-token"; } [When("I submit a confirmation request with the expired token")] public async Task WhenISubmitAConfirmationRequestWithTheExpiredToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "expired-confirmation-token"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with the tampered token")] public async Task WhenISubmitAConfirmationRequestWithTheTamperedToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "tampered-confirmation-token"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with a missing token")] public async Task WhenISubmitAConfirmationRequestWithAMissingToken() { HttpClient client = GetClient(); string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new(HttpMethod.Post, "/api/auth/confirm"); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request using an invalid HTTP method")] public async Task WhenISubmitAConfirmationRequestUsingAnInvalidHttpMethod() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "valid-confirmation-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When( "I submit a request to a protected endpoint with my confirmation token instead of access token" )] public async Task WhenISubmitARequestToAProtectedEndpointWithMyConfirmationTokenInsteadOfAccessToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "confirmation-token"; HttpRequestMessage requestMessage = new( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } } }; HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with an invalid token")] public async Task WhenISubmitAConfirmationRequestWithAnInvalidToken() { HttpClient client = GetClient(); const string token = "invalid-confirmation-token"; string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a confirmation request with the valid token without an access token")] public async Task WhenISubmitAConfirmationRequestWithTheValidTokenWithoutAnAccessToken() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : "valid-token"; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [Then("the response JSON should have a new access token")] public void ThenTheResponseJsonShouldHaveANewAccessToken() { scenario .TryGetValue(ResponseBodyKey, out string? responseBody) .Should() .BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement payload = doc.RootElement.GetProperty("payload"); string accessToken = ParseTokenFromPayload( payload, "accessToken", "AccessToken" ); accessToken.Should().NotBeNullOrWhiteSpace(); if ( scenario.TryGetValue( PreviousAccessTokenKey, out string? previousAccessToken ) ) accessToken.Should().NotBe(previousAccessToken); } [Then("the response JSON should have a new refresh token")] public void ThenTheResponseJsonShouldHaveANewRefreshToken() { scenario .TryGetValue(ResponseBodyKey, out string? responseBody) .Should() .BeTrue(); using JsonDocument doc = JsonDocument.Parse(responseBody!); JsonElement payload = doc.RootElement.GetProperty("payload"); string refreshToken = ParseTokenFromPayload( payload, "refreshToken", "RefreshToken" ); refreshToken.Should().NotBeNullOrWhiteSpace(); if ( scenario.TryGetValue( PreviousRefreshTokenKey, out string? previousRefreshToken ) ) refreshToken.Should().NotBe(previousRefreshToken); } [Given("I have confirmed my account")] public async Task GivenIHaveConfirmedMyAccount() { HttpClient client = GetClient(); string? token = scenario.TryGetValue("confirmationToken", out string? t) ? t : throw new InvalidOperationException("confirmation token not found"); string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); response.EnsureSuccessStatusCode(); } [When("I submit a resend confirmation request for my account")] public async Task WhenISubmitAResendConfirmationRequestForMyAccount() { HttpClient client = GetClient(); Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : throw new InvalidOperationException("registered user ID not found"); string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm/resend?userId={userId}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a resend confirmation request for a non-existent user")] public async Task WhenISubmitAResendConfirmationRequestForANonExistentUser() { HttpClient client = GetClient(); Guid fakeUserId = Guid.NewGuid(); string? accessToken = scenario.TryGetValue("accessToken", out string? at) ? at : string.Empty; HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm/resend?userId={fakeUserId}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit a resend confirmation request without an access token")] public async Task WhenISubmitAResendConfirmationRequestWithoutAnAccessToken() { HttpClient client = GetClient(); Guid userId = scenario.TryGetValue(RegisteredUserIdKey, out Guid id) ? id : Guid.NewGuid(); HttpRequestMessage requestMessage = new( HttpMethod.Post, $"/api/auth/confirm/resend?userId={userId}" ); HttpResponseMessage response = await client.SendAsync(requestMessage); string responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } }