Line data Source code
1 : /**
2 : * @file test_pty_send_local.c
3 : * @brief PTY tests for local-first send behaviour (US-SL-01 to US-SL-04).
4 : *
5 : * US-SL-01: Successful SMTP send → message saved to local Sent folder,
6 : * queued in pending_appends.tsv, TUI shows "Saved locally".
7 : * US-SL-02: Failed SMTP send → message saved to local Drafts folder,
8 : * queued in pending_appends.tsv, TUI shows "Saved to Drafts".
9 : * US-SL-03: email-sync uploads pending messages and clears the queue.
10 : * US-SL-04: pending_appends.tsv survives between runs (persistent queue).
11 : *
12 : * Usage: test-pty-send-local <email-tui> <email-sync>
13 : * <mock-imap-server> <mock-smtp-server>
14 : */
15 :
16 : #define _DEFAULT_SOURCE
17 : #define _XOPEN_SOURCE 600
18 :
19 : #include "ptytest.h"
20 : #include "pty_assert.h"
21 :
22 : #include <dirent.h>
23 : #include <fcntl.h>
24 : #include <netinet/in.h>
25 : #include <signal.h>
26 : #include <stdio.h>
27 : #include <stdlib.h>
28 : #include <string.h>
29 : #include <sys/socket.h>
30 : #include <sys/stat.h>
31 : #include <sys/wait.h>
32 : #include <unistd.h>
33 :
34 : /* ── Test globals ────────────────────────────────────────────────────── */
35 :
36 : int g_tests_run = 0;
37 : int g_tests_failed = 0;
38 :
39 : static char g_tui_bin[512];
40 : static char g_sync_bin[512];
41 : static char g_imap_bin[512];
42 : static char g_smtp_bin[512];
43 :
44 : static char g_test_home[512];
45 : static char g_old_home[512];
46 : static char g_editor_script[600];
47 :
48 : static pid_t g_imap_pid = -1;
49 : static pid_t g_smtp_pid = -1;
50 :
51 : #define WAIT_MS 8000
52 : #define SETTLE_MS 400
53 : #define ROWS 24
54 : #define COLS 120
55 :
56 : #define RUN_TEST(fn) do { printf(" %s...\n", #fn); fflush(stdout); fn(); } while(0)
57 :
58 : /* Account directory under test HOME (username-based path) */
59 : #define ACCOUNT_DIR "/.local/share/email-cli/accounts/testuser@example.com"
60 :
61 : /* ── Fake editor ─────────────────────────────────────────────────────── */
62 :
63 19 : static void write_editor_script(void) {
64 19 : snprintf(g_editor_script, sizeof(g_editor_script),
65 : "%s/.fake_editor.sh", g_test_home);
66 19 : FILE *f = fopen(g_editor_script, "w");
67 19 : if (!f) return;
68 19 : fprintf(f, "#!/bin/sh\necho 'Test body' >> \"$1\"\n");
69 19 : fclose(f);
70 19 : chmod(g_editor_script, 0755);
71 19 : setenv("EDITOR", g_editor_script, 1);
72 : }
73 :
74 : /* ── Directory setup ──────────────────────────────────────────────────── */
75 :
76 24 : static void mkdirs(void) {
77 : char d[700];
78 24 : snprintf(d, sizeof(d), "%s/.config", g_test_home); mkdir(d, 0700);
79 24 : snprintf(d, sizeof(d), "%s/.config/email-cli", g_test_home); mkdir(d, 0700);
80 24 : snprintf(d, sizeof(d), "%s/.config/email-cli/accounts", g_test_home); mkdir(d, 0700);
81 24 : snprintf(d, sizeof(d), "%s/.config/email-cli/accounts/testuser", g_test_home); mkdir(d, 0700);
82 24 : snprintf(d, sizeof(d), "%s/.local", g_test_home); mkdir(d, 0700);
83 24 : snprintf(d, sizeof(d), "%s/.local/share", g_test_home); mkdir(d, 0700);
84 24 : snprintf(d, sizeof(d), "%s/.local/share/email-cli", g_test_home); mkdir(d, 0700);
85 24 : snprintf(d, sizeof(d), "%s/.local/share/email-cli/accounts", g_test_home); mkdir(d, 0700);
86 24 : snprintf(d, sizeof(d), "%s%s", g_test_home, ACCOUNT_DIR); mkdir(d, 0700);
87 24 : snprintf(d, sizeof(d), "%s%s/manifests", g_test_home, ACCOUNT_DIR); mkdir(d, 0700);
88 24 : }
89 :
90 19 : static void write_config(void) {
91 19 : mkdirs();
92 : char path[800];
93 19 : snprintf(path, sizeof(path),
94 : "%s/.config/email-cli/accounts/testuser/config.ini", g_test_home);
95 19 : FILE *fp = fopen(path, "w");
96 19 : if (!fp) return;
97 19 : fprintf(fp,
98 : "EMAIL_HOST=imaps://localhost:9993\n"
99 : "EMAIL_USER=testuser@example.com\n"
100 : "EMAIL_PASS=testpass\n"
101 : "EMAIL_FOLDER=INBOX\n"
102 : "SMTP_HOST=smtps://localhost:9025\n"
103 : "SMTP_PORT=9025\n"
104 : "SMTP_USER=testuser@example.com\n"
105 : "SMTP_PASS=testpass\n"
106 : "SSL_NO_VERIFY=1\n");
107 19 : fclose(fp);
108 19 : chmod(path, 0600);
109 : }
110 :
111 : /* ── Server helpers ───────────────────────────────────────────────────── */
112 :
113 44 : static int probe_port(int port) {
114 44 : int fd = socket(AF_INET, SOCK_STREAM, 0);
115 44 : if (fd < 0) return -1;
116 44 : struct sockaddr_in a = {0};
117 44 : a.sin_family = AF_INET;
118 44 : a.sin_port = htons((uint16_t)port);
119 44 : a.sin_addr.s_addr = htonl(0x7f000001);
120 44 : int r = connect(fd, (struct sockaddr *)&a, sizeof(a));
121 44 : close(fd);
122 44 : return r;
123 : }
124 :
125 44 : static void start_server(const char *bin, pid_t *pid_out) {
126 44 : *pid_out = fork();
127 57 : if (*pid_out < 0) return;
128 57 : if (*pid_out == 0) {
129 13 : int devnull = open("/dev/null", O_WRONLY);
130 13 : if (devnull >= 0) { dup2(devnull, 1); dup2(devnull, 2); close(devnull); }
131 13 : execl(bin, bin, (char *)NULL);
132 13 : _exit(127);
133 : }
134 44 : usleep(800000);
135 : }
136 :
137 50 : static void stop_server(pid_t *pid_out) {
138 50 : if (*pid_out > 0) {
139 37 : kill(*pid_out, SIGKILL);
140 37 : waitpid(*pid_out, NULL, 0);
141 37 : *pid_out = -1;
142 : }
143 50 : }
144 :
145 : /* Kill any process still listening on the given TCP port (cleanup of
146 : * lingering instances from previous test runs). */
147 4 : static void kill_listeners_on_port(int port) {
148 : char cmd[80];
149 4 : snprintf(cmd, sizeof(cmd), "fuser -k -KILL %d/tcp >/dev/null 2>&1", port);
150 4 : int _unused = system(cmd); (void)_unused;
151 4 : usleep(200000);
152 4 : }
153 :
154 24 : static void restart_imap(void) {
155 24 : stop_server(&g_imap_pid);
156 24 : usleep(200000);
157 24 : start_server(g_imap_bin, &g_imap_pid);
158 24 : for (int i = 0; i < 30 && probe_port(9993) != 0; i++) usleep(100000);
159 24 : }
160 :
161 20 : static void restart_smtp(void) {
162 20 : stop_server(&g_smtp_pid);
163 20 : usleep(200000);
164 20 : start_server(g_smtp_bin, &g_smtp_pid);
165 20 : for (int i = 0; i < 30 && probe_port(9025) != 0; i++) usleep(100000);
166 20 : }
167 :
168 : /* ── TUI / sync runners ───────────────────────────────────────────────── */
169 :
170 14 : static PtySession *tui_run(void) {
171 14 : const char *args[4] = { g_tui_bin, NULL };
172 14 : PtySession *s = pty_open(COLS, ROWS);
173 14 : if (!s) return NULL;
174 14 : if (pty_run(s, args) != 0) { pty_close(s); return NULL; }
175 10 : return s;
176 : }
177 :
178 0 : static PtySession *sync_run(const char **extra_args) {
179 : const char *argv[16];
180 0 : int n = 0;
181 0 : argv[n++] = g_sync_bin;
182 0 : if (extra_args)
183 0 : for (int i = 0; extra_args[i] && n < 15; i++)
184 0 : argv[n++] = extra_args[i];
185 0 : argv[n] = NULL;
186 0 : PtySession *s = pty_open(COLS, ROWS);
187 0 : if (!s) return NULL;
188 0 : if (pty_run(s, argv) != 0) { pty_close(s); return NULL; }
189 0 : return s;
190 : }
191 :
192 : /*
193 : * Navigate TUI to the folder browser for the test account.
194 : * Returns a running PtySession ready at the folder browser screen,
195 : * or NULL on failure.
196 : */
197 14 : static PtySession *tui_open_to_folders(void) {
198 14 : write_config();
199 14 : PtySession *s = tui_run();
200 10 : if (!s) return NULL;
201 10 : if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
202 0 : pty_settle(s, SETTLE_MS);
203 0 : pty_send_key(s, PTY_KEY_ENTER);
204 0 : if (pty_wait_for(s, "Folders", WAIT_MS) != 0) { pty_close(s); return NULL; }
205 0 : pty_settle(s, SETTLE_MS);
206 0 : return s;
207 : }
208 :
209 : /*
210 : * Navigate TUI to the INBOX message list.
211 : */
212 14 : static PtySession *tui_open_to_inbox(void) {
213 14 : PtySession *s = tui_open_to_folders();
214 10 : if (!s) return NULL;
215 0 : pty_send_key(s, PTY_KEY_HOME);
216 0 : for (int i = 0; i < 6; i++) { pty_send_key(s, PTY_KEY_DOWN); pty_settle(s, 50); }
217 0 : if (pty_wait_for(s, "INBOX", WAIT_MS) != 0) { pty_close(s); return NULL; }
218 0 : pty_settle(s, SETTLE_MS);
219 0 : pty_send_key(s, PTY_KEY_ENTER);
220 0 : if (pty_wait_for(s, "message(s) in", WAIT_MS) != 0) { pty_close(s); return NULL; }
221 0 : pty_settle(s, SETTLE_MS);
222 0 : return s;
223 : }
224 :
225 : /* ── File-system helpers ──────────────────────────────────────────────── */
226 :
227 : /* Return 1 if the pending_appends.tsv file contains a line starting with folder. */
228 0 : static int pending_has_folder(const char *folder) {
229 : char path[800];
230 0 : snprintf(path, sizeof(path), "%s%s/pending_appends.tsv", g_test_home, ACCOUNT_DIR);
231 0 : FILE *fp = fopen(path, "r");
232 0 : if (!fp) return 0;
233 : char line[512];
234 0 : while (fgets(line, sizeof(line), fp)) {
235 0 : if (strncmp(line, folder, strlen(folder)) == 0 && line[strlen(folder)] == '\t') {
236 0 : fclose(fp);
237 0 : return 1;
238 : }
239 : }
240 0 : fclose(fp);
241 0 : return 0;
242 : }
243 :
244 : /* Return 1 if the pending_appends.tsv file is empty or does not exist. */
245 0 : static int pending_is_empty(void) {
246 : char path[800];
247 0 : snprintf(path, sizeof(path), "%s%s/pending_appends.tsv", g_test_home, ACCOUNT_DIR);
248 0 : FILE *fp = fopen(path, "r");
249 0 : if (!fp) return 1; /* does not exist → empty */
250 0 : int c = fgetc(fp);
251 0 : fclose(fp);
252 0 : return (c == EOF);
253 : }
254 :
255 : /* Delete pending_appends.tsv to reset state between tests. */
256 14 : static void pending_reset(void) {
257 : char path[800];
258 14 : snprintf(path, sizeof(path), "%s%s/pending_appends.tsv", g_test_home, ACCOUNT_DIR);
259 14 : unlink(path);
260 14 : }
261 :
262 : /* ══════════════════════════════════════════════════════════════════════
263 : * TC-SL-01 (US-SL-01): Successful SMTP send → local Sent + pending queue
264 : * ══════════════════════════════════════════════════════════════════════ */
265 :
266 5 : static void test_successful_send_saves_locally(void) {
267 5 : pending_reset();
268 5 : restart_imap(); restart_smtp();
269 5 : write_editor_script();
270 5 : PtySession *s = tui_open_to_inbox();
271 4 : ASSERT(s != NULL, "TC-SL-01: reached inbox");
272 :
273 : /* Press 'c' to open compose dialog */
274 0 : pty_send_str(s, "c");
275 0 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
276 0 : pty_settle(s, SETTLE_MS);
277 :
278 : /* Fill To: field and Tab to Subject */
279 0 : pty_send_str(s, "recipient@example.com");
280 0 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
281 0 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
282 0 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
283 0 : pty_send_str(s, "TC-SL-01 Test Subject");
284 0 : pty_send_key(s, PTY_KEY_ENTER); /* open editor */
285 :
286 : /* Wait for send confirmation prompt, confirm, then check result */
287 0 : ASSERT_WAIT_FOR(s, "Send?", WAIT_MS * 2);
288 0 : pty_send_str(s, "y");
289 0 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
290 0 : ASSERT_WAIT_FOR(s, "Message sent.", WAIT_MS);
291 0 : ASSERT_WAIT_FOR(s, "Saved locally", WAIT_MS);
292 :
293 0 : pty_close(s);
294 :
295 : /* Verify pending_appends.tsv has a Sent entry */
296 0 : ASSERT(pending_has_folder("Sent"), "TC-SL-01: pending_appends.tsv has Sent entry");
297 : }
298 :
299 : /* ══════════════════════════════════════════════════════════════════════
300 : * TC-SL-02 (US-SL-02): Failed SMTP send → local Drafts + pending queue
301 : * ══════════════════════════════════════════════════════════════════════ */
302 :
303 4 : static void test_failed_send_saves_to_drafts(void) {
304 4 : pending_reset();
305 4 : restart_imap();
306 4 : stop_server(&g_smtp_pid); /* kill the server we started */
307 4 : kill_listeners_on_port(9025); /* kill any lingering instance */
308 4 : write_editor_script();
309 4 : PtySession *s = tui_open_to_inbox();
310 3 : ASSERT(s != NULL, "TC-SL-02: reached inbox");
311 :
312 0 : pty_send_str(s, "c");
313 0 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
314 0 : pty_settle(s, SETTLE_MS);
315 :
316 0 : pty_send_str(s, "recipient@example.com");
317 0 : pty_send_key(s, PTY_KEY_TAB);
318 0 : pty_send_key(s, PTY_KEY_TAB);
319 0 : pty_send_key(s, PTY_KEY_TAB);
320 0 : pty_send_str(s, "TC-SL-02 Test Draft");
321 0 : pty_send_key(s, PTY_KEY_ENTER);
322 :
323 : /* Wait for send confirmation, confirm, then check failure path */
324 0 : ASSERT_WAIT_FOR(s, "Send?", WAIT_MS * 2);
325 0 : pty_send_str(s, "y");
326 0 : ASSERT_WAIT_FOR(s, "Sending", WAIT_MS);
327 0 : ASSERT_WAIT_FOR(s, "Saved to Drafts", WAIT_MS);
328 :
329 0 : pty_close(s);
330 :
331 : /* Verify pending_appends.tsv has a Drafts entry */
332 0 : ASSERT(pending_has_folder("Drafts"), "TC-SL-02: pending_appends.tsv has Drafts entry");
333 : }
334 :
335 : /* ══════════════════════════════════════════════════════════════════════
336 : * TC-SL-03 (US-SL-03): email-sync uploads pending messages, clears queue
337 : * ══════════════════════════════════════════════════════════════════════ */
338 :
339 3 : static void test_sync_uploads_pending_and_clears_queue(void) {
340 : /* Seed pending_appends.tsv via a successful send first */
341 3 : pending_reset();
342 3 : restart_imap(); restart_smtp();
343 3 : write_editor_script();
344 3 : PtySession *s = tui_open_to_inbox();
345 2 : ASSERT(s != NULL, "TC-SL-03 setup: reached inbox");
346 :
347 0 : pty_send_str(s, "c");
348 0 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
349 0 : pty_settle(s, SETTLE_MS);
350 0 : pty_send_str(s, "recipient@example.com");
351 0 : pty_send_key(s, PTY_KEY_TAB);
352 0 : pty_send_key(s, PTY_KEY_TAB);
353 0 : pty_send_key(s, PTY_KEY_TAB);
354 0 : pty_send_str(s, "TC-SL-03 Sync Test");
355 0 : pty_send_key(s, PTY_KEY_ENTER);
356 0 : ASSERT_WAIT_FOR(s, "Send?", WAIT_MS * 2);
357 0 : pty_send_str(s, "y");
358 0 : ASSERT_WAIT_FOR(s, "Saved locally", WAIT_MS);
359 0 : pty_close(s);
360 :
361 0 : ASSERT(pending_has_folder("Sent"), "TC-SL-03 setup: Sent entry queued before sync");
362 :
363 : /* Run email-sync to upload the pending message */
364 0 : PtySession *ss = sync_run(NULL);
365 0 : ASSERT(ss != NULL, "TC-SL-03: email-sync started");
366 0 : ASSERT_WAIT_FOR(ss, "uploaded", WAIT_MS);
367 0 : pty_close(ss);
368 :
369 : /* Queue should now be empty */
370 0 : ASSERT(pending_is_empty(), "TC-SL-03: pending_appends.tsv empty after sync");
371 : }
372 :
373 : /* ══════════════════════════════════════════════════════════════════════
374 : * TC-SL-04 (US-SL-04): pending queue survives process restart
375 : * ══════════════════════════════════════════════════════════════════════ */
376 :
377 2 : static void test_pending_queue_survives_restart(void) {
378 : /* Seed via a successful send */
379 2 : pending_reset();
380 2 : restart_imap(); restart_smtp();
381 2 : write_editor_script();
382 2 : PtySession *s = tui_open_to_inbox();
383 1 : ASSERT(s != NULL, "TC-SL-04 setup: reached inbox");
384 :
385 0 : pty_send_str(s, "c");
386 0 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
387 0 : pty_settle(s, SETTLE_MS);
388 0 : pty_send_str(s, "recipient@example.com");
389 0 : pty_send_key(s, PTY_KEY_TAB);
390 0 : pty_send_key(s, PTY_KEY_TAB);
391 0 : pty_send_key(s, PTY_KEY_TAB);
392 0 : pty_send_str(s, "TC-SL-04 Persist Test");
393 0 : pty_send_key(s, PTY_KEY_ENTER);
394 0 : ASSERT_WAIT_FOR(s, "Send?", WAIT_MS * 2);
395 0 : pty_send_str(s, "y");
396 0 : ASSERT_WAIT_FOR(s, "Saved locally", WAIT_MS);
397 0 : pty_close(s);
398 :
399 0 : ASSERT(pending_has_folder("Sent"), "TC-SL-04: Sent entry in queue after send");
400 :
401 : /* Stop all servers; restart — queue must still be there */
402 0 : stop_server(&g_smtp_pid);
403 :
404 0 : ASSERT(pending_has_folder("Sent"), "TC-SL-04: Sent entry persists after process restart");
405 : }
406 :
407 : /* ── main ─────────────────────────────────────────────────────────────── */
408 :
409 5 : int main(int argc, char **argv) {
410 5 : printf("--- Local-first send PTY Tests (US-SL-01 … US-SL-04) ---\n\n");
411 :
412 5 : if (argc < 5) {
413 0 : fprintf(stderr,
414 : "Usage: %s <email-tui> <email-sync> "
415 : "<mock-imap-server> <mock-smtp-server>\n",
416 : argv[0]);
417 0 : return 1;
418 : }
419 :
420 5 : snprintf(g_tui_bin, sizeof(g_tui_bin), "%s", argv[1]);
421 5 : snprintf(g_sync_bin, sizeof(g_sync_bin), "%s", argv[2]);
422 5 : snprintf(g_imap_bin, sizeof(g_imap_bin), "%s", argv[3]);
423 5 : snprintf(g_smtp_bin, sizeof(g_smtp_bin), "%s", argv[4]);
424 :
425 5 : snprintf(g_test_home, sizeof(g_test_home), "/tmp/test-send-local-XXXXXX");
426 5 : if (!mkdtemp(g_test_home)) {
427 0 : perror("mkdtemp");
428 0 : return 1;
429 : }
430 5 : if (getenv("HOME"))
431 5 : snprintf(g_old_home, sizeof(g_old_home), "%s", getenv("HOME"));
432 5 : setenv("HOME", g_test_home, 1);
433 :
434 5 : mkdirs();
435 5 : write_config();
436 5 : write_editor_script();
437 :
438 5 : restart_imap();
439 5 : restart_smtp();
440 :
441 5 : printf("--- US-SL-01: Successful send saves locally ---\n");
442 5 : RUN_TEST(test_successful_send_saves_locally);
443 :
444 4 : printf("\n--- US-SL-02: Failed send saves to Drafts ---\n");
445 4 : RUN_TEST(test_failed_send_saves_to_drafts);
446 :
447 3 : printf("\n--- US-SL-03: Sync uploads pending messages ---\n");
448 3 : restart_imap(); restart_smtp();
449 3 : RUN_TEST(test_sync_uploads_pending_and_clears_queue);
450 :
451 2 : printf("\n--- US-SL-04: Pending queue survives restart ---\n");
452 2 : restart_imap(); restart_smtp();
453 2 : RUN_TEST(test_pending_queue_survives_restart);
454 :
455 1 : stop_server(&g_imap_pid);
456 1 : stop_server(&g_smtp_pid);
457 :
458 1 : if (g_old_home[0]) setenv("HOME", g_old_home, 1);
459 :
460 1 : printf("\n--- Test Results ---\n");
461 1 : printf("Tests Run: %d\n", g_tests_run);
462 1 : printf("Tests Passed: %d\n", g_tests_run - g_tests_failed);
463 1 : printf("Tests Failed: %d\n", g_tests_failed);
464 :
465 1 : return g_tests_failed > 0 ? 1 : 0;
466 : }
|