Add PocketBase client with contracts and containerized tests
This commit is contained in:
1 parent
251f2d818b
commit
d78a13815a
5 files changed
+298
-73
No files matched your search
@@ -1,81 +1,35 @@
|
||||
using Elternbeirat.PocketBase;
|
||||
|
||||
namespace Elternbeirat.Web.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// Tests the <see cref="PocketBaseClient"/> against a throwaway PocketBase container
|
||||
/// with a known seed (see <see cref="PocketBaseFixture"/>). Because the data is fixed,
|
||||
/// the tests assert on exact values, and they need no network to the live instance.
|
||||
/// </summary>
|
||||
public sealed class PocketBaseClientTests : IDisposable
|
||||
public sealed class PocketBaseClientTests(PocketBaseFixture pocketBase) : IClassFixture<PocketBaseFixture>
|
||||
{
|
||||
// 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()
|
||||
public async Task Events_load_sorted_by_start()
|
||||
{
|
||||
var client = CreateClient();
|
||||
if (client is null) return; // PB_URL not configured; skip live test.
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
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);
|
||||
// Two seeded events, earliest start first.
|
||||
events.Select(e => e.Title).ShouldBe(["Elternbeiratssitzung", "Herbstbasar"]);
|
||||
events.ShouldAllBe(e => e.Public);
|
||||
}
|
||||
|
||||
[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 client = pocketBase.CreateClient();
|
||||
|
||||
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));
|
||||
// The meeting is seeded 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(e =>
|
||||
e.Title.Contains("Elternbeiratssitzung", StringComparison.Ordinal));
|
||||
meeting.ShouldNotBeNull();
|
||||
meeting.Start.Hour.ShouldBe(19);
|
||||
meeting.Start.Minute.ShouldBe(30);
|
||||
@@ -83,29 +37,61 @@ public sealed class PocketBaseClientTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pages_load_with_navigation_fields()
|
||||
public async Task Event_without_end_maps_to_null()
|
||||
{
|
||||
var client = CreateClient();
|
||||
if (client is null) return; // PB_URL not configured; skip live test.
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
var pages = await client.GetPagesAsync();
|
||||
var events = await client.GetEventsAsync();
|
||||
|
||||
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"));
|
||||
var meeting = events.Single(e => e.Title == "Elternbeiratssitzung");
|
||||
var basar = events.Single(e => e.Title == "Herbstbasar");
|
||||
meeting.End.ShouldBeNull(); // no end seeded
|
||||
basar.End.ShouldNotBeNull(); // end seeded
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Posts_and_faqs_load_from_live_instance()
|
||||
public async Task Posts_load_newest_first()
|
||||
{
|
||||
var client = CreateClient();
|
||||
if (client is null) return; // PB_URL not configured; skip live test.
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
var posts = await client.GetPostsAsync();
|
||||
|
||||
// Sorted by -date: March before January.
|
||||
posts.Select(p => p.Title).ShouldBe(["Neuer Vorstand", "Neue Sporthalle"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Pages_exclude_non_public_records()
|
||||
{
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
var pages = await client.GetPagesAsync();
|
||||
|
||||
// Three pages seeded, one with public=false; the draft must be filtered out.
|
||||
pages.Select(p => p.Slug).ShouldBe(["home", "contact"]);
|
||||
pages.ShouldNotContain(p => p.Slug == "draft");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Page_embed_is_read_as_list()
|
||||
{
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
var pages = await client.GetPagesAsync();
|
||||
|
||||
var home = pages.Single(p => p.Slug == "home");
|
||||
home.Embed.ShouldBe(["posts", "events"], ignoreOrder: true);
|
||||
home.Location.ShouldBe("header");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Faqs_load_from_instance()
|
||||
{
|
||||
var client = pocketBase.CreateClient();
|
||||
|
||||
var faqs = await client.GetFaqsAsync();
|
||||
|
||||
posts.ShouldNotBeEmpty();
|
||||
faqs.ShouldNotBeEmpty();
|
||||
faqs.Select(f => f.Topic).ShouldBe(["mensa", "schliessfach"], ignoreOrder: true);
|
||||
faqs.ShouldAllBe(f => !string.IsNullOrWhiteSpace(f.Question));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user