Line data Source code
1 : #include "test_helpers.h"
2 : #include "setup_wizard.h"
3 : #include "raii.h"
4 : #include <stdio.h>
5 : #include <string.h>
6 : #include <stdlib.h>
7 : #include <unistd.h>
8 :
9 16 : static void config_cleanup(void *ptr) {
10 16 : Config **cfg = (Config **)ptr;
11 16 : if (cfg && *cfg) {
12 11 : config_free(*cfg);
13 11 : *cfg = NULL;
14 : }
15 16 : }
16 :
17 1 : void test_wizard(void) {
18 : // 1. Full valid input
19 : {
20 1 : const char *input = "1\nimaps://imap.test.com\n\ntest@user.com\nsecretpass\nMYFOLDER\n";
21 2 : RAII_FILE FILE *stream = fmemopen((void*)input, strlen(input), "r");
22 1 : ASSERT(stream != NULL, "fmemopen should succeed");
23 :
24 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
25 1 : ASSERT(cfg != NULL, "setup_wizard_run_internal should return config");
26 1 : ASSERT(strcmp(cfg->host, "imaps://imap.test.com") == 0, "Host should match input");
27 1 : ASSERT(strcmp(cfg->user, "test@user.com") == 0, "User should match input");
28 1 : ASSERT(strcmp(cfg->pass, "secretpass") == 0, "Pass should match input");
29 1 : ASSERT(strcmp(cfg->folder, "MYFOLDER") == 0, "Folder should match input");
30 : }
31 :
32 : // 2. Empty folder defaults to INBOX
33 : {
34 1 : const char *input2 = "1\nh\n\nu\np\n\n";
35 2 : RAII_FILE FILE *stream = fmemopen((void*)input2, strlen(input2), "r");
36 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
37 1 : ASSERT(cfg != NULL, "setup_wizard should return config with empty folder");
38 1 : ASSERT(strcmp(cfg->folder, "INBOX") == 0, "Folder should default to INBOX");
39 : }
40 :
41 : // 3. EOF after host: user field missing → should return NULL
42 : {
43 1 : const char *input3 = "1\nimaps://imap.test.com\n\n";
44 2 : RAII_FILE FILE *stream = fmemopen((void*)input3, strlen(input3), "r");
45 1 : ASSERT(stream != NULL, "fmemopen should succeed for partial input");
46 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
47 1 : ASSERT(cfg == NULL, "setup_wizard should return NULL when user input is missing");
48 : }
49 :
50 : // 4. EOF after host+user: password field missing → should return NULL
51 : {
52 1 : const char *input4 = "1\nimaps://imap.test.com\n\ntest@user.com\n";
53 2 : RAII_FILE FILE *stream = fmemopen((void*)input4, strlen(input4), "r");
54 1 : ASSERT(stream != NULL, "fmemopen should succeed for partial input");
55 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
56 1 : ASSERT(cfg == NULL, "setup_wizard should return NULL when password is missing");
57 : }
58 :
59 : // 5. Test setup_wizard_run() (stdin path) via pipe redirect
60 : {
61 1 : const char *input5 = "1\nimaps://imap.stdin.com\n\nstdin@user.com\nstdinpass\nSTDIN\n";
62 : int pipefd[2];
63 1 : ASSERT(pipe(pipefd) == 0, "pipe() should succeed");
64 1 : ssize_t written = write(pipefd[1], input5, strlen(input5));
65 1 : ASSERT(written > 0, "write to pipe should succeed");
66 1 : close(pipefd[1]);
67 :
68 1 : int saved_stdin = dup(STDIN_FILENO);
69 1 : dup2(pipefd[0], STDIN_FILENO);
70 1 : close(pipefd[0]);
71 :
72 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run();
73 :
74 1 : dup2(saved_stdin, STDIN_FILENO);
75 1 : close(saved_stdin);
76 :
77 1 : ASSERT(cfg != NULL, "setup_wizard_run should return config via pipe");
78 1 : ASSERT(strcmp(cfg->host, "imaps://imap.stdin.com") == 0, "Host should match stdin input");
79 1 : ASSERT(strcmp(cfg->user, "stdin@user.com") == 0, "User should match stdin input");
80 1 : ASSERT(strcmp(cfg->pass, "stdinpass") == 0, "Pass should match stdin input");
81 1 : ASSERT(strcmp(cfg->folder, "STDIN") == 0, "Folder should match stdin input");
82 : }
83 :
84 : // 6. Custom IMAP port → appended to URL
85 : {
86 1 : const char *input6p = "1\nimap.custom.com\n10993\nuser@custom.com\npass\nINBOX\n";
87 2 : RAII_FILE FILE *stream = fmemopen((void*)input6p, strlen(input6p), "r");
88 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
89 1 : ASSERT(cfg != NULL, "Port wizard: returns config");
90 1 : ASSERT(strcmp(cfg->host, "imaps://imap.custom.com:10993") == 0,
91 : "Port wizard: custom port appended to URL");
92 : }
93 :
94 : // 7. Default IMAP port (empty) → no port in URL
95 : {
96 1 : const char *input7 = "1\nimap.default.com\n\nuser@default.com\npass\nINBOX\n";
97 2 : RAII_FILE FILE *stream = fmemopen((void*)input7, strlen(input7), "r");
98 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
99 1 : ASSERT(cfg != NULL, "Port wizard default: returns config");
100 1 : ASSERT(strcmp(cfg->host, "imaps://imap.default.com") == 0,
101 : "Port wizard default: no port suffix");
102 : }
103 :
104 : // 8. Explicit 993 → no port in URL (same as default)
105 : {
106 1 : const char *input8 = "1\nimap.explicit.com\n993\nuser@explicit.com\npass\nINBOX\n";
107 2 : RAII_FILE FILE *stream = fmemopen((void*)input8, strlen(input8), "r");
108 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
109 1 : ASSERT(cfg != NULL, "Port wizard 993: returns config");
110 1 : ASSERT(strcmp(cfg->host, "imaps://imap.explicit.com") == 0,
111 : "Port wizard 993: no port suffix (993 is default)");
112 : }
113 :
114 : // 9. Host already has port → port prompt ignored
115 : {
116 1 : const char *input9 = "1\nimap.ported.com:995\n1234\nuser@ported.com\npass\nINBOX\n";
117 2 : RAII_FILE FILE *stream = fmemopen((void*)input9, strlen(input9), "r");
118 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
119 1 : ASSERT(cfg != NULL, "Port wizard existing: returns config");
120 1 : ASSERT(strcmp(cfg->host, "imaps://imap.ported.com:995") == 0,
121 : "Port wizard existing: original port preserved");
122 : }
123 :
124 : // 10. Gmail type: EOF after email → NULL (no device flow in test)
125 : {
126 1 : const char *input6 = "2\ngmail@example.com\n";
127 2 : RAII_FILE FILE *stream = fmemopen((void*)input6, strlen(input6), "r");
128 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
129 : /* Device flow fails (no real OAuth) → but cfg should have gmail_mode set
130 : * and user set before failing. The non-TTY path skips device flow
131 : * and returns cfg with gmail_mode=1. */
132 1 : if (cfg) {
133 1 : ASSERT(cfg->gmail_mode == 1, "Gmail wizard: gmail_mode=1");
134 1 : ASSERT(strcmp(cfg->user, "gmail@example.com") == 0, "Gmail wizard: user matches");
135 : }
136 : /* cfg may be NULL if device flow was attempted and failed — that's OK */
137 : }
138 :
139 : // 11. Gmail type: empty email → NULL
140 : {
141 1 : const char *input11 = "2\n\n";
142 2 : RAII_FILE FILE *stream = fmemopen((void*)input11, strlen(input11), "r");
143 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
144 1 : ASSERT(cfg == NULL, "Gmail wizard: empty email returns NULL");
145 : }
146 :
147 : // 12. IMAP unsupported protocol (imap://) → wizard rejects, retries, then host EOF → NULL
148 : {
149 : /* First host uses unsupported protocol → printed error, re-prompt;
150 : * second attempt gets EOF → NULL */
151 1 : const char *input12 = "1\nftp://imap.test.com\n";
152 2 : RAII_FILE FILE *stream = fmemopen((void*)input12, strlen(input12), "r");
153 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
154 1 : ASSERT(cfg == NULL, "IMAP wizard: unsupported protocol then EOF → NULL");
155 : }
156 :
157 : // 13. Gmail domain in IMAP type → immediate NULL (rejection)
158 : {
159 1 : const char *input13 = "1\ngmail.com\n";
160 2 : RAII_FILE FILE *stream = fmemopen((void*)input13, strlen(input13), "r");
161 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
162 1 : ASSERT(cfg == NULL, "IMAP wizard: gmail.com domain rejected → NULL");
163 : }
164 :
165 : // 14. Full IMAP config including SMTP host
166 : {
167 1 : const char *input14 =
168 : "1\n" /* account type: IMAP */
169 : "imaps://imap.withsmtp.com\n" /* IMAP host */
170 : "\n" /* port: default */
171 : "user@withsmtp.com\n" /* user */
172 : "password\n" /* pass */
173 : "INBOX\n" /* folder */
174 : "smtp.withsmtp.com\n" /* SMTP host (plain → smtps://) */
175 : "587\n" /* SMTP port */
176 : "\n" /* SMTP user: same as IMAP */
177 : "\n"; /* SMTP pass: same as IMAP */
178 2 : RAII_FILE FILE *stream = fmemopen((void*)input14, strlen(input14), "r");
179 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
180 1 : ASSERT(cfg != NULL, "SMTP wizard: returns config with SMTP");
181 1 : ASSERT(cfg->smtp_host != NULL, "SMTP wizard: smtp_host set");
182 : /* normalize_smtp_host prepends smtps:// to plain hostnames */
183 1 : ASSERT(strncmp(cfg->smtp_host, "smtps://", 8) == 0,
184 : "SMTP wizard: smtp_host has smtps:// prefix");
185 1 : ASSERT(cfg->smtp_port == 587, "SMTP wizard: smtp_port=587");
186 : }
187 :
188 : // 15. SMTP host already has smtps:// → accepted as-is
189 : {
190 1 : const char *input15 =
191 : "1\n"
192 : "imaps://imap.smtps.com\n"
193 : "\n"
194 : "user@smtps.com\n"
195 : "pass\n"
196 : "INBOX\n"
197 : "smtps://smtp.smtps.com\n" /* already has smtps:// */
198 : "\n" /* SMTP port: default */
199 : "\n" /* SMTP user */
200 : "\n"; /* SMTP pass */
201 2 : RAII_FILE FILE *stream = fmemopen((void*)input15, strlen(input15), "r");
202 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
203 1 : ASSERT(cfg != NULL, "SMTP wizard smtps://: returns config");
204 1 : ASSERT(cfg->smtp_host != NULL, "SMTP wizard smtps://: smtp_host set");
205 1 : ASSERT(strcmp(cfg->smtp_host, "smtps://smtp.smtps.com") == 0,
206 : "SMTP wizard smtps://: host preserved as-is");
207 : }
208 :
209 : // 16. SMTP host with unsupported protocol → re-prompt; empty → skipped
210 : {
211 1 : const char *input16 =
212 : "1\n"
213 : "imaps://imap.badsmtp.com\n"
214 : "\n"
215 : "user@badsmtp.com\n"
216 : "pass\n"
217 : "INBOX\n"
218 : "ftp://smtp.badsmtp.com\n" /* bad protocol → error + retry */
219 : "\n" /* empty → skip SMTP */
220 : ;
221 2 : RAII_FILE FILE *stream = fmemopen((void*)input16, strlen(input16), "r");
222 2 : RAII_WITH_CLEANUP(config_cleanup) Config *cfg = setup_wizard_run_internal(stream);
223 1 : ASSERT(cfg != NULL, "SMTP wizard bad-then-skip: returns config");
224 1 : ASSERT(cfg->smtp_host == NULL, "SMTP wizard bad-then-skip: no smtp_host");
225 : }
226 :
227 : // 17. setup_wizard_imap() via stdin pipe
228 : {
229 1 : Config cfg = {0};
230 1 : cfg.host = strdup("imaps://imap.old.com");
231 1 : cfg.user = strdup("old@user.com");
232 1 : cfg.pass = strdup("oldpass");
233 1 : cfg.folder = strdup("INBOX");
234 :
235 : /* Keep all fields: send empty lines for each prompt (keep current) */
236 1 : const char *input17 = "\n\n\n\n";
237 : int pipefd[2];
238 1 : ASSERT(pipe(pipefd) == 0, "pipe for imap sub-wizard");
239 1 : ssize_t wr = write(pipefd[1], input17, strlen(input17));
240 1 : ASSERT(wr > 0, "write to pipe for imap sub-wizard");
241 1 : close(pipefd[1]);
242 :
243 1 : int saved = dup(STDIN_FILENO);
244 1 : dup2(pipefd[0], STDIN_FILENO);
245 1 : close(pipefd[0]);
246 1 : clearerr(stdin); /* clear EOF flag from previous stdin-redirect tests */
247 1 : int rc = setup_wizard_imap(&cfg);
248 1 : dup2(saved, STDIN_FILENO);
249 1 : close(saved);
250 :
251 1 : ASSERT(rc == 0, "setup_wizard_imap keep-all: returns 0");
252 1 : ASSERT(strcmp(cfg.host, "imaps://imap.old.com") == 0, "imap sub-wizard: host unchanged");
253 1 : ASSERT(strcmp(cfg.user, "old@user.com") == 0, "imap sub-wizard: user unchanged");
254 1 : ASSERT(strcmp(cfg.folder, "INBOX") == 0, "imap sub-wizard: folder unchanged");
255 1 : free(cfg.host); free(cfg.user); free(cfg.pass); free(cfg.folder);
256 : }
257 :
258 : // 18. setup_wizard_imap() updates host via stdin pipe
259 : {
260 1 : Config cfg = {0};
261 1 : cfg.host = strdup("imaps://imap.old.com");
262 1 : cfg.user = strdup("old@user.com");
263 1 : cfg.pass = strdup("oldpass");
264 1 : cfg.folder = strdup("INBOX");
265 :
266 : /* Provide a new IMAP host; keep everything else */
267 1 : const char *input18 = "imaps://imap.new.com\n\n\n\n";
268 : int pipefd[2];
269 1 : ASSERT(pipe(pipefd) == 0, "pipe for imap sub-wizard update");
270 1 : ssize_t wr = write(pipefd[1], input18, strlen(input18));
271 1 : ASSERT(wr > 0, "write to pipe for imap sub-wizard update");
272 1 : close(pipefd[1]);
273 :
274 1 : int saved = dup(STDIN_FILENO);
275 1 : dup2(pipefd[0], STDIN_FILENO);
276 1 : close(pipefd[0]);
277 1 : clearerr(stdin);
278 1 : int rc = setup_wizard_imap(&cfg);
279 1 : dup2(saved, STDIN_FILENO);
280 1 : close(saved);
281 :
282 1 : ASSERT(rc == 0, "setup_wizard_imap update: returns 0");
283 1 : ASSERT(strcmp(cfg.host, "imaps://imap.new.com") == 0,
284 : "imap sub-wizard: host updated");
285 1 : free(cfg.host); free(cfg.user); free(cfg.pass); free(cfg.folder);
286 : }
287 :
288 : // 19. setup_wizard_imap() aborts on EOF (host prompt)
289 : {
290 1 : Config cfg = {0};
291 1 : cfg.host = strdup("imaps://imap.eof.com");
292 :
293 : /* EOF immediately → returns -1 */
294 : int pipefd[2];
295 1 : ASSERT(pipe(pipefd) == 0, "pipe for imap sub-wizard EOF");
296 1 : close(pipefd[1]); /* close write end immediately → EOF on read */
297 :
298 1 : int saved = dup(STDIN_FILENO);
299 1 : dup2(pipefd[0], STDIN_FILENO);
300 1 : close(pipefd[0]);
301 1 : clearerr(stdin);
302 1 : int rc = setup_wizard_imap(&cfg);
303 1 : dup2(saved, STDIN_FILENO);
304 1 : close(saved);
305 :
306 1 : ASSERT(rc == -1, "setup_wizard_imap EOF: returns -1");
307 1 : free(cfg.host);
308 : }
309 :
310 : // 20. setup_wizard_smtp() via stdin pipe — keep defaults
311 : {
312 1 : Config cfg = {0};
313 1 : cfg.host = strdup("imaps://imap.smtp20.com");
314 1 : cfg.smtp_host = strdup("smtps://smtp.smtp20.com");
315 1 : cfg.smtp_port = 465;
316 1 : cfg.user = strdup("user@smtp20.com");
317 1 : cfg.pass = strdup("pass");
318 :
319 : /* Empty → keep all current values */
320 1 : const char *input20 = "\n\n\n\n";
321 : int pipefd[2];
322 1 : ASSERT(pipe(pipefd) == 0, "pipe for smtp sub-wizard keep");
323 1 : ssize_t wr = write(pipefd[1], input20, strlen(input20));
324 1 : ASSERT(wr > 0, "write to pipe for smtp sub-wizard keep");
325 1 : close(pipefd[1]);
326 :
327 1 : int saved = dup(STDIN_FILENO);
328 1 : dup2(pipefd[0], STDIN_FILENO);
329 1 : close(pipefd[0]);
330 1 : clearerr(stdin);
331 1 : int rc = setup_wizard_smtp(&cfg);
332 1 : dup2(saved, STDIN_FILENO);
333 1 : close(saved);
334 :
335 1 : ASSERT(rc == 0, "setup_wizard_smtp keep-all: returns 0");
336 1 : ASSERT(strcmp(cfg.smtp_host, "smtps://smtp.smtp20.com") == 0,
337 : "smtp sub-wizard: host unchanged");
338 1 : ASSERT(cfg.smtp_port == 465, "smtp sub-wizard: port unchanged");
339 1 : free(cfg.host); free(cfg.smtp_host); free(cfg.user); free(cfg.pass);
340 : }
341 :
342 : // 21. setup_wizard_smtp() aborts on EOF (host prompt)
343 : {
344 1 : Config cfg = {0};
345 1 : cfg.host = strdup("imaps://imap.smtp21.com");
346 :
347 : int pipefd[2];
348 1 : ASSERT(pipe(pipefd) == 0, "pipe for smtp sub-wizard EOF");
349 1 : close(pipefd[1]); /* close write end immediately → EOF on read */
350 :
351 1 : int saved = dup(STDIN_FILENO);
352 1 : dup2(pipefd[0], STDIN_FILENO);
353 1 : close(pipefd[0]);
354 1 : clearerr(stdin);
355 1 : int rc = setup_wizard_smtp(&cfg);
356 1 : dup2(saved, STDIN_FILENO);
357 1 : close(saved);
358 :
359 1 : ASSERT(rc == -1, "setup_wizard_smtp EOF: returns -1");
360 1 : free(cfg.host);
361 : }
362 :
363 : // 22. setup_wizard_smtp() with derived SMTP URL from imaps:// host
364 : {
365 1 : Config cfg = {0};
366 1 : cfg.host = strdup("imaps://imap.derived.com");
367 : /* No smtp_host → derive_smtp_url produces smtps://imap.derived.com */
368 : /* Empty input accepts the derived default */
369 1 : const char *input22 = "\n\n\n\n";
370 : int pipefd[2];
371 1 : ASSERT(pipe(pipefd) == 0, "pipe for smtp sub-wizard derived");
372 1 : ssize_t wr = write(pipefd[1], input22, strlen(input22));
373 1 : ASSERT(wr > 0, "write to pipe for smtp sub-wizard derived");
374 1 : close(pipefd[1]);
375 :
376 1 : int saved = dup(STDIN_FILENO);
377 1 : dup2(pipefd[0], STDIN_FILENO);
378 1 : close(pipefd[0]);
379 1 : clearerr(stdin);
380 1 : int rc = setup_wizard_smtp(&cfg);
381 1 : dup2(saved, STDIN_FILENO);
382 1 : close(saved);
383 :
384 1 : ASSERT(rc == 0, "setup_wizard_smtp derived: returns 0");
385 : /* Derived URL should have been accepted */
386 1 : ASSERT(cfg.smtp_host != NULL, "setup_wizard_smtp derived: smtp_host set");
387 1 : ASSERT(strncmp(cfg.smtp_host, "smtps://", 8) == 0,
388 : "setup_wizard_smtp derived: has smtps:// prefix");
389 1 : free(cfg.host); free(cfg.smtp_host);
390 : }
391 : }
|