{
  "slug": "testing-consumer-examples",
  "title": "Testing the Consumer Examples",
  "description": "How the Java and .NET consumer examples are tested. Test setup, fixtures, security checks, and how to run them locally.",
  "category": "consumers",
  "order": 20,
  "locale": "en",
  "translationGroup": "310cc06f-80ed-4b36-8fe6-0958c04b5f02",
  "helpCardId": null,
  "content": "## Why test the consumer examples\n\nThe 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.\n\nEvery consumer example ships with:\n\n1. **Unit tests** for the reader class (collection, document, findTranslation, security)\n2. **A test fixture content directory** so tests are hermetic — no dependency on real `content/` files\n3. **An end-to-end smoke test** that builds the example, starts the app, and verifies HTTP responses\n\nThis page documents how it's set up.\n\n## Java example: `java-spring-blog`\n\n### Stack\n- **Build:** Maven 3.9 (or any 3.6+)\n- **Test runner:** JUnit 5 (Jupiter) — bundled with `spring-boot-starter-test`\n- **Temp dirs:** `@TempDir` for hermetic file fixtures\n- **Java:** 21 (LTS)\n\n### Test file\n\n`src/test/java/app/webhouse/cmsreader/WebhouseReaderTest.java` — 26 tests covering:\n\n```\n├── Collection listing\n│   ├── returns all published regardless of locale\n│   ├── filters by locale\n│   ├── returns Danish posts only\n│   ├── skips draft status\n│   ├── skips malformed JSON\n│   ├── sorts by date descending\n│   └── returns empty for missing dir\n│\n├── Document loading\n│   ├── loads published post\n│   ├── returns empty for draft\n│   └── returns empty for missing\n│\n├── Translation resolution\n│   ├── resolves via translationGroup\n│   ├── returns empty for untranslated post\n│   └── returns empty when translationGroup missing\n│\n├── Security (path traversal)\n│   ├── rejects path traversal slug (../../etc/passwd)\n│   ├── rejects path traversal collection\n│   ├── rejects absolute path\n│   ├── rejects slug with dots (hello..world)\n│   ├── rejects slug with slash (hello/world)\n│   ├── accepts valid slugs (kebab-case)\n│   ├── rejects uppercase slug\n│   ├── rejects empty slug\n│   └── rejects null slug\n│\n└── Document helpers\n    ├── getString returns value when present\n    ├── getString returns null for missing key\n    ├── getStringOr returns fallback\n    └── isPublished true for published\n```\n\n### How fixtures work\n\nJUnit'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:\n\n```java\n@TempDir\nPath contentDir;\n\nprivate WebhouseReader reader;\n\n@BeforeEach\nvoid setUp() throws IOException {\n    reader = new WebhouseReader(contentDir.toString());\n\n    Path postsDir = Files.createDirectory(contentDir.resolve(\"posts\"));\n\n    Files.writeString(postsDir.resolve(\"hello-world.json\"), \"\"\"\n        {\n          \"id\": \"hello-en\",\n          \"slug\": \"hello-world\",\n          \"status\": \"published\",\n          \"locale\": \"en\",\n          \"translationGroup\": \"tg-1\",\n          \"data\": { \"title\": \"Hello\", \"date\": \"2026-01-15\" }\n        }\n        \"\"\");\n\n    // ... more fixtures\n\n    // Malformed file — must not crash the reader\n    Files.writeString(postsDir.resolve(\"bad.json\"), \"{ this is not json\");\n}\n```\n\nNo real `content/` directory is touched. Tests are fully isolated.\n\n### Running the tests\n\n```bash\ncd examples/consumers/java-spring-blog\nmvn test\n```\n\nExpected output:\n```\n[INFO] Tests run: 26, Failures: 0, Errors: 0, Skipped: 0\n[INFO] BUILD SUCCESS\n```\n\n### Running just one test\n\n```bash\nmvn test -Dtest=WebhouseReaderTest#document_rejectsPathTraversalSlug\n```\n\n### End-to-end smoke test\n\nAfter `mvn package`, you can run the full Spring Boot app and curl it:\n\n```bash\nmvn -B package -DskipTests\njava -jar target/java-spring-blog-0.1.0.jar --server.port=8080 &\nsleep 8\n\nfor url in \"/\" \"/da/\" \"/blog/hello-world\" \"/blog/hello-world-da\" \\\n           \"/blog/does-not-exist\" \"/blog/..%2F..%2Fetc%2Fpasswd\"; do\n  code=$(curl -s -o /dev/null -w \"%{http_code}\" \"http://localhost:8080$url\")\n  echo \"  $code  $url\"\ndone\n```\n\nExpected:\n```\n  200  /                              ← English home, lists 2 posts\n  200  /da/                            ← Danish home, lists 2 posts\n  200  /blog/hello-world               ← English post detail\n  200  /blog/hello-world-da            ← Danish post detail\n  404  /blog/does-not-exist            ← Friendly 404 page\n  400  /blog/..%2F..%2Fetc%2Fpasswd    ← Security: path traversal blocked\n```\n\n## .NET example: `dotnet-blog`\n\n### Stack\n- **Build:** dotnet CLI (bundled with .NET 9 SDK)\n- **Test runner:** xUnit (or MSTest if you prefer)\n- **Temp dirs:** `Path.GetTempPath()` + `Guid.NewGuid()` for hermetic fixtures\n- **.NET:** 9 (LTS)\n\n### Future test file (Phase 2)\n\nThe .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.\n\nTo add tests today, create a test project alongside the example:\n\n```bash\ncd examples/consumers/dotnet-blog\ndotnet new xunit -o tests\ndotnet add tests/tests.csproj reference DotnetBlog.csproj\n```\n\nThen create `tests/WebhouseReaderTests.cs`:\n\n```csharp\nusing System.IO;\nusing DotnetBlog.Services;\nusing Xunit;\n\npublic class WebhouseReaderTests : IDisposable\n{\n    private readonly string _contentDir;\n    private readonly WebhouseReader _reader;\n\n    public WebhouseReaderTests()\n    {\n        _contentDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());\n        Directory.CreateDirectory(Path.Combine(_contentDir, \"posts\"));\n        File.WriteAllText(\n            Path.Combine(_contentDir, \"posts\", \"hello-world.json\"),\n            \"\"\"\n            { \"slug\": \"hello-world\", \"status\": \"published\", \"locale\": \"en\",\n              \"data\": { \"title\": \"Hello\" } }\n            \"\"\");\n        _reader = new WebhouseReader(_contentDir);\n    }\n\n    public void Dispose() => Directory.Delete(_contentDir, recursive: true);\n\n    [Fact]\n    public void Collection_ReturnsPublishedPosts()\n    {\n        var posts = _reader.Collection(\"posts\");\n        Assert.Single(posts);\n    }\n\n    [Fact]\n    public void Document_RejectsPathTraversal()\n    {\n        Assert.Throws<ArgumentException>(\n            () => _reader.Document(\"posts\", \"../../etc/passwd\"));\n    }\n}\n```\n\nRun with:\n```bash\ndotnet test\n```\n\n### .NET smoke test\n\n```bash\ncd examples/consumers/dotnet-blog\ndotnet run --urls http://localhost:5000 &\nsleep 5\ncurl -s -o /dev/null -w \"%{http_code}\\n\" http://localhost:5000/\ncurl -s -o /dev/null -w \"%{http_code}\\n\" http://localhost:5000/da\ncurl -s -o /dev/null -w \"%{http_code}\\n\" http://localhost:5000/blog/hello-world\n```\n\n## CI integration\n\nIn the `webhousecode/cms` repo, the consumer examples will eventually be built on every PR:\n\n```yaml\n# .github/workflows/consumer-examples.yml\nname: Consumer Examples\non: [push, pull_request]\n\njobs:\n  java:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-java@v4\n        with: { java-version: '21', distribution: 'temurin' }\n      - run: mvn -B package\n        working-directory: examples/consumers/java-spring-blog\n\n  dotnet:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-dotnet@v4\n        with: { dotnet-version: '9.0.x' }\n      - run: dotnet build\n        working-directory: examples/consumers/dotnet-blog\n```\n\n## Why these tests matter\n\nThe consumer examples are the **proof** that @webhouse/cms is framework-agnostic. If they don't run cleanly:\n\n- The framework-agnostic story collapses\n- Developers copying the code will hit the same bugs we hit\n- Documentation gets out of sync with reality\n\n26 JUnit tests + manual smoke testing means:\n\n1. **The reader works** — collection, document, findTranslation are correct\n2. **Security holds** — path traversal, invalid slugs, malformed JSON are rejected\n3. **The Spring Boot integration is real** — bean registration, controller routing, Thymeleaf rendering\n4. **Edge cases are covered** — drafts, missing files, empty translationGroups\n\nIf you change the Java reader, run `mvn test` first. If you change the .NET reader, add tests before merging.\n\n## Related\n\n- **F125 — Framework-Agnostic Content Platform** ([feature plan](https://github.com/webhousecode/cms/blob/main/docs/features/F125-framework-agnostic-consumers.md))\n- [Consume from Java (Spring Boot)](/docs/consume-java)\n- [Consume from C# / .NET](/docs/consume-dotnet)\n- [Framework-Agnostic Architecture](/docs/framework-agnostic)",
  "excerpt": "Why test the consumer examples\n\nThe 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.\n\nEvery consumer example ships with:\n\n1. Unit tests for the reader class (co",
  "seo": {
    "metaTitle": "Testing the Consumer Examples — webhouse.app Docs",
    "metaDescription": "How the Java and .NET consumer examples are tested. Test setup, fixtures, security checks, and how to run them locally.",
    "keywords": [
      "webhouse",
      "cms",
      "testing",
      "junit",
      "xunit",
      "java",
      "dotnet"
    ]
  },
  "createdAt": "2026-04-08T12:00:00.000Z",
  "updatedAt": "2026-04-08T12:00:00.000Z"
}