using System.Text.Json; using API.Specs; 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 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; } 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 ) { var infra = new JwtInfrastructure(); 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 var 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() { 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; } [When("I submit a registration request with values:")] public async Task WhenISubmitARegistrationRequestWithValues(Table table) { var client = GetClient(); var row = table.Rows[0]; var username = row["Username"] ?? ""; var firstName = row["FirstName"] ?? ""; var lastName = row["LastName"] ?? ""; var email = row["Email"] ?? ""; var 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") { var suffix = Guid.NewGuid().ToString("N")[..8]; email = $"newuser-{suffix}@example.com"; if (username == "newuser") { username = $"newuser-{suffix}"; } } var password = row["Password"]; var registrationData = new { username, firstName, lastName, email, dateOfBirth, password, }; var body = JsonSerializer.Serialize(registrationData); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/register" ) { 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 registration request using a GET request")] public async Task WhenISubmitARegistrationRequestUsingAGetRequest() { var client = GetClient(); var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/auth/register" ) { 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; } [Given("I have registered a new account")] public async Task GivenIHaveRegisteredANewAccount() { var client = GetClient(); var 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!", }; var body = JsonSerializer.Serialize(registrationData); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/register" ) { 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; using var doc = JsonDocument.Parse(responseBody); var root = doc.RootElement; scenario[RegisteredUserIdKey] = ParseRegisteredUserId(root); scenario[RegisteredUsernameKey] = ParseRegisteredUsername(root); } [Given("I am logged in")] public async Task GivenIAmLoggedIn() { var client = GetClient(); var loginData = new { username = "test.user", password = "password" }; var body = JsonSerializer.Serialize(loginData); 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(); var doc = JsonDocument.Parse(responseBody); var root = doc.RootElement; if (root.TryGetProperty("payload", out var payloadElem)) { if ( payloadElem.TryGetProperty("accessToken", out var tokenElem) || payloadElem.TryGetProperty("AccessToken", out tokenElem) ) { scenario["accessToken"] = tokenElem.GetString(); } if ( payloadElem.TryGetProperty("refreshToken", out var 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() { var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); var username = scenario.TryGetValue( RegisteredUsernameKey, out var user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); var 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() { var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); var username = scenario.TryGetValue( RegisteredUsernameKey, out var user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); var 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() { var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); var username = scenario.TryGetValue( RegisteredUsernameKey, out var user ) ? user : throw new InvalidOperationException( "registered username not found in scenario" ); var 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() { var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : throw new InvalidOperationException( "registered user ID not found in scenario" ); var username = scenario.TryGetValue( RegisteredUsernameKey, out var 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() { var client = GetClient(); var token = scenario.TryGetValue("accessToken", out var t) ? t : "invalid-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", "Bearer invalid-token-format" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "valid-token"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } [When("I submit the same confirmation request again")] public async Task WhenISubmitTheSameConfirmationRequestAgain() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "valid-token"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); const string token = "malformed-token-not-jwt"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); if (scenario.TryGetValue("accessToken", out var oldAccessToken)) { scenario[PreviousAccessTokenKey] = oldAccessToken; } if (scenario.TryGetValue("refreshToken", out var oldRefreshToken)) { scenario[PreviousRefreshTokenKey] = oldRefreshToken; } var token = scenario.TryGetValue("refreshToken", out var t) ? t : "valid-refresh-token"; var body = JsonSerializer.Serialize(new { refreshToken = token }); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/refresh" ) { 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 refresh token request with an invalid refresh token")] public async Task WhenISubmitARefreshTokenRequestWithAnInvalidRefreshToken() { var client = GetClient(); var body = JsonSerializer.Serialize( new { refreshToken = "invalid-refresh-token" } ); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/refresh" ) { 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 refresh token request with the expired refresh token")] public async Task WhenISubmitARefreshTokenRequestWithTheExpiredRefreshToken() { var client = GetClient(); // Use an expired token var body = JsonSerializer.Serialize( new { refreshToken = "expired-refresh-token" } ); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/refresh" ) { 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 refresh token request with a missing refresh token")] public async Task WhenISubmitARefreshTokenRequestWithAMissingRefreshToken() { var client = GetClient(); var body = JsonSerializer.Serialize(new { }); var requestMessage = new HttpRequestMessage( HttpMethod.Post, "/api/auth/refresh" ) { 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 refresh token request using a GET request")] public async Task WhenISubmitARefreshTokenRequestUsingAGETRequest() { var client = GetClient(); var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/auth/refresh" ) { 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; } // Protected Endpoint Steps [When("I submit a request to a protected endpoint without an access token")] public async Task WhenISubmitARequestToAProtectedEndpointWithoutAnAccessToken() { var client = GetClient(); var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("accessToken", out var t) ? t : "expired-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("accessToken", out var t) ? t : "tampered-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("refreshToken", out var t) ? t : "refresh-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "expired-confirmation-token"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "tampered-confirmation-token"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/confirm"); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "valid-confirmation-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "confirmation-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Get, "/api/protected" ) { Headers = { { "Authorization", $"Bearer {token}" } }, }; var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); const string token = "invalid-confirmation-token"; var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : "valid-token"; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); var response = await client.SendAsync(requestMessage); var 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 var responseBody) .Should() .BeTrue(); using var doc = JsonDocument.Parse(responseBody!); var payload = doc.RootElement.GetProperty("payload"); var accessToken = ParseTokenFromPayload( payload, "accessToken", "AccessToken" ); accessToken.Should().NotBeNullOrWhiteSpace(); if ( scenario.TryGetValue( PreviousAccessTokenKey, out var previousAccessToken ) ) { accessToken.Should().NotBe(previousAccessToken); } } [Then("the response JSON should have a new refresh token")] public void ThenTheResponseJsonShouldHaveANewRefreshToken() { scenario .TryGetValue(ResponseBodyKey, out var responseBody) .Should() .BeTrue(); using var doc = JsonDocument.Parse(responseBody!); var payload = doc.RootElement.GetProperty("payload"); var refreshToken = ParseTokenFromPayload( payload, "refreshToken", "RefreshToken" ); refreshToken.Should().NotBeNullOrWhiteSpace(); if ( scenario.TryGetValue( PreviousRefreshTokenKey, out var previousRefreshToken ) ) { refreshToken.Should().NotBe(previousRefreshToken); } } [Given("I have confirmed my account")] public async Task GivenIHaveConfirmedMyAccount() { var client = GetClient(); var token = scenario.TryGetValue("confirmationToken", out var t) ? t : throw new InvalidOperationException("confirmation token not found"); var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm?token={Uri.EscapeDataString(token)}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); response.EnsureSuccessStatusCode(); } [When("I submit a resend confirmation request for my account")] public async Task WhenISubmitAResendConfirmationRequestForMyAccount() { var client = GetClient(); var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : throw new InvalidOperationException("registered user ID not found"); var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm/resend?userId={userId}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var fakeUserId = Guid.NewGuid(); var accessToken = scenario.TryGetValue("accessToken", out var at) ? at : string.Empty; var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm/resend?userId={fakeUserId}" ); if (!string.IsNullOrEmpty(accessToken)) requestMessage.Headers.Add("Authorization", $"Bearer {accessToken}"); var response = await client.SendAsync(requestMessage); var 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() { var client = GetClient(); var userId = scenario.TryGetValue(RegisteredUserIdKey, out var id) ? id : Guid.NewGuid(); var requestMessage = new HttpRequestMessage( HttpMethod.Post, $"/api/auth/confirm/resend?userId={userId}" ); var response = await client.SendAsync(requestMessage); var responseBody = await response.Content.ReadAsStringAsync(); scenario[ResponseKey] = response; scenario[ResponseBodyKey] = responseBody; } }