LCOV - code coverage report
Current view: top level - tests/pty - test_pty_attachment.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.6 % 501 499
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 41 41

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

Generated by: LCOV version 2.0-1