LCOV - code coverage report
Current view: top level - tests/functional - test_config_ini_robustness.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 211 211
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 22 22

            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 : }
        

Generated by: LCOV version 2.0-1