Line data Source code
1 : /**
2 : * @file test_pty_attachment.c
3 : * @brief PTY tests for file attachment support in compose (US-84)
4 : * and the post-compose review screen (US-85).
5 : *
6 : * These are SPECIFICATION tests written before the feature exists (TDD).
7 : * They will fail until US-84 and US-85 are implemented.
8 : *
9 : * Usage: test-pty-attachment <email-tui> <mock-imap-server> <mock-smtp-server>
10 : *
11 : * Test groups:
12 : * TC-AT-01 … TC-AT-11 Attach field in pre-compose dialog (US-84)
13 : * TC-PCR-01 … TC-PCR-16 Post-compose review screen (US-85)
14 : */
15 :
16 : #define _DEFAULT_SOURCE
17 : #define _XOPEN_SOURCE 600
18 :
19 : #include "ptytest.h"
20 : #include "pty_assert.h"
21 :
22 : #include <fcntl.h>
23 : #include <netinet/in.h>
24 : #include <signal.h>
25 : #include <stdio.h>
26 : #include <stdlib.h>
27 : #include <string.h>
28 : #include <sys/socket.h>
29 : #include <sys/stat.h>
30 : #include <sys/wait.h>
31 : #include <unistd.h>
32 :
33 : /* ── Test globals ───────────────────────────────────────────────────── */
34 :
35 : int g_tests_run = 0;
36 : int g_tests_failed = 0;
37 :
38 : static char g_tui_bin[512];
39 : static char g_imap_bin[512];
40 : static char g_smtp_bin[512];
41 :
42 : static char g_test_home[512];
43 : static char g_old_home[512];
44 : static char g_editor_script[600];
45 :
46 : static char g_attach_file1[600]; /* report.pdf (stub) */
47 : static char g_attach_file2[600]; /* notes.txt */
48 :
49 : static pid_t g_imap_pid = -1;
50 : static pid_t g_smtp_pid = -1;
51 :
52 : #define WAIT_MS 6000
53 : #define SETTLE_MS 400
54 : #define ROWS 24
55 : #define COLS 120
56 :
57 : #define SHIFT_TAB "\x1b[Z" /* Shift-Tab escape sequence */
58 :
59 : #define RUN_TEST(fn) \
60 : do { printf(" %s...\n", #fn); fflush(stdout); fn(); } while (0)
61 :
62 : /* ── Fake editor ────────────────────────────────────────────────────── */
63 :
64 28 : static void write_editor_script(void)
65 : {
66 28 : snprintf(g_editor_script, sizeof(g_editor_script),
67 : "%s/.fake_editor.sh", g_test_home);
68 28 : FILE *f = fopen(g_editor_script, "w");
69 28 : if (!f) return;
70 28 : fprintf(f, "#!/bin/sh\necho 'Test body' >> \"$1\"\n");
71 28 : fclose(f);
72 28 : chmod(g_editor_script, 0755);
73 28 : setenv("EDITOR", g_editor_script, 1);
74 : }
75 :
76 : /* ── Config / directory helpers ─────────────────────────────────────── */
77 :
78 28 : static void mkdirs(void)
79 : {
80 : char d[600];
81 : #define MK(path) snprintf(d, sizeof(d), "%s/" path, g_test_home); mkdir(d, 0700)
82 28 : MK(".config");
83 28 : MK(".config/email-cli");
84 28 : MK(".config/email-cli/accounts");
85 28 : MK(".config/email-cli/accounts/testuser");
86 28 : MK(".local");
87 28 : MK(".local/share");
88 28 : MK(".local/share/email-cli");
89 28 : MK(".local/share/email-cli/accounts");
90 28 : MK(".local/share/email-cli/accounts/testuser");
91 28 : MK(".local/share/email-cli/accounts/testuser/manifests");
92 28 : MK("attachments");
93 : #undef MK
94 28 : }
95 :
96 28 : static void write_config(void)
97 : {
98 28 : mkdirs();
99 : char path[700];
100 28 : snprintf(path, sizeof(path),
101 : "%s/.config/email-cli/accounts/testuser/config.ini", g_test_home);
102 28 : FILE *fp = fopen(path, "w");
103 28 : if (!fp) return;
104 28 : fprintf(fp,
105 : "EMAIL_HOST=imaps://localhost:9993\n"
106 : "EMAIL_USER=testuser@example.com\n"
107 : "EMAIL_PASS=testpass\n"
108 : "EMAIL_FOLDER=INBOX\n"
109 : "SMTP_HOST=smtps://localhost:9025\n"
110 : "SMTP_PORT=9025\n"
111 : "SMTP_USER=testuser@example.com\n"
112 : "SMTP_PASS=testpass\n"
113 : "SSL_NO_VERIFY=1\n");
114 28 : fclose(fp);
115 28 : chmod(path, 0600);
116 : }
117 :
118 28 : static void write_attach_files(void)
119 : {
120 28 : snprintf(g_attach_file1, sizeof(g_attach_file1),
121 : "%s/attachments/report.pdf", g_test_home);
122 28 : snprintf(g_attach_file2, sizeof(g_attach_file2),
123 : "%s/attachments/notes.txt", g_test_home);
124 :
125 : FILE *f;
126 28 : f = fopen(g_attach_file1, "w");
127 28 : if (f) { fprintf(f, "%%PDF-1.4 stub\n"); fclose(f); }
128 :
129 28 : f = fopen(g_attach_file2, "w");
130 28 : if (f) { fprintf(f, "Meeting notes:\n- Item 1\n"); fclose(f); }
131 28 : }
132 :
133 28 : static void write_contacts_tsv(void)
134 : {
135 : char path[700];
136 28 : snprintf(path, sizeof(path),
137 : "%s/.local/share/email-cli/accounts/testuser/contacts.tsv",
138 : g_test_home);
139 28 : FILE *f = fopen(path, "w");
140 28 : if (!f) return;
141 28 : fprintf(f, "alice@example.com\tAlice Example\t10\n");
142 28 : fprintf(f, "bob.smith@acme.com\tBob Smith\t8\n");
143 28 : fclose(f);
144 : }
145 :
146 : /* ── Mock server management ─────────────────────────────────────────── */
147 :
148 810 : static void start_server(const char *bin, pid_t *pid)
149 : {
150 810 : *pid = fork();
151 864 : if (*pid == 0) { execl(bin, bin, NULL); _exit(1); }
152 810 : }
153 :
154 812 : static void stop_server(pid_t *pid)
155 : {
156 812 : if (*pid > 0) { kill(*pid, SIGTERM); waitpid(*pid, NULL, 0); *pid = -1; }
157 812 : }
158 :
159 1215 : static int probe_port(int port)
160 : {
161 1215 : int fd = socket(AF_INET, SOCK_STREAM, 0);
162 1215 : if (fd < 0) return -1;
163 1215 : struct sockaddr_in sa = {
164 : .sin_family = AF_INET,
165 1215 : .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
166 1215 : .sin_port = htons((uint16_t)port),
167 : };
168 1215 : int rc = connect(fd, (struct sockaddr *)&sa, sizeof(sa));
169 1215 : close(fd);
170 1215 : return rc;
171 : }
172 :
173 405 : static void restart_servers(void)
174 : {
175 405 : stop_server(&g_imap_pid);
176 405 : stop_server(&g_smtp_pid);
177 405 : start_server(g_imap_bin, &g_imap_pid);
178 405 : start_server(g_smtp_bin, &g_smtp_pid);
179 405 : for (int i = 0; i < 30 && probe_port(9993) != 0; i++) usleep(100000);
180 810 : for (int i = 0; i < 30 && probe_port(9025) != 0; i++) usleep(100000);
181 405 : }
182 :
183 : /* ── TUI session helpers ─────────────────────────────────────────────── */
184 :
185 405 : static PtySession *tui_run(void)
186 : {
187 405 : const char *args[] = { g_tui_bin, NULL };
188 405 : PtySession *s = pty_open(COLS, ROWS);
189 405 : if (!s) return NULL;
190 405 : if (pty_run(s, args) != 0) { pty_close(s); return NULL; }
191 378 : return s;
192 : }
193 :
194 405 : static PtySession *tui_open_to_inbox(void)
195 : {
196 405 : PtySession *s = tui_run();
197 378 : if (!s) return NULL;
198 378 : if (pty_wait_for(s, "testuser", WAIT_MS) != 0) { pty_close(s); return NULL; }
199 378 : pty_send_key(s, PTY_KEY_ENTER);
200 378 : if (pty_wait_for(s, "INBOX", WAIT_MS) != 0) { pty_close(s); return NULL; }
201 378 : pty_send_key(s, PTY_KEY_HOME);
202 2646 : for (int i = 0; i < 6; i++) { pty_send_key(s, PTY_KEY_DOWN); pty_settle(s, 50); }
203 378 : pty_send_key(s, PTY_KEY_ENTER);
204 378 : if (pty_wait_for(s, "message(s)", WAIT_MS) != 0) { pty_close(s); return NULL; }
205 378 : return s;
206 : }
207 :
208 : /* Navigate compose dialog to Attach field: Tab×4 from To */
209 215 : static void nav_to_attach_field(PtySession *s)
210 : {
211 215 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
212 215 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
213 215 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
214 215 : pty_send_key(s, PTY_KEY_TAB); /* Subject → Attach */
215 215 : pty_settle(s, 100);
216 215 : }
217 :
218 : /* ══════════════════════════════════════════════════════════════════════
219 : * TC-AT — Attach field in pre-compose dialog (US-84)
220 : * ══════════════════════════════════════════════════════════════════════ */
221 :
222 : /* TC-AT-01: Compose dialog shows an "Attach:" field */
223 28 : static void test_attach_field_visible(void)
224 : {
225 28 : restart_servers();
226 28 : PtySession *s = tui_open_to_inbox();
227 27 : ASSERT(s != NULL, "TC-AT-01: inbox opens");
228 27 : pty_send_str(s, "c");
229 27 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
230 27 : ASSERT_SCREEN_CONTAINS(s, "Attach:");
231 27 : pty_send_key(s, PTY_KEY_ESC);
232 27 : pty_close(s);
233 : }
234 :
235 : /* TC-AT-02: Tab from Subject moves focus to Attach field */
236 27 : static void test_attach_field_tab_from_subject(void)
237 : {
238 27 : restart_servers();
239 27 : PtySession *s = tui_open_to_inbox();
240 26 : ASSERT(s != NULL, "TC-AT-02: inbox opens");
241 26 : pty_send_str(s, "c");
242 26 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
243 26 : nav_to_attach_field(s);
244 26 : pty_send_str(s, "/tmp");
245 26 : pty_settle(s, 200);
246 26 : ASSERT_SCREEN_CONTAINS(s, "/tmp");
247 26 : pty_send_key(s, PTY_KEY_ESC);
248 26 : pty_close(s);
249 : }
250 :
251 : /* TC-AT-03: Typing partial path shows file-browser dropdown */
252 26 : static void test_attach_dropdown_on_partial_path(void)
253 : {
254 26 : restart_servers();
255 26 : PtySession *s = tui_open_to_inbox();
256 25 : ASSERT(s != NULL, "TC-AT-03: inbox opens");
257 25 : pty_send_str(s, "c");
258 25 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
259 25 : nav_to_attach_field(s);
260 : char prefix[600];
261 25 : snprintf(prefix, sizeof(prefix), "%s/attachments/", g_test_home);
262 25 : pty_send_str(s, prefix);
263 25 : pty_settle(s, 400);
264 25 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
265 25 : ASSERT_SCREEN_CONTAINS(s, "notes.txt");
266 25 : pty_send_key(s, PTY_KEY_ESC);
267 25 : pty_close(s);
268 : }
269 :
270 : /* TC-AT-04: Tab in Attach field completes the first match */
271 25 : static void test_attach_tab_completion_cycles(void)
272 : {
273 25 : restart_servers();
274 25 : PtySession *s = tui_open_to_inbox();
275 24 : ASSERT(s != NULL, "TC-AT-04: inbox opens");
276 24 : pty_send_str(s, "c");
277 24 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
278 24 : nav_to_attach_field(s);
279 : char prefix[600];
280 24 : snprintf(prefix, sizeof(prefix), "%s/attachments/rep", g_test_home);
281 24 : pty_send_str(s, prefix);
282 24 : pty_settle(s, 200);
283 24 : pty_send_key(s, PTY_KEY_TAB);
284 24 : pty_settle(s, 200);
285 24 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
286 24 : pty_send_key(s, PTY_KEY_ESC);
287 24 : pty_close(s);
288 : }
289 :
290 : /* TC-AT-05: Enter with complete file path adds file to attachment list */
291 24 : static void test_attach_enter_adds_file(void)
292 : {
293 24 : restart_servers();
294 24 : PtySession *s = tui_open_to_inbox();
295 23 : ASSERT(s != NULL, "TC-AT-05: inbox opens");
296 23 : pty_send_str(s, "c");
297 23 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
298 23 : nav_to_attach_field(s);
299 23 : pty_send_str(s, g_attach_file1);
300 23 : pty_send_key(s, PTY_KEY_ENTER);
301 23 : pty_settle(s, 300);
302 23 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
303 23 : pty_send_key(s, PTY_KEY_ESC);
304 23 : pty_close(s);
305 : }
306 :
307 : /* TC-AT-06: Non-existent path shows inline error "File not found" */
308 23 : static void test_attach_nonexistent_shows_error(void)
309 : {
310 23 : restart_servers();
311 23 : PtySession *s = tui_open_to_inbox();
312 22 : ASSERT(s != NULL, "TC-AT-06: inbox opens");
313 22 : pty_send_str(s, "c");
314 22 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
315 22 : nav_to_attach_field(s);
316 22 : pty_send_str(s, "/tmp/does-not-exist-xyz-404.pdf");
317 22 : pty_send_key(s, PTY_KEY_ENTER);
318 22 : pty_settle(s, 300);
319 22 : ASSERT_SCREEN_CONTAINS(s, "File not found");
320 22 : pty_send_key(s, PTY_KEY_ESC);
321 22 : pty_close(s);
322 : }
323 :
324 : /* TC-AT-07: Attaching a directory path shows inline error "Not a file" */
325 22 : static void test_attach_directory_shows_error(void)
326 : {
327 22 : restart_servers();
328 22 : PtySession *s = tui_open_to_inbox();
329 21 : ASSERT(s != NULL, "TC-AT-07: inbox opens");
330 21 : pty_send_str(s, "c");
331 21 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
332 21 : nav_to_attach_field(s);
333 : char dir[600];
334 21 : snprintf(dir, sizeof(dir), "%s/attachments/", g_test_home);
335 21 : pty_send_str(s, dir);
336 21 : pty_send_key(s, PTY_KEY_ENTER);
337 21 : pty_settle(s, 300);
338 21 : ASSERT_SCREEN_CONTAINS(s, "Not a file");
339 21 : pty_send_key(s, PTY_KEY_ESC);
340 21 : pty_close(s);
341 : }
342 :
343 : /* TC-AT-08: [d] while Attach field focused removes last attachment */
344 21 : static void test_attach_d_removes_last(void)
345 : {
346 21 : restart_servers();
347 21 : PtySession *s = tui_open_to_inbox();
348 20 : ASSERT(s != NULL, "TC-AT-08: inbox opens");
349 20 : pty_send_str(s, "c");
350 20 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
351 20 : nav_to_attach_field(s);
352 20 : pty_send_str(s, g_attach_file1);
353 20 : pty_send_key(s, PTY_KEY_ENTER);
354 20 : pty_settle(s, 200);
355 20 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
356 20 : pty_send_str(s, "d");
357 20 : pty_settle(s, 200);
358 20 : ASSERT_SCREEN_NOT_CONTAINS(s, "report.pdf");
359 20 : pty_send_key(s, PTY_KEY_ESC);
360 20 : pty_close(s);
361 : }
362 :
363 : /* TC-AT-09: Multiple attachments can be added sequentially */
364 20 : static void test_attach_multiple_files(void)
365 : {
366 20 : restart_servers();
367 20 : PtySession *s = tui_open_to_inbox();
368 19 : ASSERT(s != NULL, "TC-AT-09: inbox opens");
369 19 : pty_send_str(s, "c");
370 19 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
371 19 : nav_to_attach_field(s);
372 19 : pty_send_str(s, g_attach_file1);
373 19 : pty_send_key(s, PTY_KEY_ENTER);
374 19 : pty_settle(s, 200);
375 19 : pty_send_str(s, g_attach_file2);
376 19 : pty_send_key(s, PTY_KEY_ENTER);
377 19 : pty_settle(s, 200);
378 19 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
379 19 : ASSERT_SCREEN_CONTAINS(s, "notes.txt");
380 19 : pty_send_key(s, PTY_KEY_ESC);
381 19 : pty_close(s);
382 : }
383 :
384 : /* TC-AT-10: Shift-Tab from Attach field moves back to Subject */
385 19 : static void test_attach_shift_tab_back_to_subject(void)
386 : {
387 19 : restart_servers();
388 19 : PtySession *s = tui_open_to_inbox();
389 18 : ASSERT(s != NULL, "TC-AT-10: inbox opens");
390 18 : pty_send_str(s, "c");
391 18 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
392 18 : nav_to_attach_field(s);
393 18 : pty_send_str(s, SHIFT_TAB);
394 18 : pty_settle(s, 100);
395 18 : pty_send_str(s, "ShiftTabTest");
396 18 : pty_settle(s, 100);
397 18 : ASSERT_SCREEN_CONTAINS(s, "ShiftTabTest");
398 18 : pty_send_key(s, PTY_KEY_ESC);
399 18 : pty_close(s);
400 : }
401 :
402 : /* TC-AT-11: Confirming dialog with attachment shows it in the review screen */
403 18 : static void test_attach_shown_in_review(void)
404 : {
405 18 : restart_servers();
406 18 : PtySession *s = tui_open_to_inbox();
407 17 : ASSERT(s != NULL, "TC-AT-11: inbox opens");
408 17 : pty_send_str(s, "c");
409 17 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
410 17 : pty_send_str(s, "alice@example.com");
411 17 : pty_send_key(s, PTY_KEY_TAB);
412 17 : pty_send_key(s, PTY_KEY_TAB);
413 17 : pty_send_key(s, PTY_KEY_TAB);
414 17 : pty_send_str(s, "Test with attachment");
415 17 : nav_to_attach_field(s);
416 17 : pty_send_str(s, g_attach_file1);
417 17 : pty_send_key(s, PTY_KEY_ENTER);
418 17 : pty_settle(s, 200);
419 : /* Confirm: opens editor (fake), then review screen */
420 17 : pty_send_key(s, PTY_KEY_ENTER);
421 17 : ASSERT_WAIT_FOR(s, "Review", WAIT_MS);
422 17 : ASSERT_SCREEN_CONTAINS(s, "report.pdf");
423 17 : pty_send_str(s, "q");
424 17 : pty_send_str(s, "y");
425 17 : pty_close(s);
426 : }
427 :
428 : /* ══════════════════════════════════════════════════════════════════════
429 : * TC-PCR — Post-compose review screen (US-85)
430 : * ══════════════════════════════════════════════════════════════════════ */
431 :
432 : /*
433 : * Helper: compose, fill To+Subject, confirm dialog → editor (fake) → review.
434 : * Returns open session on review screen, or NULL on failure.
435 : */
436 147 : static PtySession *compose_to_review(const char *to, const char *subject)
437 : {
438 147 : PtySession *s = tui_open_to_inbox();
439 133 : if (!s) return NULL;
440 133 : pty_send_str(s, "c");
441 133 : if (pty_wait_for(s, "New Message", WAIT_MS) != 0) { pty_close(s); return NULL; }
442 133 : pty_send_str(s, to);
443 133 : pty_send_key(s, PTY_KEY_TAB); /* To → Cc */
444 133 : pty_send_key(s, PTY_KEY_TAB); /* Cc → Bcc */
445 133 : pty_send_key(s, PTY_KEY_TAB); /* Bcc → Subject */
446 133 : pty_send_str(s, subject);
447 133 : pty_send_key(s, PTY_KEY_ENTER); /* confirm → editor → review */
448 : /* Wait for the footer, which pcr_render() prints last — waiting for the
449 : * title alone can return while the body/attachment lines are still being
450 : * written. */
451 133 : if (pty_wait_for(s, "[s] Send", WAIT_MS) != 0) { pty_close(s); return NULL; }
452 133 : return s;
453 : }
454 :
455 : /* TC-PCR-01: Review screen appears after editor exits (no plain "Send? [y/n]") */
456 17 : static void test_review_screen_appears(void)
457 : {
458 17 : restart_servers();
459 17 : PtySession *s = compose_to_review("alice@example.com", "Review screen test");
460 16 : ASSERT(s != NULL, "TC-PCR-01: review screen reached");
461 16 : ASSERT_SCREEN_CONTAINS(s, "Review");
462 16 : ASSERT_SCREEN_NOT_CONTAINS(s, "Send? [y/n]");
463 16 : pty_send_str(s, "q");
464 16 : pty_send_str(s, "y");
465 16 : pty_close(s);
466 : }
467 :
468 : /* TC-PCR-02: Review screen shows From address */
469 16 : static void test_review_shows_from(void)
470 : {
471 16 : restart_servers();
472 16 : PtySession *s = compose_to_review("alice@example.com", "From header test");
473 15 : ASSERT(s != NULL, "TC-PCR-02: review screen reached");
474 15 : ASSERT_SCREEN_CONTAINS(s, "testuser@example.com");
475 15 : pty_send_str(s, "q");
476 15 : pty_send_str(s, "y");
477 15 : pty_close(s);
478 : }
479 :
480 : /* TC-PCR-03: Review screen shows To address */
481 15 : static void test_review_shows_to(void)
482 : {
483 15 : restart_servers();
484 15 : PtySession *s = compose_to_review("alice@example.com", "To header test");
485 14 : ASSERT(s != NULL, "TC-PCR-03: review screen reached");
486 14 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
487 14 : pty_send_str(s, "q");
488 14 : pty_send_str(s, "y");
489 14 : pty_close(s);
490 : }
491 :
492 : /* TC-PCR-04: Review screen shows Subject */
493 14 : static void test_review_shows_subject(void)
494 : {
495 14 : restart_servers();
496 14 : PtySession *s = compose_to_review("alice@example.com", "My unique subject");
497 13 : ASSERT(s != NULL, "TC-PCR-04: review screen reached");
498 13 : ASSERT_SCREEN_CONTAINS(s, "My unique subject");
499 13 : pty_send_str(s, "q");
500 13 : pty_send_str(s, "y");
501 13 : pty_close(s);
502 : }
503 :
504 : /* TC-PCR-05: Review screen shows body line count */
505 13 : static void test_review_shows_body_lines(void)
506 : {
507 13 : restart_servers();
508 13 : PtySession *s = compose_to_review("alice@example.com", "Body count test");
509 12 : ASSERT(s != NULL, "TC-PCR-05: review screen reached");
510 : /* Fake editor appended one line → "1 line" or "1 lines" */
511 12 : ASSERT_SCREEN_CONTAINS(s, "line");
512 12 : pty_send_str(s, "q");
513 12 : pty_send_str(s, "y");
514 12 : pty_close(s);
515 : }
516 :
517 : /* TC-PCR-06: Review screen shows "(none)" when no attachments */
518 12 : static void test_review_no_attachments_shown(void)
519 : {
520 12 : restart_servers();
521 12 : PtySession *s = compose_to_review("alice@example.com", "No attach test");
522 11 : ASSERT(s != NULL, "TC-PCR-06: review screen reached");
523 11 : ASSERT_SCREEN_CONTAINS(s, "(none)");
524 11 : pty_send_str(s, "q");
525 11 : pty_send_str(s, "y");
526 11 : pty_close(s);
527 : }
528 :
529 : /* TC-PCR-07: [s] sends the message and shows confirmation */
530 11 : static void test_review_s_sends_message(void)
531 : {
532 11 : restart_servers();
533 11 : PtySession *s = compose_to_review("alice@example.com", "Send via review");
534 10 : ASSERT(s != NULL, "TC-PCR-07: review screen reached");
535 10 : pty_send_str(s, "s");
536 10 : ASSERT_WAIT_FOR(s, "sent", WAIT_MS);
537 10 : pty_close(s);
538 : }
539 :
540 : /* TC-PCR-08: [e] reopens header dialog pre-filled with current values */
541 10 : static void test_review_e_reopens_header_dialog(void)
542 : {
543 10 : restart_servers();
544 10 : PtySession *s = compose_to_review("alice@example.com", "Edit headers test");
545 9 : ASSERT(s != NULL, "TC-PCR-08: review screen reached");
546 9 : pty_send_str(s, "e");
547 9 : ASSERT_WAIT_FOR(s, "Message", WAIT_MS);
548 9 : ASSERT_SCREEN_CONTAINS(s, "alice@example.com");
549 9 : ASSERT_SCREEN_CONTAINS(s, "Edit headers test");
550 9 : pty_send_key(s, PTY_KEY_ESC);
551 9 : pty_close(s);
552 : }
553 :
554 : /* TC-PCR-09: [b] reopens editor; review reappears with updated line count */
555 9 : static void test_review_b_reopens_editor(void)
556 : {
557 9 : restart_servers();
558 9 : PtySession *s = compose_to_review("alice@example.com", "Edit body test");
559 8 : ASSERT(s != NULL, "TC-PCR-09: review screen reached");
560 8 : pty_send_str(s, "b");
561 : /* Fake editor appends another "Test body" line → 2 lines total.
562 : * Wait for the new count, not for "Review" — that string is still on
563 : * screen from the previous render and would match immediately. */
564 8 : ASSERT_WAIT_FOR(s, "2 lines", WAIT_MS);
565 8 : pty_send_str(s, "q");
566 8 : pty_send_str(s, "y");
567 8 : pty_close(s);
568 : }
569 :
570 : /* TC-PCR-10: [a] opens attachment picker; picked file appears in review */
571 8 : static void test_review_a_opens_attach_picker(void)
572 : {
573 8 : restart_servers();
574 8 : PtySession *s = compose_to_review("alice@example.com", "Attach from review");
575 7 : ASSERT(s != NULL, "TC-PCR-10: review screen reached");
576 7 : pty_send_str(s, "a");
577 7 : pty_settle(s, 200);
578 : /* Picker overlay visible */
579 7 : ASSERT_SCREEN_CONTAINS(s, "Attach");
580 7 : pty_send_str(s, g_attach_file2);
581 7 : pty_send_key(s, PTY_KEY_ENTER);
582 7 : ASSERT_WAIT_FOR(s, "notes.txt", WAIT_MS);
583 7 : pty_send_str(s, "q");
584 7 : pty_send_str(s, "y");
585 7 : pty_close(s);
586 : }
587 :
588 : /* TC-PCR-11: [d] removes last attachment; "(none)" shown when list empty */
589 7 : static void test_review_d_removes_attachment(void)
590 : {
591 7 : restart_servers();
592 7 : PtySession *s = compose_to_review("alice@example.com", "Remove attach test");
593 6 : ASSERT(s != NULL, "TC-PCR-11: review reached");
594 : /* Add via [a] */
595 6 : pty_send_str(s, "a");
596 6 : pty_settle(s, 200);
597 6 : pty_send_str(s, g_attach_file1);
598 6 : pty_send_key(s, PTY_KEY_ENTER);
599 6 : ASSERT_WAIT_FOR(s, "report.pdf", WAIT_MS);
600 : /* Remove via [d] */
601 6 : pty_send_str(s, "d");
602 6 : pty_settle(s, 200);
603 6 : ASSERT_SCREEN_NOT_CONTAINS(s, "report.pdf");
604 6 : ASSERT_SCREEN_CONTAINS(s, "(none)");
605 6 : pty_send_str(s, "q");
606 6 : pty_send_str(s, "y");
607 6 : pty_close(s);
608 : }
609 :
610 : /* TC-PCR-12: [q] asks discard confirmation; [n] returns to review */
611 6 : static void test_review_q_asks_discard_confirm(void)
612 : {
613 6 : restart_servers();
614 6 : PtySession *s = compose_to_review("alice@example.com", "Cancel test");
615 5 : ASSERT(s != NULL, "TC-PCR-12: review reached");
616 5 : pty_send_str(s, "q");
617 5 : pty_settle(s, 200);
618 5 : ASSERT_SCREEN_CONTAINS(s, "Discard");
619 5 : pty_send_str(s, "n");
620 5 : pty_settle(s, 200);
621 5 : ASSERT_SCREEN_CONTAINS(s, "Review");
622 5 : pty_send_str(s, "q");
623 5 : pty_send_str(s, "y");
624 5 : pty_close(s);
625 : }
626 :
627 : /* TC-PCR-13: [q]+[y] discards and returns to inbox */
628 5 : static void test_review_q_y_cancels(void)
629 : {
630 5 : restart_servers();
631 5 : PtySession *s = compose_to_review("alice@example.com", "Discard confirm");
632 4 : ASSERT(s != NULL, "TC-PCR-13: review reached");
633 4 : pty_send_str(s, "q");
634 4 : ASSERT_WAIT_FOR(s, "Discard", WAIT_MS);
635 4 : pty_send_str(s, "y");
636 4 : ASSERT_WAIT_FOR(s, "[Press any key to return to inbox]", WAIT_MS);
637 4 : pty_send_str(s, " ");
638 4 : ASSERT_WAIT_FOR(s, "message(s)", WAIT_MS);
639 4 : pty_close(s);
640 : }
641 :
642 : /* TC-PCR-14: Esc triggers discard confirmation like [q] */
643 4 : static void test_review_esc_asks_discard_confirm(void)
644 : {
645 4 : restart_servers();
646 4 : PtySession *s = compose_to_review("alice@example.com", "Esc cancel test");
647 3 : ASSERT(s != NULL, "TC-PCR-14: review reached");
648 3 : pty_send_key(s, PTY_KEY_ESC);
649 3 : pty_settle(s, 200);
650 3 : ASSERT_SCREEN_CONTAINS(s, "Discard");
651 3 : pty_send_str(s, "y");
652 3 : pty_close(s);
653 : }
654 :
655 : /* TC-PCR-15: Empty To in draft disables [s] and shows error */
656 3 : static void test_review_empty_to_disables_send(void)
657 : {
658 : /* Use a custom editor that strips the To: line from the draft */
659 3 : restart_servers();
660 : char strip_editor[700];
661 3 : snprintf(strip_editor, sizeof(strip_editor),
662 : "%s/.strip_to_editor.sh", g_test_home);
663 3 : FILE *ef = fopen(strip_editor, "w");
664 3 : if (ef) {
665 3 : fprintf(ef, "#!/bin/sh\n"
666 : "grep -v '^To:' \"$1\" > \"$1.tmp\"\n"
667 : "mv \"$1.tmp\" \"$1\"\n"
668 : "echo 'body' >> \"$1\"\n");
669 3 : fclose(ef);
670 3 : chmod(strip_editor, 0755);
671 : }
672 3 : setenv("EDITOR", strip_editor, 1);
673 :
674 3 : PtySession *s = tui_open_to_inbox();
675 2 : ASSERT(s != NULL, "TC-PCR-15: inbox opens");
676 2 : pty_send_str(s, "c");
677 2 : ASSERT_WAIT_FOR(s, "New Message", WAIT_MS);
678 2 : pty_send_str(s, "alice@example.com");
679 2 : pty_send_key(s, PTY_KEY_TAB);
680 2 : pty_send_key(s, PTY_KEY_TAB);
681 2 : pty_send_key(s, PTY_KEY_TAB);
682 2 : pty_send_str(s, "No-To test");
683 2 : pty_send_key(s, PTY_KEY_ENTER);
684 2 : ASSERT_WAIT_FOR(s, "Review", WAIT_MS);
685 2 : ASSERT_SCREEN_CONTAINS(s, "To:");
686 : /* [s] should not send; error about empty To expected */
687 2 : pty_send_str(s, "s");
688 2 : pty_settle(s, 300);
689 2 : ASSERT_SCREEN_CONTAINS(s, "Review");
690 2 : pty_send_str(s, "q");
691 2 : pty_send_str(s, "y");
692 :
693 2 : setenv("EDITOR", g_editor_script, 1);
694 2 : pty_close(s);
695 : }
696 :
697 : /* TC-PCR-16: Reply flow also uses the review screen (not plain y/n) */
698 2 : static void test_review_reply_uses_review_screen(void)
699 : {
700 2 : restart_servers();
701 2 : PtySession *s = tui_open_to_inbox();
702 1 : ASSERT(s != NULL, "TC-PCR-16: inbox opens");
703 1 : pty_send_key(s, PTY_KEY_HOME);
704 1 : pty_send_key(s, PTY_KEY_DOWN);
705 1 : pty_settle(s, 100);
706 1 : pty_send_str(s, "r");
707 1 : ASSERT_WAIT_FOR(s, "Reply", WAIT_MS);
708 1 : pty_send_key(s, PTY_KEY_ENTER);
709 1 : ASSERT_WAIT_FOR(s, "Review", WAIT_MS);
710 1 : ASSERT_SCREEN_CONTAINS(s, "Review");
711 1 : ASSERT_SCREEN_NOT_CONTAINS(s, "Send? [y/n]");
712 1 : pty_send_str(s, "q");
713 1 : pty_send_str(s, "y");
714 1 : pty_close(s);
715 : }
716 :
717 : /* ══════════════════════════════════════════════════════════════════════
718 : * main
719 : * ══════════════════════════════════════════════════════════════════════ */
720 :
721 28 : int main(int argc, char *argv[])
722 : {
723 28 : if (argc < 4) {
724 0 : fprintf(stderr,
725 : "Usage: %s <email-tui> <mock-imap-server> <mock-smtp-server>\n",
726 : argv[0]);
727 0 : return EXIT_FAILURE;
728 : }
729 28 : snprintf(g_tui_bin, sizeof(g_tui_bin), "%s", argv[1]);
730 28 : snprintf(g_imap_bin, sizeof(g_imap_bin), "%s", argv[2]);
731 28 : snprintf(g_smtp_bin, sizeof(g_smtp_bin), "%s", argv[3]);
732 :
733 28 : snprintf(g_test_home, sizeof(g_test_home),
734 28 : "/tmp/email-pty-attach-%d", (int)getpid());
735 28 : mkdir(g_test_home, 0700);
736 :
737 28 : if (getenv("HOME"))
738 28 : snprintf(g_old_home, sizeof(g_old_home), "%s", getenv("HOME"));
739 28 : setenv("HOME", g_test_home, 1);
740 28 : unsetenv("XDG_DATA_HOME");
741 28 : unsetenv("XDG_CONFIG_HOME");
742 28 : unsetenv("XDG_CACHE_HOME");
743 :
744 28 : write_config();
745 28 : write_attach_files();
746 28 : write_contacts_tsv();
747 28 : write_editor_script();
748 :
749 : /* ── TC-AT: Attach field ─────────────────────────────────────────── */
750 28 : printf("\n--- TC-AT: Attachment field in compose dialog (US-84) ---\n");
751 28 : RUN_TEST(test_attach_field_visible);
752 27 : RUN_TEST(test_attach_field_tab_from_subject);
753 26 : RUN_TEST(test_attach_dropdown_on_partial_path);
754 25 : RUN_TEST(test_attach_tab_completion_cycles);
755 24 : RUN_TEST(test_attach_enter_adds_file);
756 23 : RUN_TEST(test_attach_nonexistent_shows_error);
757 22 : RUN_TEST(test_attach_directory_shows_error);
758 21 : RUN_TEST(test_attach_d_removes_last);
759 20 : RUN_TEST(test_attach_multiple_files);
760 19 : RUN_TEST(test_attach_shift_tab_back_to_subject);
761 18 : RUN_TEST(test_attach_shown_in_review);
762 :
763 : /* ── TC-PCR: Post-compose review screen ─────────────────────────── */
764 17 : printf("\n--- TC-PCR: Post-compose review screen (US-85) ---\n");
765 17 : RUN_TEST(test_review_screen_appears);
766 16 : RUN_TEST(test_review_shows_from);
767 15 : RUN_TEST(test_review_shows_to);
768 14 : RUN_TEST(test_review_shows_subject);
769 13 : RUN_TEST(test_review_shows_body_lines);
770 12 : RUN_TEST(test_review_no_attachments_shown);
771 11 : RUN_TEST(test_review_s_sends_message);
772 10 : RUN_TEST(test_review_e_reopens_header_dialog);
773 9 : RUN_TEST(test_review_b_reopens_editor);
774 8 : RUN_TEST(test_review_a_opens_attach_picker);
775 7 : RUN_TEST(test_review_d_removes_attachment);
776 6 : RUN_TEST(test_review_q_asks_discard_confirm);
777 5 : RUN_TEST(test_review_q_y_cancels);
778 4 : RUN_TEST(test_review_esc_asks_discard_confirm);
779 3 : RUN_TEST(test_review_empty_to_disables_send);
780 2 : RUN_TEST(test_review_reply_uses_review_screen);
781 :
782 : /* ── Cleanup ─────────────────────────────────────────────────────── */
783 1 : stop_server(&g_imap_pid);
784 1 : stop_server(&g_smtp_pid);
785 1 : if (g_old_home[0]) setenv("HOME", g_old_home, 1);
786 :
787 1 : printf("\n--- PTY Attachment Test Results ---\n");
788 1 : printf("Tests Run: %d\n", g_tests_run);
789 1 : printf("Tests Passed: %d\n", g_tests_run - g_tests_failed);
790 1 : printf("Tests Failed: %d\n", g_tests_failed);
791 1 : return g_tests_failed > 0 ? EXIT_FAILURE : EXIT_SUCCESS;
792 : }
|