LCOV - code coverage report
Current view: top level - tests/unit - test_compose_service.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.5 % 273 269
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 26 26

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "compose_service.h"
       3              : #include <fcntl.h>
       4              : #include <stdio.h>
       5              : #include <stdlib.h>
       6              : #include <string.h>
       7              : #include <sys/stat.h>
       8              : #include <unistd.h>
       9              : 
      10              : /* ── compose_build_message ───────────────────────────────────────────── */
      11              : 
      12            1 : static void test_build_null_params(void) {
      13            1 :     char *out = NULL;
      14            1 :     size_t len = 0;
      15            1 :     ASSERT(compose_build_message(NULL, &out, &len) == -1,
      16              :            "build: NULL params → -1");
      17              : }
      18              : 
      19            1 : static void test_build_missing_fields(void) {
      20            1 :     char *out = NULL;
      21            1 :     size_t len = 0;
      22            1 :     ComposeParams p = {0};
      23            1 :     ASSERT(compose_build_message(&p, &out, &len) == -1,
      24              :            "build: missing from/to/subject → -1");
      25              : 
      26            1 :     p.from = "a@b.com";
      27            1 :     ASSERT(compose_build_message(&p, &out, &len) == -1,
      28              :            "build: missing to/subject → -1");
      29              : 
      30            1 :     p.to = "c@d.com";
      31            1 :     ASSERT(compose_build_message(&p, &out, &len) == -1,
      32              :            "build: missing subject → -1");
      33              : }
      34              : 
      35            1 : static void test_build_null_out(void) {
      36            1 :     ComposeParams p = {.from = "a@b.com", .to = "c@d.com", .subject = "Hi"};
      37            1 :     size_t len = 0;
      38            1 :     ASSERT(compose_build_message(&p, NULL, &len) == -1,
      39              :            "build: NULL out → -1");
      40              : }
      41              : 
      42            1 : static void test_build_basic(void) {
      43            1 :     ComposeParams p = {
      44              :         .from = "alice@example.com",
      45              :         .to = "bob@example.com",
      46              :         .subject = "Test subject",
      47              :         .body = "Hello, world!\n"
      48              :     };
      49            1 :     char *out = NULL;
      50            1 :     size_t len = 0;
      51            1 :     int rc = compose_build_message(&p, &out, &len);
      52            1 :     ASSERT(rc == 0, "build basic: success");
      53            1 :     ASSERT(out != NULL, "build basic: out not NULL");
      54            1 :     ASSERT(len > 0, "build basic: len > 0");
      55              : 
      56              :     /* Verify required headers */
      57            1 :     ASSERT(strstr(out, "From: alice@example.com\r\n") != NULL,
      58              :            "build basic: From header");
      59            1 :     ASSERT(strstr(out, "To: bob@example.com\r\n") != NULL,
      60              :            "build basic: To header");
      61            1 :     ASSERT(strstr(out, "Subject: Test subject\r\n") != NULL,
      62              :            "build basic: Subject header");
      63            1 :     ASSERT(strstr(out, "Date: ") != NULL,
      64              :            "build basic: Date header");
      65            1 :     ASSERT(strstr(out, "Message-ID: <") != NULL,
      66              :            "build basic: Message-ID header");
      67            1 :     ASSERT(strstr(out, "MIME-Version: 1.0\r\n") != NULL,
      68              :            "build basic: MIME-Version header");
      69            1 :     ASSERT(strstr(out, "Content-Type: text/plain; charset=UTF-8\r\n") != NULL,
      70              :            "build basic: Content-Type header");
      71              : 
      72              :     /* Verify header/body separator */
      73            1 :     ASSERT(strstr(out, "\r\n\r\n") != NULL,
      74              :            "build basic: CRLF separator");
      75              : 
      76              :     /* Verify body is present after separator */
      77            1 :     const char *body_start = strstr(out, "\r\n\r\n");
      78            1 :     ASSERT(body_start && strstr(body_start, "Hello, world!") != NULL,
      79              :            "build basic: body present");
      80              : 
      81              :     /* No In-Reply-To for non-reply */
      82            1 :     ASSERT(strstr(out, "In-Reply-To") == NULL,
      83              :            "build basic: no In-Reply-To");
      84              : 
      85            1 :     free(out);
      86              : }
      87              : 
      88            1 : static void test_build_reply(void) {
      89            1 :     ComposeParams p = {
      90              :         .from = "alice@example.com",
      91              :         .to = "bob@example.com",
      92              :         .subject = "Re: Original",
      93              :         .body = "Thanks!",
      94              :         .reply_to_msg_id = "<orig123@example.com>"
      95              :     };
      96            1 :     char *out = NULL;
      97            1 :     size_t len = 0;
      98            1 :     int rc = compose_build_message(&p, &out, &len);
      99            1 :     ASSERT(rc == 0, "build reply: success");
     100            1 :     ASSERT(strstr(out, "In-Reply-To: <orig123@example.com>\r\n") != NULL,
     101              :            "build reply: In-Reply-To header");
     102            1 :     ASSERT(strstr(out, "References: <orig123@example.com>\r\n") != NULL,
     103              :            "build reply: References header");
     104            1 :     free(out);
     105              : }
     106              : 
     107            1 : static void test_build_empty_body(void) {
     108            1 :     ComposeParams p = {
     109              :         .from = "a@b.com",
     110              :         .to = "c@d.com",
     111              :         .subject = "Empty",
     112              :         .body = NULL
     113              :     };
     114            1 :     char *out = NULL;
     115            1 :     size_t len = 0;
     116            1 :     int rc = compose_build_message(&p, &out, &len);
     117            1 :     ASSERT(rc == 0, "build empty body: success");
     118            1 :     ASSERT(out != NULL, "build empty body: not NULL");
     119            1 :     free(out);
     120              : }
     121              : 
     122            1 : static void test_build_lf_to_crlf(void) {
     123            1 :     ComposeParams p = {
     124              :         .from = "a@b.com",
     125              :         .to = "c@d.com",
     126              :         .subject = "Lines",
     127              :         .body = "line1\nline2\nline3"
     128              :     };
     129            1 :     char *out = NULL;
     130            1 :     size_t len = 0;
     131            1 :     int rc = compose_build_message(&p, &out, &len);
     132            1 :     ASSERT(rc == 0, "build LF→CRLF: success");
     133              :     /* Body should have CRLF line endings */
     134            1 :     const char *body = strstr(out, "\r\n\r\n");
     135            1 :     ASSERT(body && strstr(body, "line1\r\nline2\r\nline3") != NULL,
     136              :            "build LF→CRLF: body has CRLF");
     137            1 :     free(out);
     138              : }
     139              : 
     140              : /* ── compose_extract_reply_meta ──────────────────────────────────────── */
     141              : 
     142            1 : static void test_extract_null(void) {
     143            1 :     char *rt = NULL, *subj = NULL, *msgid = NULL;
     144            1 :     ASSERT(compose_extract_reply_meta(NULL, &rt, &subj, &msgid) == -1,
     145              :            "extract: NULL raw_msg → -1");
     146            1 :     ASSERT(compose_extract_reply_meta("From: a\r\n\r\n", NULL, &subj, &msgid) == -1,
     147              :            "extract: NULL reply_to_out → -1");
     148              : }
     149              : 
     150            1 : static void test_extract_basic(void) {
     151            1 :     const char *raw =
     152              :         "From: Alice <alice@example.com>\r\n"
     153              :         "Subject: Hello\r\n"
     154              :         "Message-ID: <abc123@example.com>\r\n"
     155              :         "\r\n"
     156              :         "Body.\r\n";
     157            1 :     char *rt = NULL, *subj = NULL, *msgid = NULL;
     158            1 :     int rc = compose_extract_reply_meta(raw, &rt, &subj, &msgid);
     159            1 :     ASSERT(rc == 0, "extract basic: success");
     160            1 :     ASSERT(rt != NULL, "extract basic: reply_to not NULL");
     161            1 :     ASSERT(strstr(rt, "alice@example.com") != NULL,
     162              :            "extract basic: reply_to has address");
     163            1 :     ASSERT(subj != NULL && strncmp(subj, "Re: ", 4) == 0,
     164              :            "extract basic: subject starts with Re:");
     165            1 :     ASSERT(strstr(subj, "Hello") != NULL,
     166              :            "extract basic: subject has original");
     167            1 :     ASSERT(msgid != NULL && strcmp(msgid, "<abc123@example.com>") == 0,
     168              :            "extract basic: message-id correct");
     169            1 :     free(rt); free(subj); free(msgid);
     170              : }
     171              : 
     172            1 : static void test_extract_re_dedup(void) {
     173            1 :     const char *raw =
     174              :         "From: Bob\r\n"
     175              :         "Subject: Re: Re: Re: Original\r\n"
     176              :         "Message-ID: <x@y>\r\n"
     177              :         "\r\n";
     178            1 :     char *rt = NULL, *subj = NULL, *msgid = NULL;
     179            1 :     int rc = compose_extract_reply_meta(raw, &rt, &subj, &msgid);
     180            1 :     ASSERT(rc == 0, "extract re dedup: success");
     181            1 :     ASSERT(strcmp(subj, "Re: Original") == 0,
     182              :            "extract re dedup: single Re: prefix");
     183            1 :     free(rt); free(subj); free(msgid);
     184              : }
     185              : 
     186            1 : static void test_extract_reply_to_header(void) {
     187            1 :     const char *raw =
     188              :         "From: Alice\r\n"
     189              :         "Reply-To: noreply@example.com\r\n"
     190              :         "Subject: Test\r\n"
     191              :         "Message-ID: <z@w>\r\n"
     192              :         "\r\n";
     193            1 :     char *rt = NULL, *subj = NULL, *msgid = NULL;
     194            1 :     int rc = compose_extract_reply_meta(raw, &rt, &subj, &msgid);
     195            1 :     ASSERT(rc == 0, "extract Reply-To: success");
     196            1 :     ASSERT(strstr(rt, "noreply@example.com") != NULL,
     197              :            "extract Reply-To: prefers Reply-To over From");
     198            1 :     free(rt); free(subj); free(msgid);
     199              : }
     200              : 
     201              : /* ── Registration ────────────────────────────────────────────────────── */
     202              : 
     203            1 : static void test_build_cc(void) {
     204            1 :     ComposeParams p = {
     205              :         .from = "alice@example.com",
     206              :         .to   = "bob@example.com",
     207              :         .cc   = "carol@example.com",
     208              :         .subject = "Hello",
     209              :         .body = "Hi"
     210              :     };
     211            1 :     char *out = NULL; size_t len = 0;
     212            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0, "build cc: success");
     213            1 :     ASSERT(strstr(out, "Cc: carol@example.com\r\n") != NULL,
     214              :            "build cc: Cc header present");
     215            1 :     ASSERT(strstr(out, "Bcc:") == NULL, "build cc: no Bcc when bcc NULL");
     216            1 :     free(out);
     217              : }
     218              : 
     219            1 : static void test_build_bcc(void) {
     220            1 :     ComposeParams p = {
     221              :         .from = "alice@example.com",
     222              :         .to   = "bob@example.com",
     223              :         .bcc  = "secret@example.com",
     224              :         .subject = "Hello",
     225              :         .body = "Hi"
     226              :     };
     227            1 :     char *out = NULL; size_t len = 0;
     228            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0, "build bcc: success");
     229            1 :     ASSERT(strstr(out, "Bcc: secret@example.com\r\n") != NULL,
     230              :            "build bcc: Bcc header present");
     231            1 :     ASSERT(strstr(out, "Cc:") == NULL, "build bcc: no Cc when cc NULL");
     232            1 :     free(out);
     233              : }
     234              : 
     235            1 : static void test_build_cc_and_bcc(void) {
     236            1 :     ComposeParams p = {
     237              :         .from = "alice@example.com",
     238              :         .to   = "bob@example.com",
     239              :         .cc   = "carol@example.com, dave@example.com",
     240              :         .bcc  = "eve@example.com",
     241              :         .subject = "Meeting",
     242              :         .body = "See you there"
     243              :     };
     244            1 :     char *out = NULL; size_t len = 0;
     245            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0, "build cc+bcc: success");
     246            1 :     ASSERT(strstr(out, "Cc: carol@example.com, dave@example.com\r\n") != NULL,
     247              :            "build cc+bcc: Cc header");
     248            1 :     ASSERT(strstr(out, "Bcc: eve@example.com\r\n") != NULL,
     249              :            "build cc+bcc: Bcc header");
     250            1 :     ASSERT(strstr(out, "To: bob@example.com\r\n") != NULL,
     251              :            "build cc+bcc: To header still present");
     252            1 :     free(out);
     253              : }
     254              : 
     255            1 : static void test_build_empty_cc_omitted(void) {
     256            1 :     ComposeParams p = {
     257              :         .from = "a@b.com", .to = "c@d.com",
     258              :         .cc = "", .bcc = "",
     259              :         .subject = "X", .body = "Y"
     260              :     };
     261            1 :     char *out = NULL; size_t len = 0;
     262            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0, "build empty cc: success");
     263            1 :     ASSERT(strstr(out, "Cc:") == NULL,  "build empty cc: no Cc header");
     264            1 :     ASSERT(strstr(out, "Bcc:") == NULL, "build empty cc: no Bcc header");
     265            1 :     free(out);
     266              : }
     267              : 
     268              : /* ── compose_build_message with attachments (US-84) ─────────────────── */
     269              : 
     270              : /* Write a small temp file and return a heap-allocated path, or NULL. */
     271            5 : static char *make_tmp_attach(const char *content)
     272              : {
     273            5 :     char *path = strdup("/tmp/test-compose-attach-XXXXXX");
     274            5 :     int fd = mkstemp(path);
     275            5 :     if (fd < 0) { free(path); return NULL; }
     276            5 :     size_t len = strlen(content);
     277            5 :     ssize_t written = write(fd, content, len);
     278            5 :     close(fd);
     279            5 :     if (written < 0 || (size_t)written != len) {
     280            0 :         unlink(path);
     281            0 :         free(path);
     282            0 :         return NULL;
     283              :     }
     284            5 :     return path;
     285              : }
     286              : 
     287              : 
     288              : /* Write a temp file that keeps a given extension, so that the MIME type can be
     289              :  * inferred from it.  mkstemps() reserves the trailing suffix. */
     290           16 : static char *make_tmp_attach_ext(const char *content, const char *suffix)
     291              : {
     292           16 :     char *path = NULL;
     293           16 :     if (asprintf(&path, "/tmp/test-compose-mime-XXXXXX%s", suffix) == -1) return NULL;
     294           16 :     int fd = mkstemps(path, (int)strlen(suffix));
     295           16 :     if (fd < 0) { free(path); return NULL; }
     296           16 :     size_t len = strlen(content);
     297           16 :     ssize_t written = write(fd, content, len);
     298           16 :     close(fd);
     299           16 :     if (written < 0 || (size_t)written != len) {
     300            0 :         unlink(path); free(path); return NULL;
     301              :     }
     302           16 :     return path;
     303              : }
     304              : 
     305              : /* Every extension the builder knows must reach the outgoing part's
     306              :  * Content-Type; an unknown one must fall back to application/octet-stream.
     307              :  * Attachments carry their type from the filename alone, so a wrong mapping
     308              :  * silently mislabels what the recipient receives. */
     309            1 : static void test_attach_mime_type_per_extension(void)
     310              : {
     311              :     static const struct { const char *suffix; const char *ctype; } cases[] = {
     312              :         { ".pdf",  "application/pdf"    },
     313              :         { ".txt",  "text/plain"         },
     314              :         { ".html", "text/html"          },
     315              :         { ".htm",  "text/html"          },
     316              :         { ".jpg",  "image/jpeg"         },
     317              :         { ".jpeg", "image/jpeg"         },
     318              :         { ".png",  "image/png"          },
     319              :         { ".gif",  "image/gif"          },
     320              :         { ".zip",  "application/zip"    },
     321              :         { ".gz",   "application/gzip"   },
     322              :         { ".csv",  "text/csv"           },
     323              :         { ".json", "application/json"   },
     324              :         { ".xml",  "application/xml"    },
     325              :         { ".PDF",  "application/pdf"    },   /* extension match is case-insensitive */
     326              :         { ".bin",  "application/octet-stream" },  /* unknown extension */
     327              :     };
     328              : 
     329           16 :     for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
     330           15 :         char *path = make_tmp_attach_ext("payload", cases[i].suffix);
     331           15 :         ASSERT(path != NULL, "mime: temp attachment created");
     332           15 :         if (!path) continue;
     333              : 
     334           15 :         const char *atts[] = { path, NULL };
     335           15 :         ComposeParams p = {
     336              :             .from = "a@b.com", .to = "c@d.com",
     337              :             .subject = "MIME", .body = "Hi",
     338              :             .attachments = atts, .attach_count = 1
     339              :         };
     340           15 :         char *out = NULL; size_t len = 0;
     341           15 :         int rc = compose_build_message(&p, &out, &len);
     342           15 :         ASSERT(rc == 0, "mime: build succeeds");
     343           15 :         ASSERT(out && strstr(out, cases[i].ctype) != NULL,
     344              :                "mime: Content-Type inferred from the extension");
     345           15 :         free(out);
     346           15 :         unlink(path);
     347           15 :         free(path);
     348              :     }
     349              : }
     350              : 
     351              : /* A file with no dot at all has no extension to infer from. */
     352            1 : static void test_attach_mime_type_without_extension(void)
     353              : {
     354            1 :     char *path = make_tmp_attach_ext("payload", "");
     355            1 :     ASSERT(path != NULL, "mime: extensionless temp file created");
     356            1 :     if (!path) return;
     357              : 
     358            1 :     const char *atts[] = { path, NULL };
     359            1 :     ComposeParams p = {
     360              :         .from = "a@b.com", .to = "c@d.com",
     361              :         .subject = "MIME", .body = "Hi",
     362              :         .attachments = atts, .attach_count = 1
     363              :     };
     364            1 :     char *out = NULL; size_t len = 0;
     365            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0, "mime: build succeeds");
     366            1 :     ASSERT(out && strstr(out, "application/octet-stream") != NULL,
     367              :            "mime: no extension → application/octet-stream");
     368            1 :     free(out);
     369            1 :     unlink(path);
     370            1 :     free(path);
     371              : }
     372              : 
     373            1 : static void test_build_no_attach_is_text_plain(void)
     374              : {
     375              :     /* Zero attachments → must still produce text/plain (backward compat) */
     376            1 :     ComposeParams p = {
     377              :         .from = "a@b.com", .to = "c@d.com",
     378              :         .subject = "X", .body = "Hi",
     379              :         .attachments = NULL, .attach_count = 0
     380              :     };
     381            1 :     char *out = NULL; size_t len = 0;
     382            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0,
     383              :            "attach: no attach → success");
     384            1 :     ASSERT(strstr(out, "Content-Type: text/plain") != NULL,
     385              :            "attach: no attach → text/plain Content-Type");
     386            1 :     ASSERT(strstr(out, "multipart/mixed") == NULL,
     387              :            "attach: no attach → no multipart/mixed");
     388            1 :     free(out);
     389              : }
     390              : 
     391            1 : static void test_build_one_attachment_multipart(void)
     392              : {
     393              :     /* One attachment → multipart/mixed with text part + attachment part */
     394            1 :     char *path = make_tmp_attach("attachment data\n");
     395            1 :     ASSERT(path != NULL, "attach one: tmp file created");
     396              : 
     397            1 :     const char *attachments[] = { path, NULL };
     398            1 :     ComposeParams p = {
     399              :         .from = "a@b.com", .to = "c@d.com",
     400              :         .subject = "With attach", .body = "See attached.",
     401              :         .attachments = attachments, .attach_count = 1
     402              :     };
     403            1 :     char *out = NULL; size_t len = 0;
     404            1 :     int rc = compose_build_message(&p, &out, &len);
     405            1 :     ASSERT(rc == 0, "attach one: success");
     406            1 :     ASSERT(strstr(out, "multipart/mixed") != NULL,
     407              :            "attach one: Content-Type is multipart/mixed");
     408            1 :     ASSERT(strstr(out, "boundary=") != NULL,
     409              :            "attach one: boundary present");
     410            1 :     ASSERT(strstr(out, "Content-Type: text/plain") != NULL,
     411              :            "attach one: text part present");
     412            1 :     ASSERT(strstr(out, "Content-Disposition: attachment") != NULL,
     413              :            "attach one: attachment part has Content-Disposition");
     414            1 :     ASSERT(strstr(out, "Content-Transfer-Encoding: base64") != NULL,
     415              :            "attach one: attachment encoded as base64");
     416            1 :     ASSERT(strstr(out, "See attached.") != NULL,
     417              :            "attach one: body text present");
     418            1 :     free(out);
     419            1 :     unlink(path);
     420            1 :     free(path);
     421              : }
     422              : 
     423            1 : static void test_build_attachment_filename_in_disposition(void)
     424              : {
     425              :     /* Content-Disposition must include filename="<basename>" */
     426            1 :     char *path = make_tmp_attach("pdf stub");
     427            1 :     ASSERT(path != NULL, "attach filename: tmp file created");
     428              : 
     429              :     /* Rename to something with a meaningful extension */
     430              :     char named[256];
     431            1 :     snprintf(named, sizeof(named), "/tmp/report-%d.pdf", (int)getpid());
     432            1 :     rename(path, named);
     433            1 :     free(path);
     434              : 
     435            1 :     const char *attachments[] = { named, NULL };
     436            1 :     ComposeParams p = {
     437              :         .from = "a@b.com", .to = "c@d.com",
     438              :         .subject = "PDF", .body = "Attached.",
     439              :         .attachments = attachments, .attach_count = 1
     440              :     };
     441            1 :     char *out = NULL; size_t len = 0;
     442            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0,
     443              :            "attach filename: success");
     444            1 :     ASSERT(strstr(out, "filename=") != NULL,
     445              :            "attach filename: filename= in Content-Disposition");
     446              :     /* Should contain just the basename, not the full path */
     447              :     char basename_check[64];
     448            1 :     snprintf(basename_check, sizeof(basename_check), "report-%d.pdf", (int)getpid());
     449            1 :     ASSERT(strstr(out, basename_check) != NULL,
     450              :            "attach filename: basename in Content-Disposition");
     451            1 :     free(out);
     452            1 :     unlink(named);
     453              : }
     454              : 
     455            1 : static void test_build_two_attachments(void)
     456              : {
     457              :     /* Two attachments → two MIME attachment parts */
     458            1 :     char *path1 = make_tmp_attach("file one");
     459            1 :     char *path2 = make_tmp_attach("file two");
     460            1 :     ASSERT(path1 && path2, "attach two: tmp files created");
     461              : 
     462            1 :     const char *attachments[] = { path1, path2, NULL };
     463            1 :     ComposeParams p = {
     464              :         .from = "a@b.com", .to = "c@d.com",
     465              :         .subject = "Two", .body = "Both attached.",
     466              :         .attachments = attachments, .attach_count = 2
     467              :     };
     468            1 :     char *out = NULL; size_t len = 0;
     469            1 :     ASSERT(compose_build_message(&p, &out, &len) == 0,
     470              :            "attach two: success");
     471            1 :     ASSERT(strstr(out, "multipart/mixed") != NULL,
     472              :            "attach two: multipart/mixed");
     473              :     /* Count Content-Disposition: attachment occurrences — must be 2 */
     474            1 :     int count = 0;
     475            1 :     const char *p2 = out;
     476            3 :     while ((p2 = strstr(p2, "Content-Disposition: attachment")) != NULL) {
     477            2 :         count++;
     478            2 :         p2++;
     479              :     }
     480            1 :     ASSERT(count == 2, "attach two: exactly 2 attachment parts");
     481            1 :     free(out);
     482            1 :     unlink(path1); free(path1);
     483            1 :     unlink(path2); free(path2);
     484              : }
     485              : 
     486            1 : static void test_build_attachment_boundary_unique(void)
     487              : {
     488              :     /* Two separate messages must have different MIME boundaries */
     489            1 :     char *path = make_tmp_attach("data");
     490            1 :     ASSERT(path != NULL, "attach boundary: tmp file created");
     491              : 
     492            1 :     const char *attachments[] = { path, NULL };
     493            1 :     ComposeParams p = {
     494              :         .from = "a@b.com", .to = "c@d.com",
     495              :         .subject = "B", .body = ".",
     496              :         .attachments = attachments, .attach_count = 1
     497              :     };
     498            1 :     char *out1 = NULL, *out2 = NULL;
     499            1 :     size_t len1 = 0, len2 = 0;
     500            1 :     ASSERT(compose_build_message(&p, &out1, &len1) == 0, "boundary: msg1 ok");
     501            1 :     ASSERT(compose_build_message(&p, &out2, &len2) == 0, "boundary: msg2 ok");
     502              : 
     503              :     /* Extract boundary values — they must differ (include pid/time/random) */
     504            1 :     const char *b1 = strstr(out1, "boundary=\"");
     505            1 :     const char *b2 = strstr(out2, "boundary=\"");
     506            1 :     ASSERT(b1 && b2, "boundary: both have boundary");
     507              :     /* At minimum the two messages should not be byte-identical */
     508            1 :     ASSERT(strcmp(out1, out2) != 0 || len1 != len2,
     509              :            "boundary: two messages differ (unique boundary or Message-ID)");
     510            1 :     free(out1); free(out2);
     511            1 :     unlink(path); free(path);
     512              : }
     513              : 
     514            1 : static void test_build_attachment_nonexistent_file(void)
     515              : {
     516              :     /* Non-existent file path → compose_build_message returns -1 */
     517            1 :     const char *attachments[] = { "/tmp/no-such-file-xyz-404.bin", NULL };
     518            1 :     ComposeParams p = {
     519              :         .from = "a@b.com", .to = "c@d.com",
     520              :         .subject = "Bad", .body = ".",
     521              :         .attachments = attachments, .attach_count = 1
     522              :     };
     523            1 :     char *out = NULL; size_t len = 0;
     524            1 :     ASSERT(compose_build_message(&p, &out, &len) == -1,
     525              :            "attach nonexistent: returns -1");
     526            1 :     ASSERT(out == NULL, "attach nonexistent: out is NULL");
     527              : }
     528              : 
     529            1 : void test_compose_service(void) {
     530            1 :     RUN_TEST(test_build_null_params);
     531            1 :     RUN_TEST(test_build_missing_fields);
     532            1 :     RUN_TEST(test_build_null_out);
     533            1 :     RUN_TEST(test_build_basic);
     534            1 :     RUN_TEST(test_build_reply);
     535            1 :     RUN_TEST(test_build_empty_body);
     536            1 :     RUN_TEST(test_build_lf_to_crlf);
     537            1 :     RUN_TEST(test_build_cc);
     538            1 :     RUN_TEST(test_build_bcc);
     539            1 :     RUN_TEST(test_build_cc_and_bcc);
     540            1 :     RUN_TEST(test_build_empty_cc_omitted);
     541            1 :     RUN_TEST(test_extract_null);
     542            1 :     RUN_TEST(test_extract_basic);
     543            1 :     RUN_TEST(test_extract_re_dedup);
     544            1 :     RUN_TEST(test_extract_reply_to_header);
     545              :     /* US-84: attachment tests — fail until compose_build_message handles them */
     546            1 :     RUN_TEST(test_attach_mime_type_per_extension);
     547            1 :     RUN_TEST(test_attach_mime_type_without_extension);
     548            1 :     RUN_TEST(test_build_no_attach_is_text_plain);
     549            1 :     RUN_TEST(test_build_one_attachment_multipart);
     550            1 :     RUN_TEST(test_build_attachment_filename_in_disposition);
     551            1 :     RUN_TEST(test_build_two_attachments);
     552            1 :     RUN_TEST(test_build_attachment_boundary_unique);
     553            1 :     RUN_TEST(test_build_attachment_nonexistent_file);
     554            1 : }
        

Generated by: LCOV version 2.0-1