Mercurial
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; +}