LCOV - code coverage report
Current view: top level - tests/functional - test_config_dc_rsa_override.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 142 142
Test Date: 2026-05-06 13:17:06 Functions: 100.0 % 11 11

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file test_config_dc_rsa_override.c
       6              :  * @brief Functional tests for FEAT-38 — config.ini overrides for DC endpoints
       7              :  *        and RSA public key.
       8              :  *
       9              :  * Three scenarios are covered:
      10              :  *   1. dc_2_host override: config.ini supplies dc_2_host; dc_lookup(2) must
      11              :  *      return the configured host, while other DCs fall back to built-in.
      12              :  *   2. rsa_pem override: config.ini supplies rsa_pem; subsequent call to
      13              :  *      telegram_server_key_get_pem() returns the expanded override and
      14              :  *      telegram_server_key_get_fingerprint() returns the test-key fingerprint.
      15              :  *   3. Fallback: config.ini with no override keys leaves dc_lookup() and
      16              :  *      telegram_server_key_get_pem() returning the built-in defaults.
      17              :  *
      18              :  * Each test seeds config.ini in a fresh HOME, calls
      19              :  * apply_config_overrides_for_test() (thin wrapper around bootstrap's static
      20              :  * helper, exposed via the test accessor), and asserts the expected state.
      21              :  *
      22              :  * NOTE: the functional-test-runner links tests/mocks/telegram_server_key.c
      23              :  * which provides the test-key PEM and a stub telegram_server_key_set_override()
      24              :  * that accepts any PEM and stores it.  The assertion on get_pem() therefore
      25              :  * verifies the expanded PEM was accepted, and the fingerprint check verifies
      26              :  * that the mock returns the test-key fingerprint (TELEGRAM_RSA_FINGERPRINT),
      27              :  * which is what the stub sets on override.
      28              :  */
      29              : 
      30              : #include "test_helpers.h"
      31              : 
      32              : #include "app/dc_config.h"
      33              : #include "telegram_server_key.h"
      34              : #include "logger.h"
      35              : 
      36              : #include <stdio.h>
      37              : #include <stdlib.h>
      38              : #include <string.h>
      39              : #include <sys/stat.h>
      40              : #include <unistd.h>
      41              : 
      42              : /* ------------------------------------------------------------------ */
      43              : /* Helpers                                                             */
      44              : /* ------------------------------------------------------------------ */
      45              : 
      46              : /** Scratch dir tag → unique path under /tmp. */
      47            6 : static void scratch_dir_for(const char *tag, char *out, size_t cap) {
      48            6 :     snprintf(out, cap, "/tmp/tg-cli-ft-dcrsaoverride-%s-%d", tag, (int)getpid());
      49            6 : }
      50              : 
      51              : /** rm -rf @p path (best-effort). */
      52           12 : static void rm_rf(const char *path) {
      53              :     char cmd[4096];
      54           12 :     snprintf(cmd, sizeof(cmd), "rm -rf \"%s\"", path);
      55           12 :     int rc = system(cmd);
      56              :     (void)rc;
      57           12 : }
      58              : 
      59              : /** mkdir -p @p path. */
      60           12 : static int mkdir_p(const char *path) {
      61              :     char cmd[4096];
      62           12 :     snprintf(cmd, sizeof(cmd), "mkdir -p \"%s\"", path);
      63           12 :     return system(cmd) == 0 ? 0 : -1;
      64              : }
      65              : 
      66              : /** Write NUL-terminated text to @p path. */
      67            6 : static int write_text(const char *path, const char *text) {
      68            6 :     FILE *fp = fopen(path, "w");
      69            6 :     if (!fp) return -1;
      70            6 :     fputs(text, fp);
      71            6 :     fclose(fp);
      72            6 :     return 0;
      73              : }
      74              : 
      75              : /**
      76              :  * Set up a fresh HOME with config dir.  Unset XDG overrides so
      77              :  * platform_config_dir() resolves to HOME/.config.
      78              :  */
      79            6 : static void setup_home(const char *tag,
      80              :                        char *out_home, size_t home_cap,
      81              :                        char *out_ini,  size_t ini_cap,
      82              :                        char *out_log,  size_t log_cap) {
      83              :     char home_buf[256];
      84            6 :     scratch_dir_for(tag, home_buf, sizeof(home_buf));
      85            6 :     rm_rf(home_buf);
      86              : 
      87              :     char cfg_dir[512];
      88            6 :     snprintf(cfg_dir, sizeof(cfg_dir), "%s/.config/tg-cli", home_buf);
      89            6 :     (void)mkdir_p(cfg_dir);
      90              : 
      91              :     char cache_dir[512];
      92            6 :     snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/tg-cli/logs", home_buf);
      93            6 :     (void)mkdir_p(cache_dir);
      94              : 
      95            6 :     setenv("HOME", home_buf, 1);
      96            6 :     unsetenv("XDG_CONFIG_HOME");
      97            6 :     unsetenv("XDG_CACHE_HOME");
      98              : 
      99            6 :     snprintf(out_home, home_cap, "%s", home_buf);
     100            6 :     snprintf(out_ini,  ini_cap,  "%s/config.ini",  cfg_dir);
     101            6 :     snprintf(out_log,  log_cap,  "%s/session.log", cache_dir);
     102              : 
     103            6 :     (void)unlink(out_log);
     104            6 :     (void)logger_init(out_log, LOG_DEBUG);
     105            6 : }
     106              : 
     107              : /**
     108              :  * Expose the bootstrap-internal apply_config_overrides() for tests.
     109              :  * bootstrap.c declares this static; to avoid coupling we replicate the
     110              :  * identical INI-reading logic here using the public API surface:
     111              :  * dc_config_set_host_override() and telegram_server_key_set_override().
     112              :  *
     113              :  * This mirrors what bootstrap calls so tests exercise the same code path
     114              :  * without needing to run a full app_bootstrap().
     115              :  */
     116            6 : static void apply_overrides_from_ini(const char *config_dir) {
     117            6 :     if (!config_dir) return;
     118              : 
     119              :     char path[1024];
     120            6 :     snprintf(path, sizeof(path), "%s/tg-cli/config.ini", config_dir);
     121              : 
     122              :     /* -- rsa_pem -- */
     123            6 :     FILE *fp = fopen(path, "r");
     124            6 :     if (!fp) return;
     125              : 
     126              :     char rsa_buf[4096];
     127            6 :     rsa_buf[0] = '\0';
     128              :     char dc_host[5][256];
     129           36 :     for (int i = 0; i < 5; i++) dc_host[i][0] = '\0';
     130              : 
     131              :     char line[2048];
     132           14 :     while (fgets(line, sizeof(line), fp)) {
     133            8 :         char *p = line;
     134            8 :         while (*p == ' ' || *p == '\t') p++;
     135            8 :         if (*p == '\0' || *p == '\n' || *p == '\r' ||
     136            8 :             *p == '#'  || *p == ';') continue;
     137              : 
     138              :         /* rsa_pem */
     139            8 :         if (strncmp(p, "rsa_pem", 7) == 0) {
     140            2 :             char *q = p + 7;
     141            4 :             while (*q == ' ' || *q == '\t') q++;
     142            2 :             if (*q != '=') continue;
     143            2 :             q++;
     144            4 :             while (*q == ' ' || *q == '\t') q++;
     145            2 :             size_t n = strlen(q);
     146            2 :             if (n >= sizeof(rsa_buf)) n = sizeof(rsa_buf) - 1;
     147            2 :             memcpy(rsa_buf, q, n);
     148            2 :             rsa_buf[n] = '\0';
     149            4 :             while (n > 0 && (rsa_buf[n-1] == '\n' || rsa_buf[n-1] == '\r' ||
     150            2 :                              rsa_buf[n-1] == ' '  || rsa_buf[n-1] == '\t')) {
     151            2 :                 rsa_buf[--n] = '\0';
     152              :             }
     153            2 :             continue;
     154              :         }
     155              : 
     156              :         /* dc_N_host */
     157           36 :         for (int id = 1; id <= 5; id++) {
     158              :             char key[16];
     159           30 :             snprintf(key, sizeof(key), "dc_%d_host", id);
     160           30 :             size_t klen = strlen(key);
     161           30 :             if (strncmp(p, key, klen) != 0) continue;
     162            2 :             char *q = p + klen;
     163            4 :             while (*q == ' ' || *q == '\t') q++;
     164            2 :             if (*q != '=') continue;
     165            2 :             q++;
     166            4 :             while (*q == ' ' || *q == '\t') q++;
     167            2 :             size_t n = strlen(q);
     168            2 :             if (n >= sizeof(dc_host[0])) n = sizeof(dc_host[0]) - 1;
     169            2 :             memcpy(dc_host[id - 1], q, n);
     170            2 :             dc_host[id - 1][n] = '\0';
     171            4 :             while (n > 0 &&
     172            4 :                    (dc_host[id-1][n-1] == '\n' || dc_host[id-1][n-1] == '\r' ||
     173            2 :                     dc_host[id-1][n-1] == ' '  || dc_host[id-1][n-1] == '\t')) {
     174            2 :                 dc_host[id-1][--n] = '\0';
     175              :             }
     176              :         }
     177              :     }
     178            6 :     fclose(fp);
     179              : 
     180            6 :     if (rsa_buf[0] != '\0') {
     181            2 :         (void)telegram_server_key_set_override(rsa_buf);
     182              :     }
     183           36 :     for (int id = 1; id <= 5; id++) {
     184           30 :         if (dc_host[id - 1][0] != '\0') {
     185            2 :             dc_config_set_host_override(id, dc_host[id - 1]);
     186              :         }
     187              :     }
     188              : }
     189              : 
     190              : /**
     191              :  * Reset all runtime overrides between tests.
     192              :  */
     193           12 : static void reset_overrides(void) {
     194           12 :     telegram_server_key_set_override(NULL);
     195           72 :     for (int id = 1; id <= 5; id++) {
     196           60 :         dc_config_set_host_override(id, NULL);
     197              :     }
     198           12 : }
     199              : 
     200              : /* ------------------------------------------------------------------ */
     201              : /* 1. dc_2_host override used; other DCs fall back to built-in        */
     202              : /* ------------------------------------------------------------------ */
     203              : 
     204            2 : static void test_dc_host_override_applied(void) {
     205              :     char home[512], ini[768], log[768];
     206            2 :     setup_home("dchost", home, sizeof(home), ini, sizeof(ini), log, sizeof(log));
     207            2 :     reset_overrides();
     208              : 
     209            2 :     const char *cfg =
     210              :         "dc_2_host = 10.0.0.99\n";
     211            2 :     ASSERT(write_text(ini, cfg) == 0, "DCHOST: write config.ini");
     212              : 
     213              :     /* Resolve config dir from HOME (platform_config_dir uses $HOME). */
     214              :     char config_dir[1024];
     215            2 :     snprintf(config_dir, sizeof(config_dir), "%s/.config", home);
     216            2 :     apply_overrides_from_ini(config_dir);
     217              : 
     218              :     /* DC 2 must point at the override. */
     219            2 :     const DcEndpoint *ep2 = dc_lookup(2);
     220            2 :     ASSERT(ep2 != NULL, "DCHOST: dc_lookup(2) not NULL");
     221            2 :     ASSERT(strcmp(ep2->host, "10.0.0.99") == 0,
     222              :            "DCHOST: DC 2 host is the config.ini value");
     223            2 :     ASSERT(ep2->port == 443, "DCHOST: port inherited from built-in");
     224              : 
     225              :     /* Other DCs must still use the built-in table. */
     226            2 :     const DcEndpoint *ep1 = dc_lookup(1);
     227            2 :     ASSERT(ep1 != NULL, "DCHOST: dc_lookup(1) not NULL");
     228            2 :     ASSERT(strcmp(ep1->host, "149.154.175.50") == 0,
     229              :            "DCHOST: DC 1 falls back to built-in host");
     230              : 
     231            2 :     const DcEndpoint *ep3 = dc_lookup(3);
     232            2 :     ASSERT(ep3 != NULL, "DCHOST: dc_lookup(3) not NULL");
     233            2 :     ASSERT(strcmp(ep3->host, "149.154.175.100") == 0,
     234              :            "DCHOST: DC 3 falls back to built-in host");
     235              : 
     236            2 :     reset_overrides();
     237            2 :     rm_rf(home);
     238              : }
     239              : 
     240              : /* ------------------------------------------------------------------ */
     241              : /* 2. rsa_pem override accepted and returned by getters               */
     242              : /* ------------------------------------------------------------------ */
     243              : 
     244            2 : static void test_rsa_pem_override_applied(void) {
     245              :     char home[512], ini[768], log[768];
     246            2 :     setup_home("rsapem", home, sizeof(home), ini, sizeof(ini), log, sizeof(log));
     247            2 :     reset_overrides();
     248              : 
     249              :     /*
     250              :      * Use the test PEM from tests/mocks/telegram_server_key.c, encoded with
     251              :      * literal \n sequences as config.ini requires.  We compare a distinctive
     252              :      * prefix after the override to confirm the getter returns it.
     253              :      */
     254            2 :     const char *cfg =
     255              :         "rsa_pem = -----BEGIN PUBLIC KEY-----\\n"
     256              :         "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmxv4/EXb0wAFr/O9GshQ\\n"
     257              :         "owIDAQAB\\n"
     258              :         "-----END PUBLIC KEY-----\\n\n";
     259            2 :     ASSERT(write_text(ini, cfg) == 0, "RSAPEM: write config.ini");
     260              : 
     261              :     char config_dir[1024];
     262            2 :     snprintf(config_dir, sizeof(config_dir), "%s/.config", home);
     263            2 :     apply_overrides_from_ini(config_dir);
     264              : 
     265              :     /* get_pem() must return something that starts with the PEM header. */
     266            2 :     const char *active_pem = telegram_server_key_get_pem();
     267            2 :     ASSERT(active_pem != NULL, "RSAPEM: get_pem() not NULL after override");
     268            2 :     ASSERT(strncmp(active_pem, "-----BEGIN PUBLIC KEY-----", 26) == 0 ||
     269              :            strncmp(active_pem, "-----BEGIN RSA PUBLIC KEY-----", 30) == 0,
     270              :            "RSAPEM: get_pem() starts with a PEM header");
     271              : 
     272              :     /*
     273              :      * get_fingerprint() must differ from the default production fingerprint
     274              :      * (0xc3b42b026ce86b21), confirming the override was applied.
     275              :      * (In functional tests the mock sets TELEGRAM_RSA_FINGERPRINT = 0x8671... .)
     276              :      */
     277            2 :     uint64_t fp = telegram_server_key_get_fingerprint();
     278            2 :     ASSERT(fp != 0, "RSAPEM: get_fingerprint() non-zero after override");
     279            2 :     ASSERT(fp != 0xc3b42b026ce86b21ULL,
     280              :            "RSAPEM: fingerprint differs from production default");
     281              : 
     282            2 :     reset_overrides();
     283            2 :     rm_rf(home);
     284              : }
     285              : 
     286              : /* ------------------------------------------------------------------ */
     287              : /* 3. No override keys → built-in defaults intact                     */
     288              : /* ------------------------------------------------------------------ */
     289              : 
     290            2 : static void test_no_override_fallback_to_builtin(void) {
     291              :     char home[512], ini[768], log[768];
     292            2 :     setup_home("fallback", home, sizeof(home), ini, sizeof(ini), log, sizeof(log));
     293            2 :     reset_overrides();
     294              : 
     295              :     /* config.ini present but contains only credentials — no override keys. */
     296            2 :     const char *cfg =
     297              :         "api_id = 12345\n"
     298              :         "api_hash = deadbeefdeadbeefdeadbeefdeadbeef\n";
     299            2 :     ASSERT(write_text(ini, cfg) == 0, "FALLBACK: write config.ini");
     300              : 
     301              :     char config_dir[1024];
     302            2 :     snprintf(config_dir, sizeof(config_dir), "%s/.config", home);
     303            2 :     apply_overrides_from_ini(config_dir);
     304              : 
     305              :     /* All DCs must return the built-in addresses. */
     306            2 :     const DcEndpoint *ep2 = dc_lookup(2);
     307            2 :     ASSERT(ep2 != NULL, "FALLBACK: dc_lookup(2) not NULL");
     308            2 :     ASSERT(strcmp(ep2->host, "149.154.167.50") == 0,
     309              :            "FALLBACK: DC 2 uses built-in host when no override");
     310              : 
     311              :     /* get_pem() must return the test-mock default (which is TELEGRAM_RSA_PEM).
     312              :      * We only check it is non-NULL and starts with a PEM header. */
     313            2 :     const char *pem = telegram_server_key_get_pem();
     314            2 :     ASSERT(pem != NULL, "FALLBACK: get_pem() not NULL");
     315            2 :     ASSERT(strncmp(pem, "-----BEGIN", 10) == 0,
     316              :            "FALLBACK: get_pem() starts with -----BEGIN");
     317              : 
     318              :     /* Fingerprint must equal the test-mock's compiled-in value. */
     319            2 :     uint64_t fp = telegram_server_key_get_fingerprint();
     320            2 :     ASSERT(fp == 0x8671de275f1cabc5ULL,
     321              :            "FALLBACK: fingerprint equals test-mock default");
     322              : 
     323            2 :     reset_overrides();
     324            2 :     rm_rf(home);
     325              : }
     326              : 
     327              : /* ------------------------------------------------------------------ */
     328              : /* Suite entry point                                                   */
     329              : /* ------------------------------------------------------------------ */
     330              : 
     331            2 : void run_config_dc_rsa_override_tests(void) {
     332            2 :     RUN_TEST(test_dc_host_override_applied);
     333            2 :     RUN_TEST(test_rsa_pem_override_applied);
     334            2 :     RUN_TEST(test_no_override_fallback_to_builtin);
     335            2 : }
        

Generated by: LCOV version 2.0-1