Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_config_ini_robustness.c
6 : * @brief TEST-84 — functional tests for ~/.config/tg-cli/config.ini parsing
7 : * edge cases in src/app/credentials.c (US-33).
8 : *
9 : * Each test seeds a byte-level config.ini, redirects HOME to an isolated
10 : * scratch dir, unsets XDG_CONFIG_HOME/XDG_CACHE_HOME (so CI runners do not
11 : * override the redirect), clears TG_CLI_API_ID / TG_CLI_API_HASH env vars,
12 : * and then asserts either a successful credentials_load() with the
13 : * expected values OR a specific diagnostic on the log file.
14 : *
15 : * Scenarios (from the ticket):
16 : * 1. test_crlf_line_endings_parsed_cleanly
17 : * 2. test_utf8_bom_skipped_at_start
18 : * 3. test_hash_comment_ignored
19 : * 4. test_semicolon_comment_ignored
20 : * 5. test_leading_trailing_whitespace_trimmed
21 : * 6. test_quoted_value_strips_quotes
22 : * 7. test_empty_value_is_missing_credential
23 : * 8. test_only_api_id_reports_api_hash_missing
24 : * 9. test_only_api_hash_reports_api_id_missing
25 : * 10. test_duplicate_key_last_wins_and_warns
26 : * 11. test_empty_file_is_missing_credentials
27 : * 12. test_api_hash_wrong_length_rejected
28 : */
29 :
30 : #include "test_helpers.h"
31 :
32 : #include "app/credentials.h"
33 : #include "logger.h"
34 :
35 : #include <stdio.h>
36 : #include <stdlib.h>
37 : #include <string.h>
38 : #include <sys/stat.h>
39 : #include <unistd.h>
40 :
41 : /* ------------------------------------------------------------------ */
42 : /* Helpers */
43 : /* ------------------------------------------------------------------ */
44 :
45 : /** Canonical 32-char lowercase-hex sample api_hash for happy paths. */
46 : #define VALID_HASH "deadbeefdeadbeefdeadbeefdeadbeef"
47 :
48 : /** Scratch dir template — tag + pid keeps parallel runs isolated. */
49 26 : static void scratch_dir_for(const char *tag, char *out, size_t cap) {
50 26 : snprintf(out, cap, "/tmp/tg-cli-ft-cfgini-%s-%d", tag, (int)getpid());
51 26 : }
52 :
53 : /** rm -rf @p path (best-effort). */
54 52 : static void rm_rf(const char *path) {
55 : char cmd[4096];
56 52 : snprintf(cmd, sizeof(cmd), "rm -rf \"%s\"", path);
57 52 : int sysrc = system(cmd);
58 : (void)sysrc;
59 52 : }
60 :
61 : /** mkdir -p @p path. */
62 52 : static int mkdir_p(const char *path) {
63 : char cmd[4096];
64 52 : snprintf(cmd, sizeof(cmd), "mkdir -p \"%s\"", path);
65 52 : int sysrc = system(cmd);
66 52 : return sysrc == 0 ? 0 : -1;
67 : }
68 :
69 : /**
70 : * Redirect HOME to a fresh scratch dir for the given @p tag, unset
71 : * XDG_CONFIG_HOME / XDG_CACHE_HOME (CI runners export them), clear the
72 : * env-var credentials (so the INI is the only source), init the logger at
73 : * a per-test path, and return the config.ini path in @p out_ini.
74 : *
75 : * The caller is responsible for populating config.ini after this returns.
76 : */
77 26 : static void with_fresh_home(const char *tag,
78 : char *out_home, size_t home_cap,
79 : char *out_ini, size_t ini_cap,
80 : char *out_log, size_t log_cap) {
81 : /* Intentionally modest caps — a 128-byte scratch root keeps the
82 : * compile-time FORTIFY check for snprintf happy while still leaving
83 : * plenty of headroom for the /tmp/tg-cli-ft-cfgini-<tag>-<pid> prefix. */
84 : char home_buf[256];
85 26 : scratch_dir_for(tag, home_buf, sizeof(home_buf));
86 26 : rm_rf(home_buf);
87 :
88 : char cfg_dir[512];
89 26 : snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", home_buf);
90 26 : (void)mkdir_p(cfg_dir);
91 :
92 : char cache_dir[512];
93 26 : snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/tg-cli/logs", home_buf);
94 26 : (void)mkdir_p(cache_dir);
95 :
96 26 : setenv("HOME", home_buf, 1);
97 : /* CI runners (GitHub Actions) set XDG_CONFIG_HOME / XDG_CACHE_HOME.
98 : * Without these unsets platform_config_dir() would point at the CI
99 : * runner's own config tree and the test would read somebody else's
100 : * config.ini. */
101 26 : unsetenv("XDG_CONFIG_HOME");
102 26 : unsetenv("XDG_CACHE_HOME");
103 26 : unsetenv("TG_CLI_API_ID");
104 26 : unsetenv("TG_CLI_API_HASH");
105 :
106 26 : snprintf(out_home, home_cap, "%s", home_buf);
107 26 : snprintf(out_ini, ini_cap, "%s/config.ini", cfg_dir);
108 26 : snprintf(out_log, log_cap, "%s/session.log", cache_dir);
109 26 : (void)unlink(out_log);
110 26 : (void)logger_init(out_log, LOG_DEBUG);
111 26 : }
112 :
113 : /** Write @p n bytes from @p buf to @p path (overwrite). */
114 26 : static int write_bytes(const char *path, const void *buf, size_t n) {
115 26 : FILE *fp = fopen(path, "wb");
116 26 : if (!fp) return -1;
117 26 : size_t wrote = fwrite(buf, 1, n, fp);
118 26 : fclose(fp);
119 26 : return wrote == n ? 0 : -1;
120 : }
121 :
122 : /** Convenience: write a NUL-terminated string to @p path as-is. */
123 22 : static int write_text(const char *path, const char *text) {
124 22 : return write_bytes(path, text, strlen(text));
125 : }
126 :
127 : /** Read file @p path into a heap buffer; caller frees. NUL-terminated. */
128 32 : static char *slurp(const char *path, size_t *size_out) {
129 32 : FILE *fp = fopen(path, "rb");
130 32 : if (!fp) return NULL;
131 32 : if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return NULL; }
132 32 : long sz = ftell(fp);
133 32 : if (sz < 0) { fclose(fp); return NULL; }
134 32 : if (fseek(fp, 0, SEEK_SET) != 0) { fclose(fp); return NULL; }
135 32 : char *buf = malloc((size_t)sz + 1);
136 32 : if (!buf) { fclose(fp); return NULL; }
137 32 : size_t n = fread(buf, 1, (size_t)sz, fp);
138 32 : fclose(fp);
139 32 : buf[n] = '\0';
140 32 : if (size_out) *size_out = n;
141 32 : return buf;
142 : }
143 :
144 : /** Slurp the log file and return 1 if @p needle is a substring. */
145 32 : static int log_contains(const char *log_path, const char *needle) {
146 32 : size_t sz = 0;
147 32 : char *buf = slurp(log_path, &sz);
148 32 : if (!buf) return 0;
149 32 : int hit = (strstr(buf, needle) != NULL);
150 32 : free(buf);
151 32 : return hit;
152 : }
153 :
154 : /** Flush the logger so slurp() sees the latest diagnostics. */
155 26 : static void flush_logs(void) {
156 26 : logger_close();
157 26 : }
158 :
159 : /* ================================================================ */
160 : /* 1. CRLF line endings */
161 : /* ================================================================ */
162 :
163 : /**
164 : * Windows-style CRLF line endings must parse cleanly: api_hash must not
165 : * retain a trailing \r that would later fail the 32-char hex check.
166 : */
167 2 : static void test_crlf_line_endings_parsed_cleanly(void) {
168 : char home[512], ini[768], log[768];
169 2 : with_fresh_home("crlf", home, sizeof(home), ini, sizeof(ini),
170 : log, sizeof(log));
171 :
172 2 : const char *body =
173 : "api_id=12345\r\n"
174 : "api_hash=" VALID_HASH "\r\n";
175 2 : ASSERT(write_text(ini, body) == 0, "CRLF: write config.ini");
176 :
177 : ApiConfig cfg;
178 2 : int rc = credentials_load(&cfg);
179 2 : flush_logs();
180 :
181 2 : ASSERT(rc == 0, "CRLF: credentials_load succeeds");
182 2 : ASSERT(cfg.api_id == 12345, "CRLF: api_id parsed");
183 2 : ASSERT(cfg.api_hash != NULL, "CRLF: api_hash not NULL");
184 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
185 : "CRLF: api_hash parsed with no trailing \\r");
186 : /* Extra belt-and-braces: there must be no literal CR byte in the
187 : * returned string (was a real regression in the pre-fix parser). */
188 2 : ASSERT(strchr(cfg.api_hash, '\r') == NULL,
189 : "CRLF: returned api_hash carries no CR byte");
190 :
191 2 : rm_rf(home);
192 : }
193 :
194 : /* ================================================================ */
195 : /* 2. UTF-8 BOM */
196 : /* ================================================================ */
197 :
198 : /**
199 : * A BOM (EF BB BF) at the start of config.ini must be skipped so the
200 : * first key on line 1 is still recognised.
201 : */
202 2 : static void test_utf8_bom_skipped_at_start(void) {
203 : char home[512], ini[768], log[768];
204 2 : with_fresh_home("bom", home, sizeof(home), ini, sizeof(ini),
205 : log, sizeof(log));
206 :
207 : /* EF BB BF | api_id=777\napi_hash=...\n */
208 : unsigned char buf[256];
209 2 : size_t off = 0;
210 2 : buf[off++] = 0xEF; buf[off++] = 0xBB; buf[off++] = 0xBF;
211 2 : const char *rest = "api_id=777\napi_hash=" VALID_HASH "\n";
212 2 : memcpy(buf + off, rest, strlen(rest));
213 2 : off += strlen(rest);
214 2 : ASSERT(write_bytes(ini, buf, off) == 0, "BOM: write config.ini");
215 :
216 : ApiConfig cfg;
217 2 : int rc = credentials_load(&cfg);
218 2 : flush_logs();
219 :
220 2 : ASSERT(rc == 0, "BOM: credentials_load succeeds");
221 2 : ASSERT(cfg.api_id == 777, "BOM: api_id parsed past the BOM");
222 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
223 : "BOM: api_hash parsed");
224 :
225 2 : rm_rf(home);
226 : }
227 :
228 : /* ================================================================ */
229 : /* 3. # comment */
230 : /* ================================================================ */
231 :
232 : /**
233 : * A `#` comment line must be skipped entirely — the parser must not try
234 : * to match `api_id` against `# tg-cli config` or similar.
235 : */
236 2 : static void test_hash_comment_ignored(void) {
237 : char home[512], ini[768], log[768];
238 2 : with_fresh_home("hash", home, sizeof(home), ini, sizeof(ini),
239 : log, sizeof(log));
240 :
241 2 : const char *body =
242 : "# tg-cli config\n"
243 : "# generated 2026-04-20\n"
244 : "api_id=42\n"
245 : "api_hash=" VALID_HASH "\n";
246 2 : ASSERT(write_text(ini, body) == 0, "HASH: write config.ini");
247 :
248 : ApiConfig cfg;
249 2 : int rc = credentials_load(&cfg);
250 2 : flush_logs();
251 :
252 2 : ASSERT(rc == 0, "HASH: credentials_load succeeds");
253 2 : ASSERT(cfg.api_id == 42, "HASH: api_id parsed past # comments");
254 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
255 : "HASH: api_hash parsed past # comments");
256 :
257 2 : rm_rf(home);
258 : }
259 :
260 : /* ================================================================ */
261 : /* 4. ; comment */
262 : /* ================================================================ */
263 :
264 : /**
265 : * A `;` alt-comment line must also be skipped entirely.
266 : */
267 2 : static void test_semicolon_comment_ignored(void) {
268 : char home[512], ini[768], log[768];
269 2 : with_fresh_home("semi", home, sizeof(home), ini, sizeof(ini),
270 : log, sizeof(log));
271 :
272 2 : const char *body =
273 : "; alt-comment form\n"
274 : "api_id=99\n"
275 : "; trailing alt-comment\n"
276 : "api_hash=" VALID_HASH "\n";
277 2 : ASSERT(write_text(ini, body) == 0, "SEMI: write config.ini");
278 :
279 : ApiConfig cfg;
280 2 : int rc = credentials_load(&cfg);
281 2 : flush_logs();
282 :
283 2 : ASSERT(rc == 0, "SEMI: credentials_load succeeds");
284 2 : ASSERT(cfg.api_id == 99, "SEMI: api_id parsed past ; comments");
285 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
286 : "SEMI: api_hash parsed past ; comments");
287 :
288 2 : rm_rf(home);
289 : }
290 :
291 : /* ================================================================ */
292 : /* 5. Whitespace */
293 : /* ================================================================ */
294 :
295 : /**
296 : * Leading/trailing whitespace around key AND value must be trimmed.
297 : */
298 2 : static void test_leading_trailing_whitespace_trimmed(void) {
299 : char home[512], ini[768], log[768];
300 2 : with_fresh_home("ws", home, sizeof(home), ini, sizeof(ini),
301 : log, sizeof(log));
302 :
303 2 : const char *body =
304 : " api_id = 12345 \n"
305 : "\tapi_hash\t=\t" VALID_HASH "\t\n";
306 2 : ASSERT(write_text(ini, body) == 0, "WS: write config.ini");
307 :
308 : ApiConfig cfg;
309 2 : int rc = credentials_load(&cfg);
310 2 : flush_logs();
311 :
312 2 : ASSERT(rc == 0, "WS: credentials_load succeeds");
313 2 : ASSERT(cfg.api_id == 12345, "WS: api_id trimmed correctly");
314 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
315 : "WS: api_hash trimmed correctly");
316 :
317 2 : rm_rf(home);
318 : }
319 :
320 : /* ================================================================ */
321 : /* 6. Quoted value */
322 : /* ================================================================ */
323 :
324 : /**
325 : * Double-quoted values must have their quotes stripped so the inner
326 : * string is what reaches ApiConfig.api_hash.
327 : */
328 2 : static void test_quoted_value_strips_quotes(void) {
329 : char home[512], ini[768], log[768];
330 2 : with_fresh_home("quote", home, sizeof(home), ini, sizeof(ini),
331 : log, sizeof(log));
332 :
333 2 : const char *body =
334 : "api_id=1\n"
335 : "api_hash=\"" VALID_HASH "\"\n";
336 2 : ASSERT(write_text(ini, body) == 0, "QUOTE: write config.ini");
337 :
338 : ApiConfig cfg;
339 2 : int rc = credentials_load(&cfg);
340 2 : flush_logs();
341 :
342 2 : ASSERT(rc == 0, "QUOTE: credentials_load succeeds");
343 2 : ASSERT(cfg.api_id == 1, "QUOTE: api_id parsed");
344 2 : ASSERT(cfg.api_hash != NULL, "QUOTE: api_hash not NULL");
345 2 : ASSERT(strcmp(cfg.api_hash, VALID_HASH) == 0,
346 : "QUOTE: surrounding quotes stripped from api_hash");
347 2 : ASSERT(strchr(cfg.api_hash, '"') == NULL,
348 : "QUOTE: no stray quote byte in api_hash");
349 :
350 2 : rm_rf(home);
351 : }
352 :
353 : /* ================================================================ */
354 : /* 7. Empty value */
355 : /* ================================================================ */
356 :
357 : /**
358 : * `api_id=\n` (empty RHS) must be treated as missing, produce a clear
359 : * LOG_ERROR pointing at the wizard, and NOT crash.
360 : */
361 2 : static void test_empty_value_is_missing_credential(void) {
362 : char home[512], ini[768], log[768];
363 2 : with_fresh_home("empty", home, sizeof(home), ini, sizeof(ini),
364 : log, sizeof(log));
365 :
366 2 : const char *body =
367 : "api_id=\n"
368 : "api_hash=" VALID_HASH "\n";
369 2 : ASSERT(write_text(ini, body) == 0, "EMPTY: write config.ini");
370 :
371 : ApiConfig cfg;
372 2 : int rc = credentials_load(&cfg);
373 2 : flush_logs();
374 :
375 2 : ASSERT(rc == -1, "EMPTY: credentials_load reports failure");
376 2 : ASSERT(log_contains(log, "api_id"),
377 : "EMPTY: log mentions api_id");
378 2 : ASSERT(log_contains(log, "wizard") || log_contains(log, "config --wizard"),
379 : "EMPTY: log points user at the wizard");
380 :
381 2 : rm_rf(home);
382 : }
383 :
384 : /* ================================================================ */
385 : /* 8. Only api_id */
386 : /* ================================================================ */
387 :
388 : /**
389 : * Config file has only api_id (no api_hash line at all). Error message
390 : * must target api_hash specifically and reference the wizard.
391 : */
392 2 : static void test_only_api_id_reports_api_hash_missing(void) {
393 : char home[512], ini[768], log[768];
394 2 : with_fresh_home("onlyid", home, sizeof(home), ini, sizeof(ini),
395 : log, sizeof(log));
396 :
397 2 : ASSERT(write_text(ini, "api_id=12345\n") == 0, "ONLYID: write config.ini");
398 :
399 : ApiConfig cfg;
400 2 : int rc = credentials_load(&cfg);
401 2 : flush_logs();
402 :
403 2 : ASSERT(rc == -1, "ONLYID: credentials_load reports failure");
404 2 : ASSERT(log_contains(log, "api_hash"),
405 : "ONLYID: diagnostic references api_hash");
406 2 : ASSERT(log_contains(log, "wizard"),
407 : "ONLYID: diagnostic mentions the wizard");
408 : /* And crucially: it should NOT say api_id is missing. */
409 2 : ASSERT(!log_contains(log, "api_id not found") &&
410 : !log_contains(log, "api_id/api_hash not found"),
411 : "ONLYID: diagnostic does not falsely claim api_id missing");
412 :
413 2 : rm_rf(home);
414 : }
415 :
416 : /* ================================================================ */
417 : /* 9. Only api_hash */
418 : /* ================================================================ */
419 :
420 : /**
421 : * Config file has only api_hash (no api_id). Error must target api_id
422 : * specifically.
423 : */
424 2 : static void test_only_api_hash_reports_api_id_missing(void) {
425 : char home[512], ini[768], log[768];
426 2 : with_fresh_home("onlyhash", home, sizeof(home), ini, sizeof(ini),
427 : log, sizeof(log));
428 :
429 2 : ASSERT(write_text(ini, "api_hash=" VALID_HASH "\n") == 0,
430 : "ONLYHASH: write config.ini");
431 :
432 : ApiConfig cfg;
433 2 : int rc = credentials_load(&cfg);
434 2 : flush_logs();
435 :
436 2 : ASSERT(rc == -1, "ONLYHASH: credentials_load reports failure");
437 2 : ASSERT(log_contains(log, "api_id"),
438 : "ONLYHASH: diagnostic references api_id");
439 2 : ASSERT(log_contains(log, "wizard"),
440 : "ONLYHASH: diagnostic mentions the wizard");
441 2 : ASSERT(!log_contains(log, "api_hash not found"),
442 : "ONLYHASH: diagnostic does not falsely claim api_hash missing");
443 :
444 2 : rm_rf(home);
445 : }
446 :
447 : /* ================================================================ */
448 : /* 10. Duplicate keys */
449 : /* ================================================================ */
450 :
451 : /**
452 : * If a key appears twice, the last occurrence wins AND LOG_WARN is
453 : * emitted explaining the duplicate.
454 : */
455 2 : static void test_duplicate_key_last_wins_and_warns(void) {
456 : char home[512], ini[768], log[768];
457 2 : with_fresh_home("dup", home, sizeof(home), ini, sizeof(ini),
458 : log, sizeof(log));
459 :
460 2 : const char *body =
461 : "api_id=111\n"
462 : "api_id=222\n"
463 : "api_hash=" VALID_HASH "\n";
464 2 : ASSERT(write_text(ini, body) == 0, "DUP: write config.ini");
465 :
466 : ApiConfig cfg;
467 2 : int rc = credentials_load(&cfg);
468 2 : flush_logs();
469 :
470 2 : ASSERT(rc == 0, "DUP: credentials_load succeeds");
471 2 : ASSERT(cfg.api_id == 222, "DUP: last api_id wins (222, not 111)");
472 2 : ASSERT(log_contains(log, "duplicate"),
473 : "DUP: LOG_WARN about duplicate api_id is emitted");
474 :
475 2 : rm_rf(home);
476 : }
477 :
478 : /* ================================================================ */
479 : /* 11. Empty file */
480 : /* ================================================================ */
481 :
482 : /**
483 : * A zero-byte config.ini must be treated like a missing file — clear
484 : * "credentials not found" diagnostic, no crash.
485 : */
486 2 : static void test_empty_file_is_missing_credentials(void) {
487 : char home[512], ini[768], log[768];
488 2 : with_fresh_home("zero", home, sizeof(home), ini, sizeof(ini),
489 : log, sizeof(log));
490 :
491 2 : ASSERT(write_bytes(ini, "", 0) == 0, "ZERO: write empty config.ini");
492 : /* Confirm the file is actually empty on disk. */
493 : struct stat st;
494 2 : ASSERT(stat(ini, &st) == 0 && st.st_size == 0,
495 : "ZERO: config.ini is zero-byte");
496 :
497 : ApiConfig cfg;
498 2 : int rc = credentials_load(&cfg);
499 2 : flush_logs();
500 :
501 2 : ASSERT(rc == -1, "ZERO: credentials_load reports failure");
502 2 : ASSERT(log_contains(log, "api_id") && log_contains(log, "api_hash"),
503 : "ZERO: diagnostic mentions both api_id and api_hash");
504 2 : ASSERT(log_contains(log, "wizard"),
505 : "ZERO: diagnostic mentions the wizard");
506 :
507 2 : rm_rf(home);
508 : }
509 :
510 : /* ================================================================ */
511 : /* 12. Wrong-length api_hash */
512 : /* ================================================================ */
513 :
514 : /**
515 : * An api_hash that is not exactly 32 hex chars must be rejected with a
516 : * dedicated LOG_ERROR and cause credentials_load() to fail, so a
517 : * truncated paste never becomes the live credential.
518 : */
519 2 : static void test_api_hash_wrong_length_rejected(void) {
520 : char home[512], ini[768], log[768];
521 2 : with_fresh_home("hashlen", home, sizeof(home), ini, sizeof(ini),
522 : log, sizeof(log));
523 :
524 : /* 31 chars — one byte too short. */
525 2 : const char *body =
526 : "api_id=12345\n"
527 : "api_hash=deadbeefdeadbeefdeadbeefdeadbee\n";
528 2 : ASSERT(write_text(ini, body) == 0, "HASHLEN: write config.ini");
529 :
530 : ApiConfig cfg;
531 2 : int rc = credentials_load(&cfg);
532 2 : flush_logs();
533 :
534 2 : ASSERT(rc == -1, "HASHLEN: credentials_load reports failure");
535 2 : ASSERT(log_contains(log, "api_hash"),
536 : "HASHLEN: diagnostic mentions api_hash");
537 2 : ASSERT(log_contains(log, "32") || log_contains(log, "hex"),
538 : "HASHLEN: diagnostic explains the expected length/hex rule");
539 :
540 2 : rm_rf(home);
541 :
542 : /* Second variant — 33 chars, over by one — just to exercise the
543 : * other side of the length check. */
544 : char home2[512], ini2[768], log2[768];
545 2 : with_fresh_home("hashlen33", home2, sizeof(home2),
546 : ini2, sizeof(ini2), log2, sizeof(log2));
547 2 : const char *body2 =
548 : "api_id=12345\n"
549 : "api_hash=deadbeefdeadbeefdeadbeefdeadbeef0\n";
550 2 : ASSERT(write_text(ini2, body2) == 0, "HASHLEN33: write config.ini");
551 :
552 : ApiConfig cfg2;
553 2 : int rc2 = credentials_load(&cfg2);
554 2 : flush_logs();
555 :
556 2 : ASSERT(rc2 == -1, "HASHLEN33: 33-char api_hash also rejected");
557 2 : ASSERT(log_contains(log2, "api_hash"),
558 : "HASHLEN33: diagnostic mentions api_hash");
559 :
560 2 : rm_rf(home2);
561 : }
562 :
563 : /* ================================================================ */
564 : /* Suite entry point */
565 : /* ================================================================ */
566 :
567 2 : void run_config_ini_robustness_tests(void) {
568 2 : RUN_TEST(test_crlf_line_endings_parsed_cleanly);
569 2 : RUN_TEST(test_utf8_bom_skipped_at_start);
570 2 : RUN_TEST(test_hash_comment_ignored);
571 2 : RUN_TEST(test_semicolon_comment_ignored);
572 2 : RUN_TEST(test_leading_trailing_whitespace_trimmed);
573 2 : RUN_TEST(test_quoted_value_strips_quotes);
574 2 : RUN_TEST(test_empty_value_is_missing_credential);
575 2 : RUN_TEST(test_only_api_id_reports_api_hash_missing);
576 2 : RUN_TEST(test_only_api_hash_reports_api_id_missing);
577 2 : RUN_TEST(test_duplicate_key_last_wins_and_warns);
578 2 : RUN_TEST(test_empty_file_is_missing_credentials);
579 2 : RUN_TEST(test_api_hash_wrong_length_rejected);
580 2 : }
|