using System.Net.Http.Json; using System.Text.Json; using ItWorx.KnowledgeHub.Contracts.Auth; using ItWorx.KnowledgeHub.Contracts.Setup; namespace ItWorx.KnowledgeHub.IntegrationTests.Infrastructure; public sealed record CsrfToken(string Value, string HeaderName); /// Shared helpers so every test class does not re-implement the first-run /// setup and CSRF handshake used to reach an authenticated state. public static class SetupHelper { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); public static async Task GetCsrfTokenAsync(HttpClient client) { var response = await client.GetFromJsonAsync("/api/v1/auth/csrf", JsonOptions) ?? throw new InvalidOperationException("No CSRF token returned."); return new CsrfToken(response.Token, response.HeaderName); } public static async Task CompleteFirstRunSetupAsync( HttpClient client, string workspaceName, string displayName, string email, string password) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Post, "/api/v1/setup/complete") { Content = JsonContent.Create( new CompleteSetupRequest(workspaceName, displayName, email, password), options: JsonOptions) }; request.Headers.Add(csrf.HeaderName, csrf.Value); return await client.SendAsync(request); } public static async Task LoginAsync(HttpClient client, string email, string password) { return await client.PostAsJsonAsync( "/api/v1/auth/login", new LoginRequest(email, password), JsonOptions); } public static async Task PostWithCsrfAsync( HttpClient client, string path, TBody body) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = JsonContent.Create(body, options: JsonOptions) }; request.Headers.Add(csrf.HeaderName, csrf.Value); return await client.SendAsync(request); } public static async Task PatchWithCsrfAsync( HttpClient client, string path, TBody body) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Patch, path) { Content = JsonContent.Create(body, options: JsonOptions) }; request.Headers.Add(csrf.HeaderName, csrf.Value); return await client.SendAsync(request); } public static async Task PutWithCsrfAsync( HttpClient client, string path, TBody body, string? ifMatch = null) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Put, path) { Content = JsonContent.Create(body, options: JsonOptions) }; request.Headers.Add(csrf.HeaderName, csrf.Value); if (ifMatch is not null) { request.Headers.Add("If-Match", $"\"{ifMatch}\""); } return await client.SendAsync(request); } public static async Task PostWithCsrfAndIfMatchAsync( HttpClient client, string path, TBody body, string ifMatch) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = JsonContent.Create(body, options: JsonOptions) }; request.Headers.Add(csrf.HeaderName, csrf.Value); request.Headers.Add("If-Match", $"\"{ifMatch}\""); return await client.SendAsync(request); } public static async Task DeleteWithCsrfAsync(HttpClient client, string path) { var csrf = await GetCsrfTokenAsync(client); using var request = new HttpRequestMessage(HttpMethod.Delete, path); request.Headers.Add(csrf.HeaderName, csrf.Value); return await client.SendAsync(request); } public static string? GetETag(HttpResponseMessage response) => response.Headers.ETag?.Tag.Trim('"'); public static async Task PostMultipartWithCsrfAsync( HttpClient client, string path, byte[] fileBytes, string fileName, string contentType) { var csrf = await GetCsrfTokenAsync(client); using var content = new MultipartFormDataContent(); var fileContent = new ByteArrayContent(fileBytes); fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); content.Add(fileContent, "file", fileName); using var request = new HttpRequestMessage(HttpMethod.Post, path) { Content = content }; request.Headers.Add(csrf.HeaderName, csrf.Value); return await client.SendAsync(request); } }