Auto-seed dev PocketBase and share its schema with the tests
This commit is contained in:
1 parent
d5372963bb
commit
c3da5eb6e1
6 files changed
+537
-65
No files matched your search
@@ -2,6 +2,8 @@ using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Elternbeirat.PocketBase;
|
||||
|
||||
namespace Elternbeirat.Web.Tests;
|
||||
@@ -16,9 +18,16 @@ namespace Elternbeirat.Web.Tests;
|
||||
/// (only the <c>eb-pocketbase</c> service, not the web app) by shelling out to
|
||||
/// <c>docker compose</c>, so the image version, superuser env and port stay defined
|
||||
/// in one place -- the compose files -- and the tests always exercise the same
|
||||
/// PocketBase the stack runs. The seed data is deliberately fixed here rather than
|
||||
/// exported from the real instance, so tests assert against values this file
|
||||
/// controls. Requires Docker with the Compose plugin.
|
||||
/// PocketBase the stack runs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The collection <b>schema</b> is not typed out here: it is imported from
|
||||
/// <c>pb/pb_migrations/collections_schema.json</c>, the same file the dev seed
|
||||
/// migration uses, so the schema is defined once and cannot drift between the two.
|
||||
/// The seeded <b>records</b>, by contrast, are fixed in this file on purpose (not
|
||||
/// the dev seed's data), so tests assert against values this file controls -- e.g.
|
||||
/// a non-public draft page that must never appear, and an event at a known time.
|
||||
/// Requires Docker with the Compose plugin.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class PocketBaseFixture : IAsyncLifetime
|
||||
@@ -145,15 +154,15 @@ public sealed class PocketBaseFixture : IAsyncLifetime
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authenticates as superuser, then creates the collections and records. This
|
||||
/// mirrors the shape the client reads, not the full production schema.
|
||||
/// Authenticates as superuser, then imports the shared collection schema and
|
||||
/// seeds the fixed test records the tests assert against.
|
||||
/// </summary>
|
||||
private async Task SeedAsync()
|
||||
{
|
||||
var token = await AuthenticateAsync(_http);
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(token);
|
||||
|
||||
await CreateCollectionsAsync(_http);
|
||||
await ImportCollectionsAsync(_http);
|
||||
await SeedRecordsAsync(_http);
|
||||
|
||||
// Drop the superuser token so the tests read as an anonymous visitor would,
|
||||
@@ -184,59 +193,27 @@ public sealed class PocketBaseFixture : IAsyncLifetime
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CreateCollectionsAsync(HttpClient http)
|
||||
/// <summary>
|
||||
/// Imports the four content collections from the shared schema file, the same
|
||||
/// one the dev seed migration reads (<c>pb/pb_migrations/collections_schema.json</c>).
|
||||
/// Uses PocketBase's <c>/api/collections/import</c> so the whole schema is
|
||||
/// defined in one place instead of being typed out here.
|
||||
/// </summary>
|
||||
private static async Task ImportCollectionsAsync(HttpClient http)
|
||||
{
|
||||
// listRule/viewRule = "" means publicly readable; the client's
|
||||
// filter=public=true does the visibility gating on top.
|
||||
await CreateCollectionAsync(http, "pages", new object[]
|
||||
{
|
||||
new { name = "title", type = "text", required = true },
|
||||
new { name = "body", type = "text" },
|
||||
new { name = "location", type = "select", maxSelect = 1, values = new[] { "header", "footer" } },
|
||||
new { name = "order", type = "number" },
|
||||
new { name = "slug", type = "text", required = true },
|
||||
new { name = "embed", type = "select", maxSelect = 3, values = new[] { "posts", "events", "faqs" } },
|
||||
new { name = "public", type = "bool" },
|
||||
});
|
||||
// The file maps name -> collection object; the import endpoint wants a
|
||||
// flat list, so unwrap the values. deleteMissing=false leaves anything
|
||||
// already present untouched (the import is idempotent).
|
||||
var path = Path.Combine(RepoRoot(), "pb", "pb_migrations", "collections_schema.json");
|
||||
using var schema = JsonDocument.Parse(await File.ReadAllTextAsync(path));
|
||||
var collections = schema.RootElement.EnumerateObject()
|
||||
.Select(property => property.Value)
|
||||
.ToArray();
|
||||
|
||||
await CreateCollectionAsync(http, "posts", new object[]
|
||||
{
|
||||
new { name = "date", type = "date" },
|
||||
new { name = "title", type = "text", required = true },
|
||||
new { name = "body", type = "text" },
|
||||
new { name = "slug", type = "text", required = true },
|
||||
new { name = "public", type = "bool" },
|
||||
});
|
||||
|
||||
await CreateCollectionAsync(http, "events", new object[]
|
||||
{
|
||||
new { name = "start", type = "date", required = true },
|
||||
new { name = "end", type = "date" },
|
||||
new { name = "title", type = "text", required = true },
|
||||
new { name = "location", type = "text" },
|
||||
new { name = "note", type = "text" },
|
||||
new { name = "public", type = "bool" },
|
||||
});
|
||||
|
||||
await CreateCollectionAsync(http, "faqs", new object[]
|
||||
{
|
||||
new { name = "question", type = "text", required = true },
|
||||
new { name = "answer", type = "text" },
|
||||
new { name = "topic", type = "select", maxSelect = 1, values = new[] { "mensa", "schliessfach", "elterneuro", "elternarbeit" } },
|
||||
new { name = "public", type = "bool" },
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task CreateCollectionAsync(HttpClient http, string name, object[] fields)
|
||||
{
|
||||
var response = await http.PostAsJsonAsync("/api/collections", new
|
||||
{
|
||||
name,
|
||||
type = "base",
|
||||
listRule = "",
|
||||
viewRule = "",
|
||||
fields,
|
||||
});
|
||||
// Serialize the JsonElement list back to JSON for the request body.
|
||||
var body = JsonSerializer.Serialize(new { collections, deleteMissing = false });
|
||||
using var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
var response = await http.PutAsync(new Uri("/api/collections/import", UriKind.Relative), content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
@@ -277,7 +254,7 @@ public sealed class PocketBaseFixture : IAsyncLifetime
|
||||
await CreateRecordAsync(http, "pages", new
|
||||
{
|
||||
title = "FAQ Mensa", body = "# Mensa", location = "",
|
||||
order = 0, slug = "faq-mensa", embed = Array.Empty<string>(), @public = true,
|
||||
order = 0, slug = "faq-lunch", embed = Array.Empty<string>(), @public = true,
|
||||
});
|
||||
await CreateRecordAsync(http, "pages", new
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class RouteSmokeTests : IDisposable
|
||||
"/board",
|
||||
"/patrons",
|
||||
"/faqs",
|
||||
"/faq-mensa",
|
||||
"/faq-lunch",
|
||||
"/downloads",
|
||||
"/contact",
|
||||
"/imprint",
|
||||
|
||||
Reference in new issue
Block a user