Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 263:ee04e4e69fed | 264:04fee26ecce0 |
|---|---|
| 1 /* | |
| 2 * template_renderer_test.c — unit tests for the Mjj template renderer. | |
| 3 * | |
| 4 * Tests cover: | |
| 5 * - Passthrough (no includes) | |
| 6 * - Include expansion with real parts files | |
| 7 * - Missing include (silently skipped, output is still valid) | |
| 8 * - Missing file (Mjj_Template_Render_File returns FALSE) | |
| 9 * - Capacity-overflow protection (returns FALSE) | |
| 10 * - Page bodies contain no literal {{ / }} / <{{ markers | |
| 11 * - Rendered pages contain expected design-system asset references | |
| 12 */ | |
| 13 | |
| 14 #include "mrjunejune/template_renderer.h" | |
| 15 | |
| 16 #include "dowa/dowa.h" | |
| 17 | |
| 18 #include <assert.h> | |
| 19 #include <stdio.h> | |
| 20 #include <string.h> | |
| 21 #include <stdlib.h> | |
| 22 | |
| 23 /* ------------------------------------------------------------------ */ | |
| 24 /* Utilities */ | |
| 25 /* ------------------------------------------------------------------ */ | |
| 26 | |
| 27 #define ASSERT(cond) \ | |
| 28 do { \ | |
| 29 if (!(cond)) { \ | |
| 30 fprintf(stderr, "FAIL [%s:%d]: %s\n", __FILE__, __LINE__, #cond); \ | |
| 31 abort(); \ | |
| 32 } \ | |
| 33 } while (0) | |
| 34 | |
| 35 #define TEST(name) \ | |
| 36 do { fprintf(stdout, " %-60s", name); fflush(stdout); } while (0) | |
| 37 | |
| 38 #define PASS() \ | |
| 39 do { fprintf(stdout, "PASS\n"); } while (0) | |
| 40 | |
| 41 /* Capacity used for page-level renders */ | |
| 42 #define PAGE_CAP (256 * 1024) | |
| 43 | |
| 44 /* ------------------------------------------------------------------ */ | |
| 45 /* Tests */ | |
| 46 /* ------------------------------------------------------------------ */ | |
| 47 | |
| 48 static void test_passthrough(void) | |
| 49 { | |
| 50 printf("\n[passthrough — no includes]\n"); | |
| 51 | |
| 52 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 53 ASSERT(arena); | |
| 54 | |
| 55 TEST("plain text passes through unchanged"); | |
| 56 { | |
| 57 char out[64]; | |
| 58 boolean ok = Mjj_Template_Render(out, sizeof(out), "Hello, world!", arena); | |
| 59 ASSERT(ok); | |
| 60 ASSERT(strcmp(out, "Hello, world!") == 0); | |
| 61 } | |
| 62 PASS(); | |
| 63 | |
| 64 TEST("empty template produces empty string"); | |
| 65 { | |
| 66 char out[8]; | |
| 67 out[0] = 'x'; | |
| 68 boolean ok = Mjj_Template_Render(out, sizeof(out), "", arena); | |
| 69 ASSERT(ok); | |
| 70 ASSERT(out[0] == '\0'); | |
| 71 } | |
| 72 PASS(); | |
| 73 | |
| 74 TEST("NULL template returns FALSE"); | |
| 75 { | |
| 76 char out[8]; | |
| 77 boolean ok = Mjj_Template_Render(out, sizeof(out), NULL, arena); | |
| 78 ASSERT(!ok); | |
| 79 } | |
| 80 PASS(); | |
| 81 | |
| 82 Dowa_Arena_Free(arena); | |
| 83 } | |
| 84 | |
| 85 static void test_capacity_overflow(void) | |
| 86 { | |
| 87 printf("\n[capacity-overflow protection]\n"); | |
| 88 | |
| 89 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 90 ASSERT(arena); | |
| 91 | |
| 92 TEST("render into 1-byte buffer returns FALSE"); | |
| 93 { | |
| 94 char out[1]; | |
| 95 boolean ok = Mjj_Template_Render(out, 1, "Hello", arena); | |
| 96 ASSERT(!ok); | |
| 97 /* NUL-termination must still hold */ | |
| 98 ASSERT(out[0] == '\0'); | |
| 99 } | |
| 100 PASS(); | |
| 101 | |
| 102 TEST("render into exactly-fitting buffer (size = len+1) returns TRUE"); | |
| 103 { | |
| 104 const char *src = "Hi"; | |
| 105 char out[3]; /* 2 chars + NUL */ | |
| 106 boolean ok = Mjj_Template_Render(out, sizeof(out), src, arena); | |
| 107 ASSERT(ok); | |
| 108 ASSERT(strcmp(out, "Hi") == 0); | |
| 109 } | |
| 110 PASS(); | |
| 111 | |
| 112 TEST("render into too-small buffer returns FALSE"); | |
| 113 { | |
| 114 const char *src = "Hello world"; | |
| 115 char out[5]; | |
| 116 boolean ok = Mjj_Template_Render(out, sizeof(out), src, arena); | |
| 117 ASSERT(!ok); | |
| 118 /* Must still be NUL-terminated */ | |
| 119 ASSERT(out[sizeof(out) - 1] == '\0'); | |
| 120 } | |
| 121 PASS(); | |
| 122 | |
| 123 TEST("Render_File into tiny buffer returns FALSE"); | |
| 124 { | |
| 125 char out[4]; | |
| 126 boolean ok = Mjj_Template_Render_File(out, sizeof(out), "/login/index.html", arena); | |
| 127 ASSERT(!ok); | |
| 128 ASSERT(out[sizeof(out) - 1] == '\0' || out[0] == '\0'); | |
| 129 } | |
| 130 PASS(); | |
| 131 | |
| 132 Dowa_Arena_Free(arena); | |
| 133 } | |
| 134 | |
| 135 static void test_missing_file(void) | |
| 136 { | |
| 137 printf("\n[missing file / missing include]\n"); | |
| 138 | |
| 139 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 140 ASSERT(arena); | |
| 141 | |
| 142 TEST("Render_File on non-existent path returns FALSE"); | |
| 143 { | |
| 144 char out[256]; | |
| 145 boolean ok = Mjj_Template_Render_File(out, sizeof(out), "/does/not/exist.html", arena); | |
| 146 ASSERT(!ok); | |
| 147 } | |
| 148 PASS(); | |
| 149 | |
| 150 TEST("missing include in template is skipped silently"); | |
| 151 { | |
| 152 char out[256]; | |
| 153 boolean ok = Mjj_Template_Render( | |
| 154 out, sizeof(out), | |
| 155 "before{{/parts/this_does_not_exist.html}}after", | |
| 156 arena); | |
| 157 ASSERT(ok); | |
| 158 ASSERT(strcmp(out, "beforeafter") == 0); | |
| 159 /* No literal {{ or }} should remain */ | |
| 160 ASSERT(strstr(out, "{{") == NULL); | |
| 161 ASSERT(strstr(out, "}}") == NULL); | |
| 162 } | |
| 163 PASS(); | |
| 164 | |
| 165 Dowa_Arena_Free(arena); | |
| 166 } | |
| 167 | |
| 168 static void test_include_expansion(void) | |
| 169 { | |
| 170 printf("\n[include expansion — real parts files]\n"); | |
| 171 | |
| 172 Dowa_Arena *arena = Dowa_Arena_Create(8192); | |
| 173 ASSERT(arena); | |
| 174 | |
| 175 TEST("{{/parts/base_head.html}} expands to non-empty content"); | |
| 176 { | |
| 177 char *out = (char *)malloc(PAGE_CAP); | |
| 178 ASSERT(out); | |
| 179 boolean ok = Mjj_Template_Render( | |
| 180 out, PAGE_CAP, | |
| 181 "A{{/parts/base_head.html}}B", | |
| 182 arena); | |
| 183 ASSERT(ok); | |
| 184 /* Expansion must have happened: output is longer than "AB" */ | |
| 185 ASSERT(strlen(out) > 2); | |
| 186 /* No literal markers must remain */ | |
| 187 ASSERT(strstr(out, "{{") == NULL); | |
| 188 ASSERT(strstr(out, "}}") == NULL); | |
| 189 /* base_head includes charset and stylesheet references */ | |
| 190 ASSERT(strstr(out, "charset") != NULL); | |
| 191 ASSERT(strstr(out, "design-system") != NULL); | |
| 192 free(out); | |
| 193 } | |
| 194 PASS(); | |
| 195 | |
| 196 TEST("{{/parts/header.html}} expands and contains MrJuneJune link"); | |
| 197 { | |
| 198 char *out = (char *)malloc(PAGE_CAP); | |
| 199 ASSERT(out); | |
| 200 boolean ok = Mjj_Template_Render( | |
| 201 out, PAGE_CAP, | |
| 202 "{{/parts/header.html}}", | |
| 203 arena); | |
| 204 ASSERT(ok); | |
| 205 ASSERT(strlen(out) > 0); | |
| 206 ASSERT(strstr(out, "{{") == NULL); | |
| 207 ASSERT(strstr(out, "MrJuneJune") != NULL); | |
| 208 free(out); | |
| 209 } | |
| 210 PASS(); | |
| 211 | |
| 212 Dowa_Arena_Free(arena); | |
| 213 } | |
| 214 | |
| 215 /* Check that a fully rendered page contains no literal template markers. */ | |
| 216 static void assert_page_clean(const char *path, char *buf) | |
| 217 { | |
| 218 Dowa_Arena *arena = Dowa_Arena_Create(8192); | |
| 219 ASSERT(arena); | |
| 220 | |
| 221 boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, path, arena); | |
| 222 if (!ok) | |
| 223 { | |
| 224 fprintf(stderr, " FAIL: Mjj_Template_Render_File returned FALSE for '%s'\n", path); | |
| 225 abort(); | |
| 226 } | |
| 227 | |
| 228 if (strstr(buf, "{{")) | |
| 229 { | |
| 230 fprintf(stderr, " FAIL: rendered '%s' contains '{{'\n", path); | |
| 231 abort(); | |
| 232 } | |
| 233 if (strstr(buf, "}}")) | |
| 234 { | |
| 235 fprintf(stderr, " FAIL: rendered '%s' contains '}}'\n", path); | |
| 236 abort(); | |
| 237 } | |
| 238 if (strstr(buf, "<{{")) | |
| 239 { | |
| 240 fprintf(stderr, " FAIL: rendered '%s' contains '<{{'\n", path); | |
| 241 abort(); | |
| 242 } | |
| 243 | |
| 244 Dowa_Arena_Free(arena); | |
| 245 } | |
| 246 | |
| 247 static void test_page_bodies(void) | |
| 248 { | |
| 249 printf("\n[page bodies — no literal markers, design-system assets present]\n"); | |
| 250 | |
| 251 char *buf = (char *)malloc(PAGE_CAP); | |
| 252 ASSERT(buf); | |
| 253 | |
| 254 const char *pages[] = { | |
| 255 "/login/index.html", | |
| 256 "/account/password.html", | |
| 257 "/admin/users/index.html", | |
| 258 "/index.html", | |
| 259 "/resume/index.html", | |
| 260 "/tools/index.html", | |
| 261 "/blog/index.html", | |
| 262 "/talk/index.html", | |
| 263 "/notes/index.html", | |
| 264 NULL | |
| 265 }; | |
| 266 | |
| 267 for (int i = 0; pages[i]; i++) | |
| 268 { | |
| 269 char label[128]; | |
| 270 snprintf(label, sizeof(label), "no {{ in rendered '%s'", pages[i]); | |
| 271 TEST(label); | |
| 272 assert_page_clean(pages[i], buf); | |
| 273 PASS(); | |
| 274 } | |
| 275 | |
| 276 /* Verify design-system stylesheet and component script are referenced | |
| 277 in the login page (canonical example that contains base_head). */ | |
| 278 TEST("login page references design-system/styles/tokens.css"); | |
| 279 { | |
| 280 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 281 ASSERT(arena); | |
| 282 boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/login/index.html", arena); | |
| 283 ASSERT(ok); | |
| 284 ASSERT(strstr(buf, "design-system") != NULL); | |
| 285 ASSERT(strstr(buf, "tokens.css") != NULL); | |
| 286 Dowa_Arena_Free(arena); | |
| 287 } | |
| 288 PASS(); | |
| 289 | |
| 290 TEST("admin page references design-system and header"); | |
| 291 { | |
| 292 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 293 ASSERT(arena); | |
| 294 boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/admin/users/index.html", arena); | |
| 295 ASSERT(ok); | |
| 296 ASSERT(strstr(buf, "design-system") != NULL); | |
| 297 ASSERT(strstr(buf, "MrJuneJune") != NULL); | |
| 298 Dowa_Arena_Free(arena); | |
| 299 } | |
| 300 PASS(); | |
| 301 | |
| 302 TEST("password page references design-system and header"); | |
| 303 { | |
| 304 Dowa_Arena *arena = Dowa_Arena_Create(4096); | |
| 305 ASSERT(arena); | |
| 306 boolean ok = Mjj_Template_Render_File(buf, PAGE_CAP, "/account/password.html", arena); | |
| 307 ASSERT(ok); | |
| 308 ASSERT(strstr(buf, "design-system") != NULL); | |
| 309 ASSERT(strstr(buf, "MrJuneJune") != NULL); | |
| 310 Dowa_Arena_Free(arena); | |
| 311 } | |
| 312 PASS(); | |
| 313 | |
| 314 free(buf); | |
| 315 } | |
| 316 | |
| 317 /* ------------------------------------------------------------------ */ | |
| 318 /* main */ | |
| 319 /* ------------------------------------------------------------------ */ | |
| 320 | |
| 321 int main(void) | |
| 322 { | |
| 323 printf("=== template_renderer_test ===\n"); | |
| 324 | |
| 325 /* The renderer defaults to "mrjunejune/src"; in Bazel test, CWD is the | |
| 326 runfiles root where mrjunejune/src/... is directly accessible. */ | |
| 327 | |
| 328 test_passthrough(); | |
| 329 test_capacity_overflow(); | |
| 330 test_missing_file(); | |
| 331 test_include_expansion(); | |
| 332 test_page_bodies(); | |
| 333 | |
| 334 printf("\nAll template_renderer tests passed.\n"); | |
| 335 return 0; | |
| 336 } |