Add PocketBase client with contracts and sharp build tooling

This commit is contained in:
tleininger committed 2026-09-22 19:32:32 +02:00
1 parent 56922707ea
commit 251f2d818b
24 files changed
+578 -20

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# Test-project-only overrides. This file inherits from the root .editorconfig
# (root = true there) and applies on top of it for everything under this folder.
[*.cs]
# Test methods use Given_When_Then style names with underscores, which is the
# common, readable convention for tests. CA1707 (no underscores in member names)
# stays enforced in production code, but is turned off here.
dotnet_diagnostic.CA1707.severity = none
@@ -8,19 +8,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.12" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Shouldly" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="Shouldly" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elternbeirat.Web\Elternbeirat.Web.csproj" />
<ProjectReference Include="..\Elternbeirat.PocketBase\Elternbeirat.PocketBase.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,111 @@
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.
/// </summary>
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();
}
}
+4 -4
View File
@@ -48,9 +48,9 @@ public sealed class RouteSmokeTests : IClassFixture<WebApplicationFactory<Progra
{
var client = _factory.CreateClient();
var response = await client.GetAsync(route);
var response = await client.GetAsync(new Uri(route, UriKind.Relative));
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
[Theory]
@@ -60,8 +60,8 @@ public sealed class RouteSmokeTests : IClassFixture<WebApplicationFactory<Progra
{
var client = _factory.CreateClient();
var response = await client.GetAsync(route);
var response = await client.GetAsync(new Uri(route, UriKind.Relative));
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
}
}