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:
- Unit tests for the reader class (collection, document, findTranslation, security)
- A test fixture content directory so tests are hermetic — no dependency on real
content/files - 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:
@TempDirfor 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 publishedHow 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:
@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
cd examples/consumers/java-spring-blog
mvn testExpected output:
[INFO] Tests run: 26, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESSRunning just one test
mvn test -Dtest=WebhouseReaderTest#document_rejectsPathTraversalSlugEnd-to-end smoke test
After mvn package, you can run the full Spring Boot app and curl it:
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"
doneExpected:
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:
cd examples/consumers/dotnet-blog
dotnet new xunit -o tests
dotnet add tests/tests.csproj reference DotnetBlog.csprojThen create tests/WebhouseReaderTests.cs:
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:
dotnet test.NET smoke test
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-worldCI integration
In the webhousecode/cms repo, the consumer examples will eventually be built on every PR:
# .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-blogWhy 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:
- The reader works — collection, document, findTranslation are correct
- Security holds — path traversal, invalid slugs, malformed JSON are rejected
- The Spring Boot integration is real — bean registration, controller routing, Thymeleaf rendering
- 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.
Related
- F125 — Framework-Agnostic Content Platform (feature plan)
- Consume from Java (Spring Boot)
- Consume from C# / .NET
- Framework-Agnostic Architecture