webhouse.appwebhouse.appdocs

How the Java and .NET consumer examples are tested. Test setup, fixtures, security checks, and how to run them locally.

Why test the consumer examples

The consumer examples in examples/consumers/ are reference implementations of the F125 reader pattern. If they break, the docs are wrong and developers copying the code will hit the same bugs.

Every consumer example ships with:

  1. Unit tests for the reader class (collection, document, findTranslation, security)
  2. A test fixture content directory so tests are hermetic — no dependency on real content/ files
  3. An end-to-end smoke test that builds the example, starts the app, and verifies HTTP responses

This page documents how it's set up.

Java example: java-spring-blog

Stack

  • Build: Maven 3.9 (or any 3.6+)
  • Test runner: JUnit 5 (Jupiter) — bundled with spring-boot-starter-test
  • Temp dirs: @TempDir for hermetic file fixtures
  • Java: 21 (LTS)

Test file

src/test/java/app/webhouse/cmsreader/WebhouseReaderTest.java — 26 tests covering:

├── Collection listing
│   ├── returns all published regardless of locale
│   ├── filters by locale
│   ├── returns Danish posts only
│   ├── skips draft status
│   ├── skips malformed JSON
│   ├── sorts by date descending
│   └── returns empty for missing dir

├── Document loading
│   ├── loads published post
│   ├── returns empty for draft
│   └── returns empty for missing

├── Translation resolution
│   ├── resolves via translationGroup
│   ├── returns empty for untranslated post
│   └── returns empty when translationGroup missing

├── Security (path traversal)
│   ├── rejects path traversal slug (../../etc/passwd)
│   ├── rejects path traversal collection
│   ├── rejects absolute path
│   ├── rejects slug with dots (hello..world)
│   ├── rejects slug with slash (hello/world)
│   ├── accepts valid slugs (kebab-case)
│   ├── rejects uppercase slug
│   ├── rejects empty slug
│   └── rejects null slug

└── Document helpers
    ├── getString returns value when present
    ├── getString returns null for missing key
    ├── getStringOr returns fallback
    └── isPublished true for published

How fixtures work

JUnit's @TempDir creates a fresh temporary directory before each test. The setup writes mock JSON files to it, then the test runs against that hermetic directory:

java
@TempDir
Path contentDir;

private WebhouseReader reader;

@BeforeEach
void setUp() throws IOException {
    reader = new WebhouseReader(contentDir.toString());

    Path postsDir = Files.createDirectory(contentDir.resolve("posts"));

    Files.writeString(postsDir.resolve("hello-world.json"), """
        {
          "id": "hello-en",
          "slug": "hello-world",
          "status": "published",
          "locale": "en",
          "translationGroup": "tg-1",
          "data": { "title": "Hello", "date": "2026-01-15" }
        }
        """);

    // ... more fixtures

    // Malformed file — must not crash the reader
    Files.writeString(postsDir.resolve("bad.json"), "{ this is not json");
}

No real content/ directory is touched. Tests are fully isolated.

Running the tests

bash
cd examples/consumers/java-spring-blog
mvn test

Expected output:

[INFO] Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Running just one test

bash
mvn test -Dtest=WebhouseReaderTest#document_rejectsPathTraversalSlug

End-to-end smoke test

After mvn package, you can run the full Spring Boot app and curl it:

bash
mvn -B package -DskipTests
java -jar target/java-spring-blog-0.1.0.jar --server.port=8080 &
sleep 8

for url in "/" "/da/" "/blog/hello-world" "/blog/hello-world-da" \
           "/blog/does-not-exist" "/blog/..%2F..%2Fetc%2Fpasswd"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080$url")
  echo "  $code  $url"
done

Expected:

200  /                              ← English home, lists 2 posts
  200  /da/                            ← Danish home, lists 2 posts
  200  /blog/hello-world               ← English post detail
  200  /blog/hello-world-da            ← Danish post detail
  404  /blog/does-not-exist            ← Friendly 404 page
  400  /blog/..%2F..%2Fetc%2Fpasswd    ← Security: path traversal blocked

.NET example: dotnet-blog

Stack

  • Build: dotnet CLI (bundled with .NET 9 SDK)
  • Test runner: xUnit (or MSTest if you prefer)
  • Temp dirs: Path.GetTempPath() + Guid.NewGuid() for hermetic fixtures
  • .NET: 9 (LTS)

Future test file (Phase 2)

The .NET example does not yet ship with xUnit tests because the reader is essentially a 1:1 port of the Java reader. Phase 2 of F125 will publish Webhouse.Cms.Reader as a NuGet package with its own test suite.

To add tests today, create a test project alongside the example:

bash
cd examples/consumers/dotnet-blog
dotnet new xunit -o tests
dotnet add tests/tests.csproj reference DotnetBlog.csproj

Then create tests/WebhouseReaderTests.cs:

csharp
using System.IO;
using DotnetBlog.Services;
using Xunit;

public class WebhouseReaderTests : IDisposable
{
    private readonly string _contentDir;
    private readonly WebhouseReader _reader;

    public WebhouseReaderTests()
    {
        _contentDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(Path.Combine(_contentDir, "posts"));
        File.WriteAllText(
            Path.Combine(_contentDir, "posts", "hello-world.json"),
            """
            { "slug": "hello-world", "status": "published", "locale": "en",
              "data": { "title": "Hello" } }
            """);
        _reader = new WebhouseReader(_contentDir);
    }

    public void Dispose() => Directory.Delete(_contentDir, recursive: true);

    [Fact]
    public void Collection_ReturnsPublishedPosts()
    {
        var posts = _reader.Collection("posts");
        Assert.Single(posts);
    }

    [Fact]
    public void Document_RejectsPathTraversal()
    {
        Assert.Throws<ArgumentException>(
            () => _reader.Document("posts", "../../etc/passwd"));
    }
}

Run with:

bash
dotnet test

.NET smoke test

bash
cd examples/consumers/dotnet-blog
dotnet run --urls http://localhost:5000 &
sleep 5
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/da
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/blog/hello-world

CI integration

In the webhousecode/cms repo, the consumer examples will eventually be built on every PR:

yaml
# .github/workflows/consumer-examples.yml
name: Consumer Examples
on: [push, pull_request]

jobs:
  java:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { java-version: '21', distribution: 'temurin' }
      - run: mvn -B package
        working-directory: examples/consumers/java-spring-blog

  dotnet:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: '9.0.x' }
      - run: dotnet build
        working-directory: examples/consumers/dotnet-blog

Why these tests matter

The consumer examples are the proof that @webhouse/cms is framework-agnostic. If they don't run cleanly:

  • The framework-agnostic story collapses
  • Developers copying the code will hit the same bugs we hit
  • Documentation gets out of sync with reality

26 JUnit tests + manual smoke testing means:

  1. The reader works — collection, document, findTranslation are correct
  2. Security holds — path traversal, invalid slugs, malformed JSON are rejected
  3. The Spring Boot integration is real — bean registration, controller routing, Thymeleaf rendering
  4. Edge cases are covered — drafts, missing files, empty translationGroups

If you change the Java reader, run mvn test first. If you change the .NET reader, add tests before merging.

Tags:FrameworksFilesystem Adapter
Previous
Consume from C# / .NET
JSON API →Edit on GitHub →