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