using Elternbeirat.PocketBase; namespace Elternbeirat.Web.Tests; /// /// Integration tests that hit a live PocketBase instance. They read the base URL /// from the PB_URL environment variable (falling back to the repo .env file) and /// skip themselves when no URL is configured, so CI without PocketBase stays green. /// public sealed class PocketBaseClientTests : IDisposable { // One HttpClient per test instance; xUnit creates a fresh instance per test, // so this is disposed in Dispose() when the test finishes. private readonly HttpClient _http = new(); public void Dispose() => _http.Dispose(); private static string? ResolvePocketBaseUrl() { var fromEnv = Environment.GetEnvironmentVariable("PB_URL"); if (!string.IsNullOrWhiteSpace(fromEnv)) return fromEnv; // Fall back to the repo .env (gitignored, holds PB_URL locally). var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null) { var envPath = Path.Combine(dir.FullName, ".env"); if (File.Exists(envPath)) { foreach (var line in File.ReadAllLines(envPath)) { var trimmed = line.Trim(); if (trimmed.StartsWith("PB_URL=", StringComparison.Ordinal)) return trimmed["PB_URL=".Length..].Trim(); } } dir = dir.Parent; } return null; } private PocketBaseClient? CreateClient() { var url = ResolvePocketBaseUrl(); if (string.IsNullOrWhiteSpace(url)) return null; _http.BaseAddress = new Uri(url); return new PocketBaseClient(_http); } [Fact] public async Task Events_load_from_live_instance() { var client = CreateClient(); if (client is null) return; // PB_URL not configured; skip live test. var events = await client.GetEventsAsync(); events.ShouldNotBeEmpty(); // Every event must at least have a title and a start. events.ShouldAllBe(e => !string.IsNullOrWhiteSpace(e.Title) && e.Start != default); } [Fact] public async Task Event_time_is_read_as_wall_clock_not_shifted() { var client = CreateClient(); if (client is null) return; // PB_URL not configured; skip live test. var events = await client.GetEventsAsync(); // The Elternbeiratssitzung is stored as 19:30; by the timezone convention // the number is taken at face value, so no shift to 20:30/21:30 happens. var meeting = events.FirstOrDefault(@event => @event.Title.Contains("Elternbeiratssitzung", StringComparison.Ordinal)); meeting.ShouldNotBeNull(); meeting.Start.Hour.ShouldBe(19); meeting.Start.Minute.ShouldBe(30); meeting.Start.Kind.ShouldBe(DateTimeKind.Unspecified); } [Fact] public async Task Pages_load_with_navigation_fields() { var client = CreateClient(); if (client is null) return; // PB_URL not configured; skip live test. var pages = await client.GetPagesAsync(); pages.ShouldNotBeEmpty(); // Navigation depends on location + slug being present on every page. pages.ShouldAllBe(p => !string.IsNullOrWhiteSpace(p.Slug) && (p.Location == "header" || p.Location == "footer")); } [Fact] public async Task Posts_and_faqs_load_from_live_instance() { var client = CreateClient(); if (client is null) return; // PB_URL not configured; skip live test. var posts = await client.GetPostsAsync(); var faqs = await client.GetFaqsAsync(); posts.ShouldNotBeEmpty(); faqs.ShouldNotBeEmpty(); } }