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 }