diff mrjunejune/test/template_renderer_test.c @ 264:04fee26ecce0

add authenticated JRPG conversation platform Add reusable auth/session storage, owned conversation recovery, guest quotas, admin workflows, URL-routed conversation UI, mobile frame support, and parallel browser acceptance. Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Fri, 07 Aug 2026 07:34:12 -0700
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/test/template_renderer_test.c	Fri Aug 07 07:34:12 2026 -0700
@@ -0,0 +1,336 @@
+/*
+ * template_renderer_test.c — unit tests for the Mjj template renderer.
+ *
+ * Tests cover:
+ *   - Passthrough (no includes)
+ *   - Include expansion with real parts files
+ *   - Missing include (silently skipped, output is still valid)
+ *   - Missing file (Mjj_Template_Render_File returns FALSE)
+ *   - Capacity-overflow protection (returns FALSE)
+ *   - Page bodies contain no literal {{ / }} / <{{ markers
+ *   - Rendered pages contain expected design-system asset references
+ */
+
+#include "mrjunejune/template_renderer.h"
+
+#include "dowa/dowa.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+/* ------------------------------------------------------------------ */
+/* Utilities                                                            */
+/* ------------------------------------------------------------------ */
+
+#define ASSERT(cond) \
+  do { \
+    if (!(cond)) { \
+      fprintf(stderr, "FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \
+      abort(); \
+    } \
+  } while (0)
+
+#define TEST(name) \
+  do { fprintf(stdout, "  %-60s", name); fflush(stdout); } while (0)
+
+#define PASS() \
+  do { fprintf(stdout, "PASS\n"); } while (0)
+
+/* Capacity used for page-level renders */
+#define PAGE_CAP (256 * 1024)
+
+/* ------------------------------------------------------------------ */
+/* Tests                                                                */
+/* ------------------------------------------------------------------ */
+
+static void test_passthrough(void)
+{
+  printf("\n[passthrough — no includes]\n");
+
+  Dowa_Arena *arena = Dowa_Arena_Create(4096);
+  ASSERT(arena);
+
+  TEST("plain text passes through unchanged");
+  {
+    char out[64];
+    boolean ok = Mjj_Template_Render(out, sizeof(out), "Hello, world!", arena);
+    ASSERT(ok);
+    ASSERT(strcmp(out, "Hello, world!") == 0);
+  }
+  PASS();
+
+  TEST("empty template produces empty string");
+  {
+    char out[8];
+    out[0] = 'x';
+    boolean ok = Mjj_Template_Render(out, sizeof(out), "", arena);
+    ASSERT(ok);
+    ASSERT(out[0] == '\0');
+  }
+  PASS();
+
+  TEST("NULL template returns FALSE");
+  {
+    char out[8];
+    boolean ok = Mjj_Template_Render(out, sizeof(out), NULL, arena);
+    ASSERT(!ok);
+  }
+  PASS();
+
+  Dowa_Arena_Free(arena);
+}
+
+static void test_capacity_overflow(void)
+{
+  printf("\n[capacity-overflow protection]\n");
+
+  Dowa_Arena *arena = Dowa_Arena_Create(4096);
+  ASSERT(arena);
+
+  TEST("render into 1-byte buffer returns FALSE");
+  {
+    char out[1];
+    boolean ok = Mjj_Template_Render(out, 1, "Hello", arena);
+    ASSERT(!ok);
+    /* NUL-termination must still hold */
+    ASSERT(out[0] == '\0');
+  }
+  PASS();
+
+  TEST("render into exactly-fitting buffer (size = len+1) returns TRUE");
+  {
+    const char *src = "Hi";
+    char out[3]; /* 2 chars + NUL */
+    boolean ok = Mjj_Template_Render(out, sizeof(out), src, arena);
+    ASSERT(ok);
+    ASSERT(strcmp(out, "Hi") == 0);
+  }
+  PASS();
+
+  TEST("render into too-small buffer returns FALSE");
+  {
+    const char *src = "Hello world";
+    char out[5];
+    boolean ok = Mjj_Template_Render(out, sizeof(out), src, arena);
+    ASSERT(!ok);
+    /* Must still be NUL-terminated */
+    ASSERT(out[sizeof(out) - 1] == '\0');
+  }
+  PASS();
+
+  TEST("Render_File into tiny buffer returns FALSE");
+  {
+    char out[4];
+    boolean ok = Mjj_Template_Render_File(out, sizeof(out), "/login/index.html", arena);
+    ASSERT(!ok);
+    ASSERT(out[sizeof(out) - 1] == '\0' || out[0] == '\0');
+  }
+  PASS();
+
+  Dowa_Arena_Free(arena);
+}
+
+static void test_missing_file(void)
+{
+  printf("\n[missing file / missing include]\n");
+
+  Dowa_Arena *arena = Dowa_Arena_Create(4096);
+  ASSERT(arena);
+
+  TEST("Render_File on non-existent path returns FALSE");
+  {
+    char out[256];
+    boolean ok = Mjj_Template_Render_File(out, sizeof(out), "/does/not/exist.html", arena);
+    ASSERT(!ok);
+  }
+  PASS();
+
+  TEST("missing include in template is skipped silently");
+  {
+    char out[256];
+    boolean ok = Mjj_Template_Render(
+        out, sizeof(out),
+        "before{{/parts/this_does_not_exist.html}}after",
+        arena);
+    ASSERT(ok);
+    ASSERT(strcmp(out, "beforeafter") == 0);
+    /* No literal {{ or }} should remain */
+    ASSERT(strstr(out, "{{") == NULL);
+    ASSERT(strstr(out, "}}") == NULL);
+  }
+  PASS();
+
+  Dowa_Arena_Free(arena);
+}
+
+static void test_include_expansion(void)
+{
+  printf("\n[include expansion — real parts files]\n");
+
+  Dowa_Arena *arena = Dowa_Arena_Create(8192);
+  ASSERT(arena);
+
+  TEST("{{/parts/base_head.html}} expands to non-empty content");
+  {
+    char *out = (char *)malloc(PAGE_CAP);
+    ASSERT(out);
+    boolean ok = Mjj_Template_Render(
+        out, PAGE_CAP,
+        "A{{/parts/base_head.html}}B",
+        arena);
+    ASSERT(ok);
+    /* Expansion must have happened: output is longer than "AB" */
+    ASSERT(strlen(out) > 2);
+    /* No literal markers must remain */
+    ASSERT(strstr(out, "{{") == NULL);
+    ASSERT(strstr(out, "}}") == NULL);
+    /* base_head includes charset and stylesheet references */
+    ASSERT(strstr(out, "charset") != NULL);
+    ASSERT(strstr(out, "design-system") != NULL);
+    free(out);
+  }
+  PASS();
+
+  TEST("{{/parts/header.html}} expands and contains MrJuneJune link");
+  {
+    char *out = (char *)malloc(PAGE_CAP);
+    ASSERT(out);
+    boolean ok = Mjj_Template_Render(
+        out, PAGE_CAP,
+        "{{/parts/header.html}}",
+        arena);
+    ASSERT(ok);
+    ASSERT(strlen(out) > 0);
+    ASSERT(strstr(out, "{{") == NULL);
+    ASSERT(strstr(out, "MrJuneJune") != NULL);
+    free(out);
+  }
+  PASS();
+
+  Dowa_Arena_Free(arena);
+}
+
+/* Check that a fully rendered page contains no literal template markers. */
+static void assert_page_clean(const char *path, char *buf)
+{
+  Dowa_Arena *arena = Dowa_Arena_Create(8192);
+  ASSERT(arena);
+
+  boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, path, arena);
+  if (!ok)
+  {
+    fprintf(stderr, "  FAIL: Mjj_Template_Render_File returned FALSE for '%s'\n", path);
+    abort();
+  }
+
+  if (strstr(buf, "{{"))
+  {
+    fprintf(stderr, "  FAIL: rendered '%s' contains '{{'\n", path);
+    abort();
+  }
+  if (strstr(buf, "}}"))
+  {
+    fprintf(stderr, "  FAIL: rendered '%s' contains '}}'\n", path);
+    abort();
+  }
+  if (strstr(buf, "<{{"))
+  {
+    fprintf(stderr, "  FAIL: rendered '%s' contains '<{{'\n", path);
+    abort();
+  }
+
+  Dowa_Arena_Free(arena);
+}
+
+static void test_page_bodies(void)
+{
+  printf("\n[page bodies — no literal markers, design-system assets present]\n");
+
+  char *buf = (char *)malloc(PAGE_CAP);
+  ASSERT(buf);
+
+  const char *pages[] = {
+    "/login/index.html",
+    "/account/password.html",
+    "/admin/users/index.html",
+    "/index.html",
+    "/resume/index.html",
+    "/tools/index.html",
+    "/blog/index.html",
+    "/talk/index.html",
+    "/notes/index.html",
+    NULL
+  };
+
+  for (int i = 0; pages[i]; i++)
+  {
+    char label[128];
+    snprintf(label, sizeof(label), "no {{ in rendered '%s'", pages[i]);
+    TEST(label);
+    assert_page_clean(pages[i], buf);
+    PASS();
+  }
+
+  /* Verify design-system stylesheet and component script are referenced
+     in the login page (canonical example that contains base_head). */
+  TEST("login page references design-system/styles/tokens.css");
+  {
+    Dowa_Arena *arena = Dowa_Arena_Create(4096);
+    ASSERT(arena);
+    boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/login/index.html", arena);
+    ASSERT(ok);
+    ASSERT(strstr(buf, "design-system") != NULL);
+    ASSERT(strstr(buf, "tokens.css") != NULL);
+    Dowa_Arena_Free(arena);
+  }
+  PASS();
+
+  TEST("admin page references design-system and header");
+  {
+    Dowa_Arena *arena = Dowa_Arena_Create(4096);
+    ASSERT(arena);
+    boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/admin/users/index.html", arena);
+    ASSERT(ok);
+    ASSERT(strstr(buf, "design-system") != NULL);
+    ASSERT(strstr(buf, "MrJuneJune") != NULL);
+    Dowa_Arena_Free(arena);
+  }
+  PASS();
+
+  TEST("password page references design-system and header");
+  {
+    Dowa_Arena *arena = Dowa_Arena_Create(4096);
+    ASSERT(arena);
+    boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/account/password.html", arena);
+    ASSERT(ok);
+    ASSERT(strstr(buf, "design-system") != NULL);
+    ASSERT(strstr(buf, "MrJuneJune") != NULL);
+    Dowa_Arena_Free(arena);
+  }
+  PASS();
+
+  free(buf);
+}
+
+/* ------------------------------------------------------------------ */
+/* main                                                                 */
+/* ------------------------------------------------------------------ */
+
+int main(void)
+{
+  printf("=== template_renderer_test ===\n");
+
+  /* The renderer defaults to "mrjunejune/src"; in Bazel test, CWD is the
+     runfiles root where mrjunejune/src/... is directly accessible. */
+
+  test_passthrough();
+  test_capacity_overflow();
+  test_missing_file();
+  test_include_expansion();
+  test_page_bodies();
+
+  printf("\nAll template_renderer tests passed.\n");
+  return 0;
+}