LCOV - code coverage report
Current view: top level - tests/unit - test_wizard.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 100.0 % 211 211
Test Date: 2026-05-07 15:53:07 Functions: 100.0 % 2 2

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

Generated by: LCOV version 2.0-1