Line data Source code
1 : /* SPDX-License-Identifier: GPL-3.0-or-later */
2 : /* Copyright 2026 Peter Csaszar */
3 :
4 : /**
5 : * @file test_config_wizard_batch.c
6 : * @brief Functional tests for config_wizard_run_batch() (FEAT-37).
7 : *
8 : * Tests:
9 : * - Happy path: writes config.ini with mode 0600 and correct content.
10 : * - Refuses to overwrite an existing non-empty config without --force.
11 : * - Accepts overwrite with --force.
12 : * - Rejects malformed api_id (0, negative, non-numeric).
13 : * - Rejects malformed api_hash (wrong length, uppercase, non-hex).
14 : */
15 :
16 : #include "test_helpers.h"
17 : #include "app/config_wizard.h"
18 :
19 : #include <stdio.h>
20 : #include <stdlib.h>
21 : #include <string.h>
22 : #include <sys/stat.h>
23 : #include <unistd.h>
24 :
25 : /* ---- Helpers ---- */
26 :
27 : /** Return a temp dir unique to this test run. */
28 60 : static const char *get_tmp_dir(void) {
29 : static char tmp[256];
30 60 : if (tmp[0]) return tmp;
31 2 : const char *t = getenv("TMPDIR");
32 2 : if (!t) t = "/tmp";
33 2 : snprintf(tmp, sizeof(tmp), "%s/tg-cli-wiz-test-%d", t, (int)getpid());
34 2 : return tmp;
35 : }
36 :
37 : /** Set XDG_CONFIG_HOME to @p dir so config_wizard writes there. */
38 22 : static void set_config_home(const char *dir) {
39 22 : setenv("XDG_CONFIG_HOME", dir, 1);
40 22 : }
41 :
42 : /** Build the expected config.ini path. */
43 32 : static void cfg_path(char *out, size_t cap) {
44 32 : snprintf(out, cap, "%s/tg-cli/config.ini", get_tmp_dir());
45 32 : }
46 :
47 : /** Remove the test config file (ignore errors). */
48 24 : static void rm_cfg(void) {
49 24 : char p[512]; cfg_path(p, sizeof(p));
50 24 : unlink(p);
51 24 : }
52 :
53 : /** Create a non-empty config file to simulate "already configured". */
54 4 : static void create_nonempty_cfg(void) {
55 4 : char p[512]; cfg_path(p, sizeof(p));
56 4 : char dir[512]; snprintf(dir, sizeof(dir), "%s/tg-cli", get_tmp_dir());
57 : /* mkdir -p */
58 4 : char cmd[1024]; snprintf(cmd, sizeof(cmd), "mkdir -p %s", dir);
59 4 : int sysrc = system(cmd); (void)sysrc;
60 4 : FILE *fp = fopen(p, "w");
61 4 : if (fp) { fprintf(fp, "api_id=99999\napi_hash=%s\n", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1"); fclose(fp); }
62 4 : }
63 :
64 : /* ---- Tests ---- */
65 :
66 2 : static void test_batch_happy_path(void) {
67 2 : set_config_home(get_tmp_dir());
68 2 : rm_cfg();
69 :
70 2 : int rc = config_wizard_run_batch("12345",
71 : "deadbeefdeadbeefdeadbeefdeadbeef",
72 : 0);
73 2 : ASSERT(rc == 0, "batch happy: must return 0");
74 :
75 : /* Check file exists. */
76 2 : char p[512]; cfg_path(p, sizeof(p));
77 : struct stat st;
78 2 : ASSERT(stat(p, &st) == 0, "batch happy: config.ini must exist");
79 :
80 : /* Check mode 0600. */
81 2 : ASSERT((st.st_mode & 0777) == 0600, "batch happy: mode must be 0600");
82 :
83 : /* Check content. */
84 2 : FILE *fp = fopen(p, "r");
85 2 : ASSERT(fp != NULL, "batch happy: can open config.ini");
86 2 : char content[512] = {0};
87 2 : size_t n = fread(content, 1, sizeof(content) - 1, fp);
88 2 : fclose(fp);
89 2 : ASSERT(n > 0, "batch happy: config.ini is non-empty");
90 2 : ASSERT(strstr(content, "api_id=12345") != NULL,
91 : "batch happy: config.ini contains api_id=12345");
92 2 : ASSERT(strstr(content, "api_hash=deadbeefdeadbeefdeadbeefdeadbeef") != NULL,
93 : "batch happy: config.ini contains correct api_hash");
94 :
95 2 : rm_cfg();
96 : }
97 :
98 2 : static void test_batch_refuses_overwrite_without_force(void) {
99 2 : set_config_home(get_tmp_dir());
100 2 : create_nonempty_cfg();
101 :
102 2 : int rc = config_wizard_run_batch("12345",
103 : "deadbeefdeadbeefdeadbeefdeadbeef",
104 : 0 /* force=0 */);
105 2 : ASSERT(rc != 0, "batch no-force: must refuse to overwrite existing config");
106 :
107 2 : rm_cfg();
108 : }
109 :
110 2 : static void test_batch_force_overwrites(void) {
111 2 : set_config_home(get_tmp_dir());
112 2 : create_nonempty_cfg();
113 :
114 2 : int rc = config_wizard_run_batch("99",
115 : "abcdef0123456789abcdef0123456789",
116 : 1 /* force=1 */);
117 2 : ASSERT(rc == 0, "batch --force: must succeed");
118 :
119 2 : char p[512]; cfg_path(p, sizeof(p));
120 2 : FILE *fp = fopen(p, "r");
121 2 : ASSERT(fp != NULL, "batch --force: config.ini must exist after overwrite");
122 2 : char content[512] = {0};
123 2 : size_t nf = fread(content, 1, sizeof(content) - 1, fp);
124 2 : fclose(fp);
125 : (void)nf;
126 2 : ASSERT(strstr(content, "api_id=99") != NULL,
127 : "batch --force: config.ini has new api_id");
128 :
129 2 : rm_cfg();
130 : }
131 :
132 2 : static void test_batch_rejects_zero_api_id(void) {
133 2 : set_config_home(get_tmp_dir());
134 2 : rm_cfg();
135 2 : ASSERT(config_wizard_run_batch("0", "deadbeefdeadbeefdeadbeefdeadbeef", 0) != 0,
136 : "api_id=0: must fail");
137 : }
138 :
139 2 : static void test_batch_rejects_negative_api_id(void) {
140 2 : set_config_home(get_tmp_dir());
141 2 : rm_cfg();
142 2 : ASSERT(config_wizard_run_batch("-1", "deadbeefdeadbeefdeadbeefdeadbeef", 0) != 0,
143 : "api_id=-1: must fail");
144 : }
145 :
146 2 : static void test_batch_rejects_non_numeric_api_id(void) {
147 2 : set_config_home(get_tmp_dir());
148 2 : rm_cfg();
149 2 : ASSERT(config_wizard_run_batch("abc", "deadbeefdeadbeefdeadbeefdeadbeef", 0) != 0,
150 : "api_id=abc: must fail");
151 : }
152 :
153 2 : static void test_batch_rejects_wrong_length_api_hash(void) {
154 2 : set_config_home(get_tmp_dir());
155 2 : rm_cfg();
156 : /* 31 chars — too short */
157 2 : ASSERT(config_wizard_run_batch("12345", "deadbeefdeadbeefdeadbeefdeadbee", 0) != 0,
158 : "api_hash 31 chars: must fail");
159 : }
160 :
161 2 : static void test_batch_rejects_uppercase_api_hash(void) {
162 2 : set_config_home(get_tmp_dir());
163 2 : rm_cfg();
164 : /* 32 chars but uppercase */
165 2 : ASSERT(config_wizard_run_batch("12345", "DEADBEEFDEADBEEFDEADBEEFDEADBEEF", 0) != 0,
166 : "api_hash uppercase: must fail");
167 : }
168 :
169 2 : static void test_batch_rejects_non_hex_api_hash(void) {
170 2 : set_config_home(get_tmp_dir());
171 2 : rm_cfg();
172 : /* 32 chars but 'z' is not hex */
173 2 : ASSERT(config_wizard_run_batch("12345", "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", 0) != 0,
174 : "api_hash non-hex: must fail");
175 : }
176 :
177 2 : static void test_batch_null_args(void) {
178 2 : set_config_home(get_tmp_dir());
179 2 : rm_cfg();
180 2 : ASSERT(config_wizard_run_batch(NULL, "deadbeefdeadbeefdeadbeefdeadbeef", 0) != 0,
181 : "null api_id_str: must fail");
182 2 : ASSERT(config_wizard_run_batch("12345", NULL, 0) != 0,
183 : "null api_hash_str: must fail");
184 : }
185 :
186 : /* Script-invocation guard: when stdin is not a TTY (e.g. piped from a
187 : * shell script or a cron job), config_wizard_run_interactive() must
188 : * bail out immediately with a clear error rather than block on fgets
189 : * (which would read EOF in a tight loop or hang). CTest runs its
190 : * children with stdin attached to a pipe, so calling the function
191 : * here is the exact script scenario. */
192 2 : static void test_interactive_refuses_when_stdin_not_tty(void) {
193 2 : set_config_home(get_tmp_dir());
194 2 : rm_cfg();
195 2 : ASSERT(!isatty(STDIN_FILENO),
196 : "precondition: CTest gives us a non-TTY stdin");
197 : /* Must return -1 quickly — no read, no hang. */
198 2 : ASSERT(config_wizard_run_interactive() == -1,
199 : "interactive wizard: must refuse non-TTY stdin with rc=-1");
200 : /* Must NOT have created the config file on this error path. */
201 : struct stat st;
202 : char cfg[1024];
203 2 : snprintf(cfg, sizeof(cfg), "%s/tg-cli/config.ini", get_tmp_dir());
204 2 : ASSERT(stat(cfg, &st) != 0,
205 : "interactive wizard: must not create config on TTY guard failure");
206 : }
207 :
208 : /* ---- Runner ---- */
209 :
210 2 : void run_config_wizard_batch_tests(void) {
211 2 : RUN_TEST(test_batch_happy_path);
212 2 : RUN_TEST(test_batch_refuses_overwrite_without_force);
213 2 : RUN_TEST(test_batch_force_overwrites);
214 2 : RUN_TEST(test_batch_rejects_zero_api_id);
215 2 : RUN_TEST(test_batch_rejects_negative_api_id);
216 2 : RUN_TEST(test_batch_rejects_non_numeric_api_id);
217 2 : RUN_TEST(test_batch_rejects_wrong_length_api_hash);
218 2 : RUN_TEST(test_batch_rejects_uppercase_api_hash);
219 2 : RUN_TEST(test_batch_rejects_non_hex_api_hash);
220 2 : RUN_TEST(test_batch_null_args);
221 2 : RUN_TEST(test_interactive_refuses_when_stdin_not_tty);
222 2 : }
|