LCOV - code coverage report
Current view: top level - tests/unit - test_local_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 97.2 % 705 685
Test Date: 2026-08-21 10:11:28 Functions: 100.0 % 28 28

            Line data    Source code
       1              : #include "test_helpers.h"
       2              : #include "local_store.h"
       3              : #include "fs_util.h"
       4              : #include "raii.h"
       5              : #include <string.h>
       6              : #include <strings.h>
       7              : #include <stdlib.h>
       8              : #include <unistd.h>
       9              : #include <stdio.h>
      10              : 
      11              : /* ── Helper to set up a clean test environment ────────────────────────── */
      12              : 
      13           20 : static void setup_test_env(const char *home) {
      14           20 :     setenv("HOME", home, 1);
      15           20 :     unsetenv("XDG_DATA_HOME");
      16           20 :     local_store_init("imaps://test.example.com", "testuser");
      17           20 : }
      18              : 
      19              : /* ── Message store tests ─────────────────────────────────────────────── */
      20              : 
      21            1 : void test_local_msg_store(void) {
      22            1 :     char *old_home = getenv("HOME");
      23            1 :     setup_test_env("/tmp/email-cli-store-test");
      24              : 
      25            1 :     const char *folder = "INBOX";
      26            1 :     const char *uid    = "0000000000000137";
      27              : 
      28              :     /* Pre-clean */
      29            1 :     unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
      30              :            "testuser/store/INBOX/7/3/0000000000000137.eml");
      31              : 
      32              :     /* 1. Not stored initially */
      33            1 :     ASSERT(local_msg_exists(folder, uid) == 0,
      34              :            "local_msg_exists: should be 0 before save");
      35              : 
      36              :     /* 2. Save and verify */
      37            1 :     const char *content = "From: test@example.com\r\nDate: Mon, 30 Mar 2026 12:00:00 +0000\r\n"
      38              :                           "Subject: Test\r\n\r\nHello!";
      39            1 :     int rc = local_msg_save(folder, uid, content, strlen(content));
      40            1 :     ASSERT(rc == 0, "local_msg_save: should return 0");
      41            1 :     ASSERT(local_msg_exists(folder, uid) == 1,
      42              :            "local_msg_exists: should be 1 after save");
      43              : 
      44              :     /* 3. Load and verify content */
      45              :     {
      46            2 :         RAII_STRING char *loaded = local_msg_load(folder, uid);
      47            1 :         ASSERT(loaded != NULL, "local_msg_load: should not be NULL");
      48            1 :         ASSERT(strcmp(loaded, content) == 0, "local_msg_load: content mismatch");
      49              :     }
      50              : 
      51              :     /* 4. Different UIDs are independent */
      52            1 :     ASSERT(local_msg_exists(folder, "0000000000000099") == 0,
      53              :            "local_msg_exists: UID 99 should not exist");
      54              : 
      55              :     /* 5. Reverse digit bucketing: UID 42 → 2/4/ */
      56            1 :     const char *c2 = "Subject: UID 42\r\n\r\nBody";
      57            1 :     local_msg_save(folder, "0000000000000042", c2, strlen(c2));
      58            1 :     ASSERT(local_msg_exists(folder, "0000000000000042") == 1,
      59              :            "local_msg_exists: UID 42 after save (bucket 2/4)");
      60              : 
      61              :     /* 6. UID 5 → 5/0/ (single digit pads to 0) */
      62            1 :     const char *c3 = "Subject: UID 5\r\n\r\nBody";
      63            1 :     local_msg_save(folder, "0000000000000005", c3, strlen(c3));
      64            1 :     ASSERT(local_msg_exists(folder, "0000000000000005") == 1,
      65              :            "local_msg_exists: UID 5 after save (bucket 5/0)");
      66              : 
      67              :     /* Cleanup */
      68            1 :     unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
      69              :            "testuser/store/INBOX/7/3/0000000000000137.eml");
      70            1 :     unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
      71              :            "testuser/store/INBOX/2/4/0000000000000042.eml");
      72            1 :     unlink("/tmp/email-cli-store-test/.local/share/email-cli/accounts/"
      73              :            "testuser/store/INBOX/5/0/0000000000000005.eml");
      74              : 
      75            1 :     if (old_home) setenv("HOME", old_home, 1);
      76            0 :     else unsetenv("HOME");
      77              : }
      78              : 
      79              : /* ── Header eviction tests ───────────────────────────────────────────── */
      80              : 
      81            1 : void test_local_hdr_evict(void) {
      82            1 :     char *old_home = getenv("HOME");
      83            1 :     setup_test_env("/tmp/email-cli-hdr-evict-test");
      84              : 
      85            1 :     const char *folder = "INBOX";
      86              : 
      87            1 :     local_hdr_save(folder, "0000000000000010", "header-10", 9);
      88            1 :     local_hdr_save(folder, "0000000000000020", "header-20", 9);
      89              : 
      90            1 :     ASSERT(local_hdr_exists(folder, "0000000000000010") == 1, "hdr_evict: UID 10 before");
      91            1 :     ASSERT(local_hdr_exists(folder, "0000000000000020") == 1, "hdr_evict: UID 20 before");
      92              : 
      93            1 :     char keep[][17] = {"0000000000000020"};
      94            1 :     local_hdr_evict_stale(folder, (const char (*)[17])keep, 1);
      95              : 
      96            1 :     ASSERT(local_hdr_exists(folder, "0000000000000010") == 0, "hdr_evict: UID 10 evicted");
      97            1 :     ASSERT(local_hdr_exists(folder, "0000000000000020") == 1, "hdr_evict: UID 20 kept");
      98              : 
      99              :     /* Cleanup */
     100            1 :     local_hdr_evict_stale(folder, NULL, 0);
     101              : 
     102            1 :     if (old_home) setenv("HOME", old_home, 1);
     103            0 :     else unsetenv("HOME");
     104              : }
     105              : 
     106              : /* ── Index tests ─────────────────────────────────────────────────────── */
     107              : 
     108            1 : void test_local_index(void) {
     109            1 :     char *old_home = getenv("HOME");
     110            1 :     setup_test_env("/tmp/email-cli-index-test");
     111              : 
     112            1 :     const char *msg =
     113              :         "From: noreply@github.com\r\n"
     114              :         "Date: Tue, 15 Mar 2026 10:30:00 +0100\r\n"
     115              :         "Subject: Test\r\n\r\nBody";
     116              : 
     117            1 :     int rc = local_index_update("INBOX", "0000000000000042", msg);
     118            1 :     ASSERT(rc == 0, "local_index_update: should return 0");
     119              : 
     120              :     /* Verify from index exists */
     121            1 :     const char *from_path =
     122              :         "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
     123              :         "testuser/index/from/github.com/noreply";
     124              :     {
     125            2 :         RAII_FILE FILE *fp = fopen(from_path, "r");
     126            1 :         ASSERT(fp != NULL, "from index file should exist");
     127            1 :         if (fp) {
     128              :             char line[256];
     129            1 :             ASSERT(fgets(line, sizeof(line), fp) != NULL,
     130              :                    "from index should have a line");
     131            1 :             ASSERT(strstr(line, "INBOX/0000000000000042") != NULL,
     132              :                    "from index should contain INBOX/0000000000000042");
     133              :         }
     134              :     }
     135              : 
     136              :     /* Verify date index exists */
     137            1 :     const char *date_path =
     138              :         "/tmp/email-cli-index-test/.local/share/email-cli/accounts/"
     139              :         "testuser/index/date/2026/03/15";
     140              :     {
     141            2 :         RAII_FILE FILE *fp = fopen(date_path, "r");
     142            1 :         ASSERT(fp != NULL, "date index file should exist");
     143            1 :         if (fp) {
     144              :             char line[256];
     145            1 :             ASSERT(fgets(line, sizeof(line), fp) != NULL,
     146              :                    "date index should have a line");
     147            1 :             ASSERT(strstr(line, "INBOX/0000000000000042") != NULL,
     148              :                    "date index should contain INBOX/0000000000000042");
     149              :         }
     150              :     }
     151              : 
     152              :     /* Duplicate should not be added */
     153            1 :     local_index_update("INBOX", "0000000000000042", msg);
     154              :     {
     155            2 :         RAII_FILE FILE *fp = fopen(from_path, "r");
     156            1 :         int count = 0;
     157              :         char line[256];
     158            2 :         while (fp && fgets(line, sizeof(line), fp)) count++;
     159            1 :         ASSERT(count == 1, "from index should have exactly 1 entry (no dupes)");
     160              :     }
     161              : 
     162              :     /* Cleanup */
     163            1 :     if (from_path) unlink(from_path);
     164            1 :     if (date_path) unlink(date_path);
     165              : 
     166            1 :     if (old_home) setenv("HOME", old_home, 1);
     167            0 :     else unsetenv("HOME");
     168              : }
     169              : 
     170              : /* ── Manifest tests ──────────────────────────────────────────────────── */
     171              : 
     172            1 : void test_manifest(void) {
     173            1 :     char *old_home = getenv("HOME");
     174            1 :     setup_test_env("/tmp/email-cli-manifest-test");
     175              : 
     176              :     /* Pre-clean */
     177            1 :     unlink("/tmp/email-cli-manifest-test/.local/share/email-cli/"
     178              :            "accounts/testuser/manifests/INBOX.tsv");
     179              : 
     180              :     /* 1. Load non-existent manifest returns NULL */
     181            1 :     Manifest *m = manifest_load("INBOX");
     182            1 :     ASSERT(m == NULL, "manifest_load: NULL for missing file");
     183              : 
     184              :     /* 2. Create manifest, add entries, save */
     185            1 :     m = calloc(1, sizeof(Manifest));
     186            1 :     ASSERT(m != NULL, "manifest: calloc");
     187            1 :     manifest_upsert(m, "0000000000000042", strdup("Alice <alice@example.com>"),
     188              :                               strdup("Hello World"),
     189              :                               strdup("2024-03-15 10:00"), MSG_FLAG_UNSEEN);
     190            1 :     manifest_upsert(m, "0000000000000137", strdup("Bob <bob@test.org>"),
     191              :                               strdup("Re: Meeting"),
     192              :                               strdup("2024-03-16 14:30"), 0);
     193            1 :     ASSERT(m->count == 2, "manifest: 2 entries after upsert");
     194              : 
     195            1 :     int rc = manifest_save("INBOX", m);
     196            1 :     ASSERT(rc == 0, "manifest_save: returns 0");
     197              : 
     198              :     /* 3. Load back and verify */
     199            1 :     manifest_free(m);
     200            1 :     m = manifest_load("INBOX");
     201            1 :     ASSERT(m != NULL, "manifest_load: not NULL after save");
     202            1 :     ASSERT(m->count == 2, "manifest_load: 2 entries");
     203              : 
     204            1 :     ManifestEntry *e42 = manifest_find(m, "0000000000000042");
     205            1 :     ASSERT(e42 != NULL, "manifest_find: UID 42 found");
     206            1 :     ASSERT(strcmp(e42->from, "Alice <alice@example.com>") == 0,
     207              :            "manifest: UID 42 from correct");
     208            1 :     ASSERT(strcmp(e42->subject, "Hello World") == 0,
     209              :            "manifest: UID 42 subject correct");
     210            1 :     ASSERT(strcmp(e42->date, "2024-03-15 10:00") == 0,
     211              :            "manifest: UID 42 date correct");
     212              : 
     213            1 :     ManifestEntry *e137 = manifest_find(m, "0000000000000137");
     214            1 :     ASSERT(e137 != NULL, "manifest_find: UID 137 found");
     215            1 :     ASSERT(strcmp(e137->subject, "Re: Meeting") == 0,
     216              :            "manifest: UID 137 subject correct");
     217              : 
     218              :     /* 4. Upsert updates existing entry */
     219            1 :     manifest_upsert(m, "0000000000000042", strdup("Alice Updated"),
     220              :                            strdup("Updated Subject"),
     221              :                            strdup("2024-03-15 11:00"), 0 /* no flags */);
     222            1 :     ASSERT(m->count == 2, "manifest: still 2 after upsert-update");
     223            1 :     e42 = manifest_find(m, "0000000000000042");
     224            1 :     ASSERT(strcmp(e42->subject, "Updated Subject") == 0,
     225              :            "manifest: upsert updated subject");
     226              : 
     227              :     /* 5. manifest_find returns NULL for missing UID */
     228            1 :     ASSERT(manifest_find(m, "0000000000000999") == NULL,
     229              :            "manifest_find: NULL for missing UID");
     230              : 
     231              :     /* 6. manifest_retain keeps only specified UIDs */
     232            1 :     char keep[][17] = {"0000000000000137"};
     233            1 :     manifest_retain(m, (const char (*)[17])keep, 1);
     234            1 :     ASSERT(m->count == 1, "manifest_retain: 1 entry after retain");
     235            1 :     ASSERT(manifest_find(m, "0000000000000137") != NULL, "manifest_retain: UID 137 kept");
     236            1 :     ASSERT(manifest_find(m, "0000000000000042") == NULL, "manifest_retain: UID 42 removed");
     237              : 
     238              :     /* 7. Nested folder manifest */
     239            1 :     Manifest *m2 = calloc(1, sizeof(Manifest));
     240            1 :     manifest_upsert(m2, "0000000000000001", strdup("Test"), strdup("Nested"), strdup("2024-01-01 00:00"), 0 /* no flags */);
     241            1 :     ASSERT(manifest_save("munka/ai", m2) == 0, "manifest_save: nested folder");
     242            1 :     manifest_free(m2);
     243            1 :     m2 = manifest_load("munka/ai");
     244            1 :     ASSERT(m2 != NULL, "manifest_load: nested folder");
     245            1 :     ASSERT(m2->count == 1, "manifest: nested has 1 entry");
     246            1 :     manifest_free(m2);
     247              : 
     248              :     /* Cleanup */
     249            1 :     manifest_free(m);
     250            1 :     if (old_home) setenv("HOME", old_home, 1);
     251            0 :     else unsetenv("HOME");
     252              : }
     253              : 
     254              : /* ── UI preferences tests ────────────────────────────────────────────── */
     255              : 
     256            1 : void test_ui_prefs(void) {
     257            1 :     char *old_home = getenv("HOME");
     258            1 :     setenv("HOME", "/tmp/email-cli-ui-pref-test-home", 1);
     259            1 :     unsetenv("XDG_DATA_HOME");
     260            1 :     unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
     261              : 
     262            1 :     ASSERT(ui_pref_get_int("folder_view_mode", 1) == 1,
     263              :            "ui_pref_get_int: missing key should return default 1");
     264            1 :     ASSERT(ui_pref_get_int("folder_view_mode", 0) == 0,
     265              :            "ui_pref_get_int: missing key should return default 0");
     266              : 
     267            1 :     ASSERT(ui_pref_set_int("folder_view_mode", 0) == 0,
     268              :            "ui_pref_set_int: should return 0");
     269            1 :     ASSERT(ui_pref_get_int("folder_view_mode", 1) == 0,
     270              :            "ui_pref_get_int: should return stored value 0");
     271              : 
     272            1 :     ASSERT(ui_pref_set_int("folder_view_mode", 1) == 0,
     273              :            "ui_pref_set_int: overwrite should return 0");
     274            1 :     ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
     275              :            "ui_pref_get_int: should return updated value 1");
     276              : 
     277            1 :     ASSERT(ui_pref_set_int("other_pref", 42) == 0,
     278              :            "ui_pref_set_int: second key should return 0");
     279            1 :     ASSERT(ui_pref_get_int("folder_view_mode", 0) == 1,
     280              :            "ui_pref_get_int: first key intact");
     281            1 :     ASSERT(ui_pref_get_int("other_pref", 0) == 42,
     282              :            "ui_pref_get_int: second key should return 42");
     283              : 
     284            1 :     ASSERT(ui_pref_get_int("no_such_key", 7) == 7,
     285              :            "ui_pref_get_int: unknown key should return default");
     286              : 
     287            1 :     unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
     288              : 
     289              :     /* ui_pref_get_str / ui_pref_set_str */
     290            1 :     ASSERT(ui_pref_get_str("str_key") == NULL,
     291              :            "ui_pref_get_str: missing file returns NULL");
     292              : 
     293            1 :     ASSERT(ui_pref_set_str("str_key", "hello") == 0,
     294              :            "ui_pref_set_str: returns 0 on first write");
     295              :     {
     296            1 :         char *v = ui_pref_get_str("str_key");
     297            1 :         ASSERT(v != NULL, "ui_pref_get_str: returns non-NULL after set");
     298            1 :         ASSERT(strcmp(v, "hello") == 0, "ui_pref_get_str: value matches");
     299            1 :         free(v);
     300              :     }
     301              : 
     302            1 :     ASSERT(ui_pref_set_str("str_key", "world") == 0,
     303              :            "ui_pref_set_str: overwrite returns 0");
     304              :     {
     305            1 :         char *v = ui_pref_get_str("str_key");
     306            1 :         ASSERT(v != NULL, "ui_pref_get_str: overwrite non-NULL");
     307            1 :         ASSERT(strcmp(v, "world") == 0, "ui_pref_get_str: overwrite value matches");
     308            1 :         free(v);
     309              :     }
     310              : 
     311            1 :     ASSERT(ui_pref_set_str("another_key", "value2") == 0,
     312              :            "ui_pref_set_str: second key returns 0");
     313              :     {
     314            1 :         char *v1 = ui_pref_get_str("str_key");
     315            1 :         char *v2 = ui_pref_get_str("another_key");
     316            1 :         ASSERT(v1 && strcmp(v1, "world") == 0,  "ui_pref_get_str: first key intact");
     317            1 :         ASSERT(v2 && strcmp(v2, "value2") == 0, "ui_pref_get_str: second key correct");
     318            1 :         free(v1); free(v2);
     319              :     }
     320              : 
     321            1 :     ASSERT(ui_pref_get_str("no_such_str_key") == NULL,
     322              :            "ui_pref_get_str: unknown key returns NULL");
     323              : 
     324            1 :     unlink("/tmp/email-cli-ui-pref-test-home/.local/share/email-cli/ui.ini");
     325              : 
     326            1 :     if (old_home) setenv("HOME", old_home, 1);
     327            0 :     else unsetenv("HOME");
     328              : }
     329              : 
     330              : /* ── msg delete tests ────────────────────────────────────────────────── */
     331              : 
     332            1 : void test_local_msg_delete(void) {
     333            1 :     char *old_home = getenv("HOME");
     334            1 :     setup_test_env("/tmp/email-cli-delete-test");
     335              : 
     336            1 :     const char *folder = "INBOX";
     337            1 :     const char *uid    = "0000000000001000";
     338              : 
     339              :     /* Save then delete */
     340            1 :     int rc = local_msg_save(folder, uid, "test body", 9);
     341            1 :     ASSERT(rc == 0, "delete: save succeeded");
     342            1 :     ASSERT(local_msg_exists(folder, uid) == 1, "delete: exists before delete");
     343              : 
     344            1 :     int del = local_msg_delete(folder, uid);
     345            1 :     ASSERT(del == 0, "delete: returns 0");
     346            1 :     ASSERT(local_msg_exists(folder, uid) == 0, "delete: does not exist after delete");
     347              : 
     348              :     /* Deleting a non-existent message should not crash */
     349            1 :     int del2 = local_msg_delete(folder, "0000000000001001");
     350            1 :     ASSERT(del2 == 0, "delete: non-existent msg returns 0");
     351              : 
     352              :     /* Delete also removes .hdr if it exists */
     353            1 :     const char *uid2 = "0000000000001002";
     354            1 :     local_hdr_save(folder, uid2, "from\tsubject\tdate\t\t0", 20);
     355            1 :     ASSERT(local_hdr_exists(folder, uid2) == 1, "delete: hdr exists before delete");
     356            1 :     local_msg_delete(folder, uid2);
     357            1 :     ASSERT(local_hdr_exists(folder, uid2) == 0, "delete: hdr removed by delete");
     358              : 
     359            1 :     if (old_home) setenv("HOME", old_home, 1);
     360            0 :     else unsetenv("HOME");
     361              : }
     362              : 
     363              : /* ── index email extraction tests ────────────────────────────────────── */
     364              : 
     365            1 : void test_local_index_email_extraction(void) {
     366            1 :     char *old_home = getenv("HOME");
     367            1 :     setup_test_env("/tmp/email-cli-email-extract-test");
     368              : 
     369              :     /* "Name <user@domain>" format */
     370              :     {
     371            1 :         const char *msg =
     372              :             "From: Alice <alice@example.com>\r\n"
     373              :             "Date: Mon, 01 Jan 2024 10:00:00 +0000\r\n"
     374              :             "Subject: Test\r\n\r\nBody";
     375            1 :         int rc = local_index_update("INBOX", "0000000000000201", msg);
     376            1 :         ASSERT(rc == 0, "email_extraction: Name<email> index_update returns 0");
     377              : 
     378            1 :         const char *from_path =
     379              :             "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
     380              :             "testuser/index/from/example.com/alice";
     381            2 :         RAII_FILE FILE *fp = fopen(from_path, "r");
     382            1 :         ASSERT(fp != NULL, "email_extraction: Name<email> from index created");
     383            1 :         if (fp) {
     384              :             char line[256];
     385            1 :             ASSERT(fgets(line, sizeof(line), fp) != NULL,
     386              :                    "email_extraction: from index has a line");
     387            1 :             ASSERT(strstr(line, "INBOX/0000000000000201") != NULL,
     388              :                    "email_extraction: from index contains correct ref");
     389              :         }
     390              :     }
     391              : 
     392              :     /* "<user@domain>" format without display name */
     393              :     {
     394            1 :         const char *msg2 =
     395              :             "From: <bob@test.org>\r\n"
     396              :             "Date: Tue, 02 Jan 2024 11:00:00 +0000\r\n"
     397              :             "Subject: Test2\r\n\r\nBody";
     398            1 :         int rc = local_index_update("INBOX", "0000000000000202", msg2);
     399            1 :         ASSERT(rc == 0, "email_extraction: <email> index_update returns 0");
     400              : 
     401            1 :         const char *from_path2 =
     402              :             "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
     403              :             "testuser/index/from/test.org/bob";
     404            2 :         RAII_FILE FILE *fp2 = fopen(from_path2, "r");
     405            1 :         ASSERT(fp2 != NULL, "email_extraction: <email> from index created");
     406              :     }
     407              : 
     408              :     /* Plain "user@domain" format */
     409              :     {
     410            1 :         const char *msg3 =
     411              :             "From: carol@sample.net\r\n"
     412              :             "Date: Wed, 03 Jan 2024 12:00:00 +0000\r\n"
     413              :             "Subject: Test3\r\n\r\nBody";
     414            1 :         int rc = local_index_update("INBOX", "0000000000000203", msg3);
     415            1 :         ASSERT(rc == 0, "email_extraction: plain email index_update returns 0");
     416              : 
     417            1 :         const char *from_path3 =
     418              :             "/tmp/email-cli-email-extract-test/.local/share/email-cli/accounts/"
     419              :             "testuser/index/from/sample.net/carol";
     420            2 :         RAII_FILE FILE *fp3 = fopen(from_path3, "r");
     421            1 :         ASSERT(fp3 != NULL, "email_extraction: plain email from index created");
     422              :     }
     423              : 
     424              :     /* From header with no @ sign: no from index entry created (graceful skip) */
     425              :     {
     426            1 :         const char *msg4 =
     427              :             "From: noemail\r\n"
     428              :             "Date: Thu, 04 Jan 2024 13:00:00 +0000\r\n"
     429              :             "Subject: Test4\r\n\r\nBody";
     430            1 :         int rc = local_index_update("INBOX", "0000000000000204", msg4);
     431            1 :         ASSERT(rc == 0, "email_extraction: no-@ returns 0 (graceful)");
     432              :     }
     433              : 
     434            1 :     if (old_home) setenv("HOME", old_home, 1);
     435            0 :     else unsetenv("HOME");
     436              : }
     437              : 
     438              : /* ── trash labels tests ──────────────────────────────────────────────── */
     439              : 
     440            1 : void test_local_trash_labels(void) {
     441            1 :     char *old_home = getenv("HOME");
     442            1 :     setup_test_env("/tmp/email-cli-trash-labels-test");
     443              : 
     444            1 :     const char *uid = "0000000000002000";
     445            1 :     const char *labels = "INBOX,Work,Important";
     446              : 
     447              :     /* Save and load */
     448            1 :     int rc = local_trash_labels_save(uid, labels);
     449            1 :     ASSERT(rc == 0, "trash_labels: save returns 0");
     450              : 
     451            2 :     RAII_STRING char *loaded = local_trash_labels_load(uid);
     452            1 :     ASSERT(loaded != NULL, "trash_labels: load returns non-NULL");
     453            1 :     ASSERT(strcmp(loaded, labels) == 0, "trash_labels: loaded value matches saved");
     454              : 
     455              :     /* Remove */
     456            1 :     local_trash_labels_remove(uid);
     457            2 :     RAII_STRING char *after_remove = local_trash_labels_load(uid);
     458            1 :     ASSERT(after_remove == NULL, "trash_labels: NULL after remove");
     459              : 
     460              :     /* Load non-existent returns NULL */
     461            2 :     RAII_STRING char *missing = local_trash_labels_load("0000000000009999");
     462            1 :     ASSERT(missing == NULL, "trash_labels: missing uid returns NULL");
     463              : 
     464              :     /* Remove non-existent should not crash */
     465            1 :     local_trash_labels_remove("0000000000009998");
     466              : 
     467            1 :     if (old_home) setenv("HOME", old_home, 1);
     468            0 :     else unsetenv("HOME");
     469              : }
     470              : 
     471              : /* ── gmail history id tests ──────────────────────────────────────────── */
     472              : 
     473            1 : void test_local_gmail_history(void) {
     474            1 :     char *old_home = getenv("HOME");
     475            1 :     setup_test_env("/tmp/email-cli-gmail-history-test");
     476              : 
     477              :     /* Pre-clean any leftover history file from previous runs */
     478            1 :     unlink("/tmp/email-cli-gmail-history-test/.local/share/email-cli/accounts/"
     479              :            "testuser/gmail_history_id");
     480              : 
     481              :     /* Load when missing returns NULL */
     482            2 :     RAII_STRING char *none = local_gmail_history_load();
     483            1 :     ASSERT(none == NULL, "gmail_history: missing returns NULL");
     484              : 
     485              :     /* Save and load */
     486            1 :     int rc = local_gmail_history_save("12345678");
     487            1 :     ASSERT(rc == 0, "gmail_history: save returns 0");
     488              : 
     489            2 :     RAII_STRING char *loaded = local_gmail_history_load();
     490            1 :     ASSERT(loaded != NULL, "gmail_history: load returns non-NULL");
     491            1 :     ASSERT(strcmp(loaded, "12345678") == 0, "gmail_history: loaded value matches");
     492              : 
     493              :     /* Overwrite with new value */
     494            1 :     int rc2 = local_gmail_history_save("99999999");
     495            1 :     ASSERT(rc2 == 0, "gmail_history: overwrite returns 0");
     496              : 
     497            2 :     RAII_STRING char *loaded2 = local_gmail_history_load();
     498            1 :     ASSERT(loaded2 != NULL, "gmail_history: overwrite load returns non-NULL");
     499            1 :     ASSERT(strcmp(loaded2, "99999999") == 0, "gmail_history: overwrite value correct");
     500              : 
     501              :     /* Null history id returns -1 */
     502            1 :     int rc3 = local_gmail_history_save(NULL);
     503            1 :     ASSERT(rc3 == -1, "gmail_history: NULL id returns -1");
     504              : 
     505            1 :     if (old_home) setenv("HOME", old_home, 1);
     506            0 :     else unsetenv("HOME");
     507              : }
     508              : 
     509              : /* ── local_hdr_get_labels tests ──────────────────────────────────────── */
     510              : 
     511            1 : void test_local_hdr_get_labels(void) {
     512            1 :     char *old_home = getenv("HOME");
     513            1 :     setup_test_env("/tmp/email-cli-hdr-labels-test");
     514              : 
     515            1 :     const char *folder = "";  /* Gmail flat store uses empty folder */
     516            1 :     const char *uid    = "0000000000003000";
     517              : 
     518              :     /* Gmail .hdr format: from\tsubject\tdate\tlabels\tflags */
     519            1 :     const char *hdr_content = "Alice <alice@example.com>\tHello\t2024-01-01 10:00\tINBOX,Work\t1";
     520            1 :     int rc = local_hdr_save(folder, uid, hdr_content, strlen(hdr_content));
     521            1 :     ASSERT(rc == 0, "hdr_labels: save returns 0");
     522              : 
     523            2 :     RAII_STRING char *labels = local_hdr_get_labels(folder, uid);
     524            1 :     ASSERT(labels != NULL, "hdr_labels: get_labels returns non-NULL");
     525            1 :     ASSERT(strcmp(labels, "INBOX,Work") == 0, "hdr_labels: labels value correct");
     526              : 
     527              :     /* Non-Gmail .hdr (no labels field) → NULL */
     528            1 :     const char *uid2 = "0000000000003001";
     529            1 :     const char *hdr2 = "Bob\tSubject\tDate";  /* only 3 fields, no 4th tab */
     530            1 :     local_hdr_save(folder, uid2, hdr2, strlen(hdr2));
     531            2 :     RAII_STRING char *labels2 = local_hdr_get_labels(folder, uid2);
     532            1 :     ASSERT(labels2 == NULL, "hdr_labels: non-Gmail hdr returns NULL");
     533              : 
     534              :     /* Non-existent uid → NULL */
     535            2 :     RAII_STRING char *labels3 = local_hdr_get_labels(folder, "0000000000009997");
     536            1 :     ASSERT(labels3 == NULL, "hdr_labels: missing uid returns NULL");
     537              : 
     538            1 :     if (old_home) setenv("HOME", old_home, 1);
     539            0 :     else unsetenv("HOME");
     540              : }
     541              : 
     542              : /* ── local_flag_search tests ────────────────────────────────────────────── */
     543              : 
     544            1 : void test_local_flag_search(void) {
     545            1 :     char *old_home = getenv("HOME");
     546            1 :     setup_test_env("/tmp/email-cli-flag-search-test");
     547              : 
     548              :     /* Pre-clean */
     549            1 :     unlink("/tmp/email-cli-flag-search-test/.local/share/email-cli/"
     550              :            "accounts/testuser/manifests/INBOX.tsv");
     551            1 :     unlink("/tmp/email-cli-flag-search-test/.local/share/email-cli/"
     552              :            "accounts/testuser/manifests/Sent.tsv");
     553              : 
     554              :     /* Build two manifests across two folders */
     555            1 :     Manifest *inbox = calloc(1, sizeof(Manifest));
     556            1 :     manifest_upsert(inbox, "0000000000000001", strdup("Alice"), strdup("Unread in INBOX"),
     557              :                     strdup("2024-03-01 10:00"), MSG_FLAG_UNSEEN);
     558            1 :     manifest_upsert(inbox, "0000000000000002", strdup("Bob"),   strdup("Read in INBOX"),
     559              :                     strdup("2024-03-02 11:00"), 0);
     560            1 :     manifest_save("INBOX", inbox);
     561            1 :     manifest_free(inbox);
     562              : 
     563            1 :     Manifest *sent = calloc(1, sizeof(Manifest));
     564            1 :     manifest_upsert(sent, "0000000000000003", strdup("Carol"), strdup("Flagged in Sent"),
     565              :                     strdup("2024-03-03 12:00"), MSG_FLAG_FLAGGED);
     566            1 :     manifest_upsert(sent, "0000000000000004", strdup("Dave"),  strdup("Unread in Sent"),
     567              :                     strdup("2024-03-04 13:00"), MSG_FLAG_UNSEEN);
     568            1 :     manifest_save("Sent", sent);
     569            1 :     manifest_free(sent);
     570              : 
     571              :     /* Test 1: flag_search for UNSEEN finds UID 1 (INBOX) and UID 4 (Sent) */
     572            1 :     SearchResult *res = NULL;
     573            1 :     int cnt = 0;
     574            1 :     int rc = local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
     575            1 :     ASSERT(rc == 0,   "flag_search: returns 0");
     576            1 :     ASSERT(cnt == 2,  "flag_search UNSEEN: 2 results");
     577            1 :     int found1 = 0, found4 = 0;
     578            3 :     for (int i = 0; i < cnt; i++) {
     579            2 :         if (strcmp(res[i].uid, "0000000000000001") == 0) {
     580            1 :             found1 = 1;
     581            1 :             ASSERT(strcmp(res[i].folder, "INBOX") == 0, "flag_search: UID1 folder is INBOX");
     582            1 :             ASSERT(res[i].flags & MSG_FLAG_UNSEEN,       "flag_search: UID1 has UNSEEN");
     583              :         }
     584            2 :         if (strcmp(res[i].uid, "0000000000000004") == 0) {
     585            1 :             found4 = 1;
     586            1 :             ASSERT(strcmp(res[i].folder, "Sent") == 0,   "flag_search: UID4 folder is Sent");
     587              :         }
     588              :     }
     589            1 :     ASSERT(found1, "flag_search UNSEEN: UID1 found");
     590            1 :     ASSERT(found4, "flag_search UNSEEN: UID4 found");
     591            1 :     local_search_free(res, cnt);
     592              : 
     593              :     /* Test 2: flag_search for FLAGGED finds only UID 3 (Sent) */
     594            1 :     res = NULL; cnt = 0;
     595            1 :     local_flag_search(MSG_FLAG_FLAGGED, &res, &cnt);
     596            1 :     ASSERT(cnt == 1,  "flag_search FLAGGED: 1 result");
     597            1 :     ASSERT(strcmp(res[0].uid, "0000000000000003") == 0, "flag_search FLAGGED: UID3");
     598            1 :     ASSERT(strcmp(res[0].folder, "Sent") == 0,           "flag_search FLAGGED: Sent folder");
     599            1 :     local_search_free(res, cnt);
     600              : 
     601              :     /* Test 3: flag_search for both bits returns union (UID1, UID3, UID4) */
     602            1 :     res = NULL; cnt = 0;
     603            1 :     local_flag_search(MSG_FLAG_UNSEEN | MSG_FLAG_FLAGGED, &res, &cnt);
     604            1 :     ASSERT(cnt == 3,  "flag_search UNSEEN|FLAGGED: 3 results");
     605            1 :     local_search_free(res, cnt);
     606              : 
     607              :     /* Test 4: UID2 (read, no flags) is never returned */
     608            1 :     res = NULL; cnt = 0;
     609            1 :     local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
     610            1 :     int found2 = 0;
     611            3 :     for (int i = 0; i < cnt; i++)
     612            2 :         if (strcmp(res[i].uid, "0000000000000002") == 0) found2 = 1;
     613            1 :     ASSERT(!found2, "flag_search: read UID2 not in UNSEEN results");
     614            1 :     local_search_free(res, cnt);
     615              : 
     616              :     /* Test 5: mask 0 means "no flag requirement", i.e. every cached message
     617              :      * across every folder — this is what the __all__ view is built on.  A
     618              :      * literal reading of `flags & 0` would return nothing, so the special
     619              :      * case has to be explicit. */
     620            1 :     res = NULL; cnt = 0;
     621            1 :     int rc0 = local_flag_search(0, &res, &cnt);
     622            1 :     ASSERT(rc0 == 0, "flag_search mask 0: returns 0");
     623            1 :     ASSERT(cnt == 4, "flag_search mask 0: all four messages");
     624            1 :     int seen_read = 0, seen_folders = 0;
     625            5 :     for (int i = 0; i < cnt; i++) {
     626            4 :         if (strcmp(res[i].uid, "0000000000000002") == 0) seen_read = 1;
     627            4 :         if (strcmp(res[i].folder, "Sent") == 0)          seen_folders = 1;
     628              :     }
     629            1 :     ASSERT(seen_read,    "flag_search mask 0: includes the flagless message");
     630            1 :     ASSERT(seen_folders, "flag_search mask 0: spans folders");
     631            1 :     local_search_free(res, cnt);
     632              : 
     633              :     /* Test 6: the aggregate manifest honours the same convention. */
     634            1 :     Manifest *all = manifest_load_all_with_flag(0);
     635            1 :     ASSERT(all != NULL,     "manifest_load_all_with_flag(0): returns a manifest");
     636            1 :     ASSERT(all && all->count == 4,
     637              :            "manifest_load_all_with_flag(0): every message, not none");
     638            1 :     manifest_free(all);
     639              : 
     640            1 :     Manifest *only_unread = manifest_load_all_with_flag(MSG_FLAG_UNSEEN);
     641            1 :     ASSERT(only_unread && only_unread->count == 2,
     642              :            "manifest_load_all_with_flag(UNSEEN): still filters");
     643            1 :     manifest_free(only_unread);
     644              : 
     645            1 :     res = NULL; cnt = 0;
     646            1 :     local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
     647            1 :     local_search_free(res, cnt);
     648              : 
     649            1 :     if (old_home) setenv("HOME", old_home, 1);
     650            0 :     else unsetenv("HOME");
     651              : }
     652              : 
     653            1 : void test_manifest_count_after_flag_update(void) {
     654            1 :     char *old_home = getenv("HOME");
     655            1 :     setup_test_env("/tmp/email-cli-count-update-test");
     656              : 
     657              :     /* Pre-clean */
     658            1 :     unlink("/tmp/email-cli-count-update-test/.local/share/email-cli/"
     659              :            "accounts/testuser/manifests/INBOX.tsv");
     660              : 
     661              :     /* Build a manifest with 3 unread messages */
     662            1 :     Manifest *m = calloc(1, sizeof(Manifest));
     663            1 :     manifest_upsert(m, "0000000000000010", strdup("A"), strdup("Msg1"),
     664              :                     strdup("2024-01-01 08:00"), MSG_FLAG_UNSEEN);
     665            1 :     manifest_upsert(m, "0000000000000020", strdup("B"), strdup("Msg2"),
     666              :                     strdup("2024-01-02 09:00"), MSG_FLAG_UNSEEN);
     667            1 :     manifest_upsert(m, "0000000000000030", strdup("C"), strdup("Msg3"),
     668              :                     strdup("2024-01-03 10:00"), 0 /* read */);
     669            1 :     manifest_save("INBOX", m);
     670              : 
     671              :     /* Initial count: 2 unread, 0 flagged */
     672            1 :     int unread = -1, flagged = -1;
     673            1 :     manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
     674            1 :     ASSERT(unread  == 2, "count_after_update: initial unread is 2");
     675            1 :     ASSERT(flagged == 0, "count_after_update: initial flagged is 0");
     676              : 
     677              :     /* Simulate user pressing 'n' on UID 10 — mark as read */
     678            1 :     ManifestEntry *e = manifest_find(m, "0000000000000010");
     679            1 :     ASSERT(e != NULL, "count_after_update: UID10 found");
     680            1 :     e->flags &= ~MSG_FLAG_UNSEEN;
     681            1 :     manifest_save("INBOX", m);
     682              : 
     683              :     /* Count should now be 1 */
     684            1 :     manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
     685            1 :     ASSERT(unread == 1, "count_after_update: unread drops to 1 after save");
     686              : 
     687              :     /* Mark UID 20 as read too */
     688            1 :     e = manifest_find(m, "0000000000000020");
     689            1 :     e->flags &= ~MSG_FLAG_UNSEEN;
     690            1 :     manifest_save("INBOX", m);
     691              : 
     692            1 :     manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
     693            1 :     ASSERT(unread == 0, "count_after_update: unread drops to 0");
     694              : 
     695              :     /* Flag UID 30 */
     696            1 :     e = manifest_find(m, "0000000000000030");
     697            1 :     e->flags |= MSG_FLAG_FLAGGED;
     698            1 :     manifest_save("INBOX", m);
     699              : 
     700            1 :     manifest_count_all_flags(&unread, &flagged, NULL, NULL, NULL, NULL);
     701            1 :     ASSERT(flagged == 1, "count_after_update: flagged becomes 1");
     702              : 
     703            1 :     manifest_free(m);
     704            1 :     if (old_home) setenv("HOME", old_home, 1);
     705            0 :     else unsetenv("HOME");
     706              : }
     707              : 
     708            1 : void test_flag_search_folder_isolation(void) {
     709            1 :     char *old_home = getenv("HOME");
     710            1 :     setup_test_env("/tmp/email-cli-flag-iso-test");
     711              : 
     712              :     /* Pre-clean: remove all TSV files (stale files from other test runs
     713              :      * would otherwise inflate cnt and make the count assertion fragile) */
     714            1 :     int _clean_rc = system(
     715              :         "find '/tmp/email-cli-flag-iso-test/.local/share/email-cli/"
     716              :         "accounts/testuser/manifests' -name '*.tsv' -delete 2>/dev/null");
     717              :     (void)_clean_rc;   /* best-effort pre-clean; the directory may not exist yet */
     718              : 
     719              :     /* Same UID in two different folders: verify that dedup returns it only once */
     720            1 :     Manifest *inbox = calloc(1, sizeof(Manifest));
     721            1 :     manifest_upsert(inbox, "0000000000000099", strdup("X"), strdup("INBOX copy"),
     722              :                     strdup("2024-06-01 00:00"), MSG_FLAG_UNSEEN);
     723            1 :     manifest_save("INBOX", inbox);
     724            1 :     manifest_free(inbox);
     725              : 
     726            1 :     Manifest *spam = calloc(1, sizeof(Manifest));
     727            1 :     manifest_upsert(spam, "0000000000000099", strdup("X"), strdup("Spam copy"),
     728              :                     strdup("2024-06-01 00:00"), MSG_FLAG_UNSEEN);
     729            1 :     manifest_save("Spam", spam);
     730            1 :     manifest_free(spam);
     731              : 
     732            1 :     SearchResult *res = NULL; int cnt = 0;
     733            1 :     local_flag_search(MSG_FLAG_UNSEEN, &res, &cnt);
     734              :     /* Dedup: same UID across two non-empty folders → only one entry kept */
     735            1 :     int found99 = 0;
     736            1 :     const char *folder99 = NULL;
     737            2 :     for (int i = 0; i < cnt; i++) {
     738            1 :         if (strcmp(res[i].uid, "0000000000000099") == 0) {
     739            1 :             found99++;
     740            1 :             folder99 = res[i].folder;
     741              :         }
     742              :     }
     743            1 :     ASSERT(found99 == 1, "flag_search dedup: UID 99 appears exactly once");
     744            1 :     ASSERT(folder99 && (strcmp(folder99, "INBOX") == 0 || strcmp(folder99, "Spam") == 0),
     745              :            "flag_search dedup: UID 99 folder is INBOX or Spam");
     746            1 :     local_search_free(res, cnt);
     747              : 
     748            1 :     if (old_home) setenv("HOME", old_home, 1);
     749            0 :     else unsetenv("HOME");
     750              : }
     751              : 
     752              : /* ── local_contacts_update tests ─────────────────────────────────────── */
     753              : 
     754              : #define CONTACTS_PATH \
     755              :     "/tmp/email-cli-contacts-test/.local/share/email-cli/accounts/testuser/contacts.tsv"
     756              : 
     757           10 : static void contacts_cleanup(void) { unlink(CONTACTS_PATH); }
     758              : 
     759            2 : static int contacts_count_lines(void) {
     760            2 :     FILE *f = fopen(CONTACTS_PATH, "r");
     761            2 :     if (!f) return 0;
     762            2 :     int n = 0; char line[512];
     763            5 :     while (fgets(line, sizeof(line), f)) n++;
     764            2 :     fclose(f);
     765            2 :     return n;
     766              : }
     767              : 
     768            7 : static int contacts_has_addr(const char *addr) {
     769            7 :     FILE *f = fopen(CONTACTS_PATH, "r");
     770            7 :     if (!f) return 0;
     771            7 :     char line[512]; int found = 0;
     772            9 :     while (fgets(line, sizeof(line), f)) {
     773            9 :         char *tab = strchr(line, '\t');
     774            9 :         if (tab) *tab = '\0';
     775            9 :         char *nl = strchr(line, '\n'); if (nl) *nl = '\0';
     776            9 :         if (strcasecmp(line, addr) == 0) { found = 1; break; }
     777              :     }
     778            7 :     fclose(f);
     779            7 :     return found;
     780              : }
     781              : 
     782            3 : static int contacts_get_freq(const char *addr) {
     783            3 :     FILE *f = fopen(CONTACTS_PATH, "r");
     784            3 :     if (!f) return -1;
     785            3 :     char line[512]; int freq = -1;
     786            3 :     while (fgets(line, sizeof(line), f)) {
     787            3 :         char copy[512]; snprintf(copy, sizeof(copy), "%s", line);
     788            3 :         char *t1 = strchr(copy, '\t');
     789            3 :         if (!t1) continue;
     790            3 :         *t1 = '\0';
     791            3 :         if (strcasecmp(copy, addr) != 0) continue;
     792            3 :         char *t2 = strchr(t1 + 1, '\t');
     793            3 :         if (t2) freq = atoi(t2 + 1);
     794            3 :         break;
     795              :     }
     796            3 :     fclose(f);
     797            3 :     return freq;
     798              : }
     799              : 
     800            2 : static char *contacts_get_name(const char *addr) {
     801            2 :     FILE *f = fopen(CONTACTS_PATH, "r");
     802            2 :     if (!f) return NULL;
     803            2 :     char line[512]; static char cname[256]; cname[0] = '\0';
     804            2 :     while (fgets(line, sizeof(line), f)) {
     805            2 :         char *t1 = strchr(line, '\t');
     806            2 :         if (!t1) continue;
     807            2 :         *t1 = '\0';
     808            2 :         if (strcasecmp(line, addr) != 0) continue;
     809            2 :         char *t2 = strchr(t1 + 1, '\t');
     810            2 :         if (t2) { *t2 = '\0'; snprintf(cname, sizeof(cname), "%s", t1 + 1); }
     811            2 :         break;
     812              :     }
     813            2 :     fclose(f);
     814            2 :     return cname[0] ? cname : NULL;
     815              : }
     816              : 
     817            1 : void test_local_contacts_update(void) {
     818            1 :     char *old_home = getenv("HOME");
     819            1 :     setup_test_env("/tmp/email-cli-contacts-test");
     820            1 :     contacts_cleanup();
     821              : 
     822              :     /* 1. Bare address in From creates entry with freq=1 */
     823            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     824            1 :     ASSERT(contacts_has_addr("alice@example.com"),
     825              :            "contacts: bare From address added");
     826            1 :     ASSERT(contacts_get_freq("alice@example.com") == 1,
     827              :            "contacts: initial frequency is 1");
     828              : 
     829              :     /* 2. Display-name form: addr extracted, name stored */
     830            1 :     contacts_cleanup();
     831            1 :     local_contacts_update("Alice Smith <alice@example.com>", NULL, NULL);
     832            1 :     ASSERT(contacts_has_addr("alice@example.com"),
     833              :            "contacts: addr extracted from display-name form");
     834            1 :     char *cn = contacts_get_name("alice@example.com");
     835            1 :     ASSERT(cn && strcmp(cn, "Alice Smith") == 0,
     836              :            "contacts: display name stored correctly");
     837              : 
     838              :     /* 3. Multiple comma-separated addresses in To */
     839            1 :     contacts_cleanup();
     840            1 :     local_contacts_update(NULL, "alice@example.com, bob@example.com", NULL);
     841            1 :     ASSERT(contacts_has_addr("alice@example.com"), "contacts: To addr1 added");
     842            1 :     ASSERT(contacts_has_addr("bob@example.com"),   "contacts: To addr2 added");
     843            1 :     ASSERT(contacts_count_lines() == 2,            "contacts: 2 entries total");
     844              : 
     845              :     /* 4. Cc addresses are also collected */
     846            1 :     contacts_cleanup();
     847            1 :     local_contacts_update(NULL, NULL, "carol@example.com");
     848            1 :     ASSERT(contacts_has_addr("carol@example.com"), "contacts: Cc addr added");
     849              : 
     850              :     /* 5. Frequency increments on repeated calls */
     851            1 :     contacts_cleanup();
     852            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     853            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     854            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     855            1 :     ASSERT(contacts_get_freq("alice@example.com") == 3,
     856              :            "contacts: frequency increments to 3 on 3 calls");
     857              : 
     858              :     /* 6. Case-insensitive deduplication */
     859            1 :     contacts_cleanup();
     860            1 :     local_contacts_update("ALICE@EXAMPLE.COM", NULL, NULL);
     861            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     862            1 :     ASSERT(contacts_count_lines() == 1,
     863              :            "contacts: case-insensitive dedup — only 1 entry");
     864            1 :     ASSERT(contacts_get_freq("alice@example.com") == 2,
     865              :            "contacts: case-insensitive freq increment");
     866              : 
     867              :     /* 7. Name updated when initially absent */
     868            1 :     contacts_cleanup();
     869            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     870            1 :     local_contacts_update("Alice Smith <alice@example.com>", NULL, NULL);
     871            1 :     char *cn2 = contacts_get_name("alice@example.com");
     872            1 :     ASSERT(cn2 && strcmp(cn2, "Alice Smith") == 0,
     873              :            "contacts: name updated when initially missing");
     874              : 
     875              :     /* 8. Most frequent addr appears first in file */
     876            1 :     contacts_cleanup();
     877            1 :     local_contacts_update("bob@example.com", NULL, NULL);
     878            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     879            1 :     local_contacts_update("alice@example.com", NULL, NULL);
     880              :     {
     881            1 :         FILE *cf = fopen(CONTACTS_PATH, "r");
     882            1 :         ASSERT(cf != NULL, "contacts: file exists after updates");
     883            1 :         if (cf) {
     884            1 :             char fline[256]; fline[0] = '\0';
     885            1 :             if (fgets(fline, sizeof(fline), cf) == NULL) fline[0] = '\0';
     886            1 :             fclose(cf);
     887            1 :             char *tab = strchr(fline, '\t'); if (tab) *tab = '\0';
     888            1 :             ASSERT(strcasecmp(fline, "alice@example.com") == 0,
     889              :                    "contacts: most frequent addr is first");
     890              :         }
     891              :     }
     892              : 
     893              :     /* 9. NULL headers are safe (no crash) */
     894            1 :     contacts_cleanup();
     895            1 :     local_contacts_update(NULL, NULL, NULL);
     896            1 :     ASSERT(1, "contacts: all-NULL headers do not crash");
     897              : 
     898              :     /* 10. Semicolon-separated addresses parsed */
     899            1 :     contacts_cleanup();
     900            1 :     local_contacts_update(NULL, "dave@example.com; eve@example.com", NULL);
     901            1 :     ASSERT(contacts_has_addr("dave@example.com"), "contacts: semicolon sep addr1");
     902            1 :     ASSERT(contacts_has_addr("eve@example.com"),  "contacts: semicolon sep addr2");
     903              : 
     904            1 :     if (old_home) setenv("HOME", old_home, 1);
     905            0 :     else unsetenv("HOME");
     906              : }
     907              : 
     908              : /* ── local_search (text/subject/from search) ──────────────────────────── */
     909              : 
     910            1 : void test_local_search(void) {
     911            1 :     char *old_home = getenv("HOME");
     912            1 :     setup_test_env("/tmp/email-cli-search-test");
     913              : 
     914              :     /* Build a manifest with two messages */
     915            1 :     Manifest *m = calloc(1, sizeof(Manifest));
     916            1 :     manifest_upsert(m, "0000000000000001", strdup("alice@example.com"),
     917              :                     strdup("Hello World subject"), strdup("2024-01-01 10:00"), 0);
     918            1 :     manifest_upsert(m, "0000000000000002", strdup("bob@other.org"),
     919              :                     strdup("Different topic"), strdup("2024-01-02 11:00"), MSG_FLAG_UNSEEN);
     920            1 :     manifest_save("INBOX", m);
     921            1 :     manifest_free(m);
     922              : 
     923            1 :     SearchResult *res = NULL;
     924            1 :     int cnt = 0;
     925              : 
     926              :     /* scope=0: subject search — "Hello" matches UID1 */
     927            1 :     int rc = local_search("Hello", 0, &res, &cnt);
     928            1 :     ASSERT(rc == 0,  "local_search subject: returns 0");
     929            1 :     ASSERT(cnt == 1, "local_search subject: 1 result");
     930            1 :     if (cnt == 1) {
     931            1 :         ASSERT(strcmp(res[0].uid, "0000000000000001") == 0, "local_search subject: UID1");
     932            1 :         ASSERT(strcmp(res[0].folder, "INBOX") == 0, "local_search subject: INBOX");
     933              :     }
     934            1 :     local_search_free(res, cnt);
     935              : 
     936              :     /* scope=0: case-insensitive */
     937            1 :     res = NULL; cnt = 0;
     938            1 :     local_search("hello", 0, &res, &cnt);
     939            1 :     ASSERT(cnt == 1, "local_search case-insensitive: 1 result");
     940            1 :     local_search_free(res, cnt);
     941              : 
     942              :     /* scope=1: from search — "alice" matches UID1 */
     943            1 :     res = NULL; cnt = 0;
     944            1 :     local_search("alice", 1, &res, &cnt);
     945            1 :     ASSERT(cnt == 1, "local_search from: 1 result");
     946            1 :     if (cnt == 1)
     947            1 :         ASSERT(strcmp(res[0].uid, "0000000000000001") == 0, "local_search from: UID1");
     948            1 :     local_search_free(res, cnt);
     949              : 
     950              :     /* scope=0: no match → 0 results */
     951            1 :     res = NULL; cnt = 0;
     952            1 :     local_search("ZZZNOMATCH99", 0, &res, &cnt);
     953            1 :     ASSERT(cnt == 0, "local_search no match: 0 results");
     954            1 :     local_search_free(res, cnt);
     955              : 
     956              :     /* NULL / empty query → 0 results, no crash */
     957            1 :     res = NULL; cnt = 0;
     958            1 :     local_search(NULL, 0, &res, &cnt);
     959            1 :     ASSERT(cnt == 0, "local_search NULL query: safe");
     960            1 :     res = NULL; cnt = 0;
     961            1 :     local_search("", 0, &res, &cnt);
     962            1 :     ASSERT(cnt == 0, "local_search empty query: safe");
     963              : 
     964            1 :     if (old_home) setenv("HOME", old_home, 1);
     965            0 :     else unsetenv("HOME");
     966              : }
     967              : 
     968              : /* ── local_contacts_rebuild ───────────────────────────────────────────── */
     969              : 
     970            1 : void test_local_contacts_rebuild(void) {
     971            1 :     char *old_home = getenv("HOME");
     972            1 :     setup_test_env("/tmp/email-cli-rebuild-test");
     973              : 
     974              :     /* Save a message header with From/To so rebuild can extract contacts */
     975            1 :     const char *hdr = "From: Carol <carol@rebuild.test>\r\nTo: dave@rebuild.test\r\n"
     976              :                       "Subject: Test\r\nDate: Mon, 1 Jan 2024 10:00:00 +0000\r\n";
     977            1 :     local_hdr_save("INBOX", "0000000000000001", hdr, strlen(hdr));
     978              : 
     979              :     /* Save folder list so local_contacts_rebuild can find INBOX */
     980            1 :     const char *flist[] = { "INBOX", NULL };
     981            1 :     local_folder_list_save(flist, 1, '/');
     982              : 
     983              :     /* Run rebuild — should not crash, creates contacts file */
     984            1 :     local_contacts_rebuild();
     985              : 
     986              :     /* Check the contacts file at the correct path for this test (not CONTACTS_PATH
     987              :      * which is hardcoded to the contacts-update test directory). */
     988            1 :     const char *ctacts_file =
     989              :         "/tmp/email-cli-rebuild-test/.local/share/email-cli/accounts/testuser/contacts.tsv";
     990            1 :     FILE *cf = fopen(ctacts_file, "r");
     991            1 :     ASSERT(cf != NULL, "contacts_rebuild: contacts file created");
     992            1 :     int found = 0;
     993              :     char cline[256];
     994            1 :     while (fgets(cline, sizeof(cline), cf)) {
     995            1 :         if (strstr(cline, "carol@rebuild.test")) { found = 1; break; }
     996              :     }
     997            1 :     fclose(cf);
     998            1 :     ASSERT(found, "contacts_rebuild: From addr present");
     999              : 
    1000            1 :     if (old_home) setenv("HOME", old_home, 1);
    1001            0 :     else unsetenv("HOME");
    1002              : }
    1003              : 
    1004              : /* ── local_pending_append_* ───────────────────────────────────────────── */
    1005              : 
    1006            1 : void test_local_pending_append(void) {
    1007            1 :     char *old_home = getenv("HOME");
    1008            1 :     setup_test_env("/tmp/email-cli-pending-append-test");
    1009              :     /* Remove leftover from previous test runs */
    1010            1 :     unlink("/tmp/email-cli-pending-append-test/.local/share/email-cli/accounts/testuser/pending_appends.tsv");
    1011              : 
    1012            1 :     int rc1 = local_pending_append_add("Sent",   "0000000000000042");
    1013            1 :     int rc2 = local_pending_append_add("Drafts", "0000000000000043");
    1014            1 :     ASSERT(rc1 == 0, "pending_append_add: first entry ok");
    1015            1 :     ASSERT(rc2 == 0, "pending_append_add: second entry ok");
    1016              : 
    1017            1 :     int cnt = 0;
    1018            1 :     PendingAppend *pa = local_pending_append_load(&cnt);
    1019            1 :     ASSERT(cnt == 2, "pending_append_load: 2 entries");
    1020            1 :     if (cnt >= 2) {
    1021            1 :         ASSERT(strcmp(pa[0].folder, "Sent")   == 0, "pending_append: first folder");
    1022            1 :         ASSERT(strcmp(pa[0].uid, "0000000000000042") == 0, "pending_append: first uid");
    1023            1 :         ASSERT(strcmp(pa[1].folder, "Drafts") == 0, "pending_append: second folder");
    1024              :     }
    1025            1 :     free(pa);
    1026              : 
    1027            1 :     local_pending_append_remove("Sent", "0000000000000042");
    1028            1 :     cnt = 0;
    1029            1 :     pa = local_pending_append_load(&cnt);
    1030            1 :     ASSERT(cnt == 1, "pending_append_remove: 1 entry left");
    1031            1 :     if (cnt == 1)
    1032            1 :         ASSERT(strcmp(pa[0].folder, "Drafts") == 0, "pending_append_remove: Drafts left");
    1033            1 :     free(pa);
    1034              : 
    1035              :     /* no crash on removing non-existent entry */
    1036            1 :     local_pending_append_remove("INBOX", "0000000000000099");
    1037              : 
    1038            1 :     if (old_home) setenv("HOME", old_home, 1);
    1039            0 :     else unsetenv("HOME");
    1040              : }
    1041              : 
    1042              : /* ── local_pending_fetch_* ────────────────────────────────────────────── */
    1043              : 
    1044            1 : void test_local_pending_fetch(void) {
    1045            1 :     char *old_home = getenv("HOME");
    1046            1 :     setup_test_env("/tmp/email-cli-pending-fetch-test");
    1047              : 
    1048            1 :     ASSERT(local_pending_fetch_count() == 0, "pending_fetch: initially 0");
    1049              : 
    1050            1 :     local_pending_fetch_add("0000000000000010");
    1051            1 :     local_pending_fetch_add("0000000000000011");
    1052            1 :     ASSERT(local_pending_fetch_count() == 2, "pending_fetch_add: count 2");
    1053              : 
    1054            1 :     int cnt = 0;
    1055            1 :     char (*uids)[17] = local_pending_fetch_load(&cnt);
    1056            1 :     ASSERT(cnt == 2, "pending_fetch_load: 2 entries");
    1057            1 :     free(uids);
    1058              : 
    1059            1 :     local_pending_fetch_remove("0000000000000010");
    1060            1 :     ASSERT(local_pending_fetch_count() == 1, "pending_fetch_remove: count 1");
    1061              : 
    1062            1 :     local_pending_fetch_clear();
    1063            1 :     ASSERT(local_pending_fetch_count() == 0, "pending_fetch_clear: count 0");
    1064              : 
    1065            1 :     local_pending_fetch_add(NULL);
    1066            1 :     ASSERT(local_pending_fetch_count() == 0, "pending_fetch_add NULL: safe");
    1067              : 
    1068            1 :     if (old_home) setenv("HOME", old_home, 1);
    1069            0 :     else unsetenv("HOME");
    1070              : }
    1071              : 
    1072              : /* ── local_save_outgoing ──────────────────────────────────────────────── */
    1073              : 
    1074            1 : void test_local_save_outgoing(void) {
    1075            1 :     char *old_home = getenv("HOME");
    1076            1 :     setup_test_env("/tmp/email-cli-outgoing-test");
    1077              :     /* Remove leftover from previous test runs */
    1078            1 :     unlink("/tmp/email-cli-outgoing-test/.local/share/email-cli/accounts/testuser/pending_appends.tsv");
    1079              : 
    1080            1 :     const char *msg = "From: me@test.local\r\nTo: you@test.local\r\n"
    1081              :                       "Subject: test outgoing\r\n\r\nHello!\r\n";
    1082            1 :     size_t mlen = strlen(msg);
    1083              : 
    1084            1 :     int rc = local_save_outgoing("Sent", msg, mlen);
    1085            1 :     ASSERT(rc == 0, "local_save_outgoing: returns 0");
    1086              : 
    1087            1 :     int cnt = 0;
    1088            1 :     PendingAppend *pa = local_pending_append_load(&cnt);
    1089            1 :     ASSERT(cnt == 1, "local_save_outgoing: pending append queued");
    1090            1 :     if (cnt == 1)
    1091            1 :         ASSERT(strcmp(pa[0].folder, "Sent") == 0, "local_save_outgoing: Sent folder");
    1092            1 :     free(pa);
    1093              : 
    1094              :     /* NULL folder should not crash */
    1095            1 :     local_save_outgoing(NULL, msg, mlen);
    1096              : 
    1097            1 :     if (old_home) setenv("HOME", old_home, 1);
    1098            0 :     else unsetenv("HOME");
    1099              : }
    1100              : 
    1101            1 : void test_manifest_dmarc_flags(void) {
    1102            1 :     char *old_home = getenv("HOME");
    1103            1 :     setup_test_env("/tmp/email-cli-dmarc-manifest-test");
    1104              : 
    1105            1 :     unlink("/tmp/email-cli-dmarc-manifest-test/.local/share/email-cli/"
    1106              :            "accounts/testuser/manifests/INBOX.tsv");
    1107              : 
    1108            1 :     Manifest *m = calloc(1, sizeof(Manifest));
    1109            1 :     ASSERT(m != NULL, "dmarc manifest: calloc");
    1110              : 
    1111              :     /* 1. DMARC pass + checked */
    1112            1 :     manifest_upsert(m, "0000000000000001", strdup("Alice"), strdup("Pass msg"),
    1113              :                     strdup("2025-01-01 10:00"),
    1114              :                     MSG_FLAG_DMARC_PASS | MSG_FLAG_DMARC_CHECKED);
    1115              :     /* 2. DMARC fail + checked */
    1116            1 :     manifest_upsert(m, "0000000000000002", strdup("Bob"), strdup("Fail msg"),
    1117              :                     strdup("2025-01-02 10:00"),
    1118              :                     MSG_FLAG_DMARC_FAIL | MSG_FLAG_DMARC_CHECKED);
    1119              :     /* 3. Checked but no pass/fail (no Authentication-Results in header) */
    1120            1 :     manifest_upsert(m, "0000000000000003", strdup("Carol"), strdup("No AR msg"),
    1121              :                     strdup("2025-01-03 10:00"),
    1122              :                     MSG_FLAG_DMARC_CHECKED);
    1123              :     /* 4. Combined: UNSEEN + DMARC_PASS + DMARC_CHECKED */
    1124            1 :     manifest_upsert(m, "0000000000000004", strdup("Dave"), strdup("Unread pass"),
    1125              :                     strdup("2025-01-04 10:00"),
    1126              :                     MSG_FLAG_UNSEEN | MSG_FLAG_DMARC_PASS | MSG_FLAG_DMARC_CHECKED);
    1127              :     /* 5. PASS and FAIL not set simultaneously: only PASS wins */
    1128            1 :     manifest_upsert(m, "0000000000000005", strdup("Eve"), strdup("Pass checked"),
    1129              :                     strdup("2025-01-05 10:00"),
    1130              :                     MSG_FLAG_DMARC_PASS | MSG_FLAG_DMARC_CHECKED | MSG_FLAG_FLAGGED);
    1131              : 
    1132            1 :     ASSERT(manifest_save("INBOX", m) == 0, "dmarc manifest: save succeeds");
    1133            1 :     manifest_free(m);
    1134              : 
    1135            1 :     m = manifest_load("INBOX");
    1136            1 :     ASSERT(m != NULL, "dmarc manifest: load not NULL");
    1137            1 :     ASSERT(m->count == 5, "dmarc manifest: 5 entries");
    1138              : 
    1139            1 :     ManifestEntry *e1 = manifest_find(m, "0000000000000001");
    1140            1 :     ASSERT(e1 != NULL, "dmarc manifest: UID1 found");
    1141            1 :     ASSERT( (e1->flags & MSG_FLAG_DMARC_PASS),    "dmarc manifest: UID1 DMARC_PASS set");
    1142            1 :     ASSERT( (e1->flags & MSG_FLAG_DMARC_CHECKED), "dmarc manifest: UID1 DMARC_CHECKED set");
    1143            1 :     ASSERT(!(e1->flags & MSG_FLAG_DMARC_FAIL),    "dmarc manifest: UID1 DMARC_FAIL clear");
    1144              : 
    1145            1 :     ManifestEntry *e2 = manifest_find(m, "0000000000000002");
    1146            1 :     ASSERT(e2 != NULL, "dmarc manifest: UID2 found");
    1147            1 :     ASSERT( (e2->flags & MSG_FLAG_DMARC_FAIL),    "dmarc manifest: UID2 DMARC_FAIL set");
    1148            1 :     ASSERT( (e2->flags & MSG_FLAG_DMARC_CHECKED), "dmarc manifest: UID2 DMARC_CHECKED set");
    1149            1 :     ASSERT(!(e2->flags & MSG_FLAG_DMARC_PASS),    "dmarc manifest: UID2 DMARC_PASS clear");
    1150              : 
    1151            1 :     ManifestEntry *e3 = manifest_find(m, "0000000000000003");
    1152            1 :     ASSERT(e3 != NULL, "dmarc manifest: UID3 found");
    1153            1 :     ASSERT( (e3->flags & MSG_FLAG_DMARC_CHECKED), "dmarc manifest: UID3 DMARC_CHECKED set");
    1154            1 :     ASSERT(!(e3->flags & MSG_FLAG_DMARC_PASS),    "dmarc manifest: UID3 DMARC_PASS clear");
    1155            1 :     ASSERT(!(e3->flags & MSG_FLAG_DMARC_FAIL),    "dmarc manifest: UID3 DMARC_FAIL clear");
    1156              : 
    1157            1 :     ManifestEntry *e4 = manifest_find(m, "0000000000000004");
    1158            1 :     ASSERT(e4 != NULL, "dmarc manifest: UID4 found");
    1159            1 :     ASSERT( (e4->flags & MSG_FLAG_UNSEEN),        "dmarc manifest: UID4 UNSEEN set");
    1160            1 :     ASSERT( (e4->flags & MSG_FLAG_DMARC_PASS),    "dmarc manifest: UID4 DMARC_PASS set");
    1161            1 :     ASSERT( (e4->flags & MSG_FLAG_DMARC_CHECKED), "dmarc manifest: UID4 DMARC_CHECKED set");
    1162              : 
    1163            1 :     ManifestEntry *e5 = manifest_find(m, "0000000000000005");
    1164            1 :     ASSERT(e5 != NULL, "dmarc manifest: UID5 found");
    1165            1 :     ASSERT( (e5->flags & MSG_FLAG_FLAGGED),       "dmarc manifest: UID5 FLAGGED set");
    1166            1 :     ASSERT( (e5->flags & MSG_FLAG_DMARC_PASS),    "dmarc manifest: UID5 DMARC_PASS set");
    1167            1 :     ASSERT( (e5->flags & MSG_FLAG_DMARC_CHECKED), "dmarc manifest: UID5 DMARC_CHECKED set");
    1168              : 
    1169            1 :     manifest_free(m);
    1170              : 
    1171            1 :     if (old_home) setenv("HOME", old_home, 1);
    1172            0 :     else unsetenv("HOME");
    1173              : }
    1174              : 
    1175              : /* ── Cross-folder UID lookup ─────────────────────────────────────────── */
    1176              : 
    1177              : /* The lookup enumerates manifests, so a folder only counts once it has one —
    1178              :  * mirror what a real sync produces. */
    1179            5 : static void ff_add(const char *folder, const char *uid, const char *body) {
    1180            5 :     local_msg_save(folder, uid, body, strlen(body));
    1181            5 :     const char *base = "/tmp/email-cli-find-folders-test/.local/share/"
    1182              :                        "email-cli/accounts/testuser/manifests";
    1183            5 :     fs_mkdir_p(base, 0700);
    1184              :     char path[512];
    1185            5 :     snprintf(path, sizeof(path), "%s/%s.tsv", base, folder);
    1186            5 :     FILE *f = fopen(path, "w");
    1187            5 :     ASSERT(f != NULL, "find_folders fixture: manifest must be writable");
    1188            5 :     if (f) {
    1189            5 :         fprintf(f, "%s\ta@x\tS\t2024-01-01 10:00\t0\n", uid);
    1190            5 :         fclose(f);
    1191              :     }
    1192              : }
    1193              : 
    1194            1 : void test_local_msg_find_folders(void) {
    1195              :     /* Start from a clean tree: leftovers from an earlier run would change the
    1196              :      * folder counts asserted below. */
    1197            1 :     if (system("rm -rf '/tmp/email-cli-find-folders-test'") != 0) { /* best effort */ }
    1198            1 :     setup_test_env("/tmp/email-cli-find-folders-test");
    1199              : 
    1200            1 :     const char *uid  = "0000000000000837";
    1201            1 :     const char *body = "From: a@x\r\nSubject: S\r\n\r\nb";
    1202              : 
    1203              :     /* Not stored anywhere yet. */
    1204            1 :     char **f = NULL; int n = -1;
    1205            1 :     local_msg_find_folders(uid, &f, &n);
    1206            1 :     ASSERT(n == 0, "find_folders: 0 when the UID is nowhere");
    1207            1 :     ASSERT(f == NULL, "find_folders: no list when the UID is nowhere");
    1208              : 
    1209              :     /* One folder — the caller needs no --folder. */
    1210            1 :     ff_add("vasarlas", uid, body);
    1211            1 :     local_msg_find_folders(uid, &f, &n);
    1212            1 :     ASSERT(n == 1, "find_folders: 1 for a single-folder UID");
    1213            1 :     ASSERT(n == 1 && strcmp(f[0], "vasarlas") == 0, "find_folders: names that folder");
    1214            1 :     local_folder_list_free(f, n); f = NULL;
    1215              : 
    1216              :     /* Several folders — every candidate is reported, sorted, so the message
    1217              :      * and the caller's choice do not depend on readdir order. */
    1218            1 :     ff_add("Trash", uid, body);
    1219            1 :     ff_add("Archive", uid, body);
    1220            1 :     local_msg_find_folders(uid, &f, &n);
    1221            1 :     ASSERT(n == 3, "find_folders: 3 when three folders hold the UID");
    1222            1 :     ASSERT(n == 3 && strcmp(f[0], "Archive") == 0, "find_folders: sorted, Archive first");
    1223            1 :     ASSERT(n == 3 && strcmp(f[1], "Trash") == 0,   "find_folders: sorted, Trash second");
    1224            1 :     ASSERT(n == 3 && strcmp(f[2], "vasarlas") == 0, "find_folders: sorted, vasarlas last");
    1225            1 :     local_folder_list_free(f, n); f = NULL;
    1226              : 
    1227              :     /* RFC 3501: INBOX is case-insensitive, so "Inbox" and "INBOX" are one
    1228              :      * mailbox and must not look like two conflicting locations. */
    1229            1 :     ff_add("INBOX", uid, body);
    1230            1 :     ff_add("Inbox", uid, body);
    1231            1 :     local_msg_find_folders(uid, &f, &n);
    1232            1 :     ASSERT(n == 4, "find_folders: Inbox/INBOX counted once, not twice");
    1233            1 :     int inbox_seen = 0;
    1234            5 :     for (int i = 0; i < n; i++)
    1235            4 :         if (strcasecmp(f[i], "INBOX") == 0) inbox_seen++;
    1236            1 :     ASSERT(inbox_seen == 1, "find_folders: exactly one INBOX entry");
    1237            1 :     local_folder_list_free(f, n); f = NULL;
    1238              : 
    1239              :     /* A NULL/empty UID must not report matches. */
    1240            1 :     local_msg_find_folders("", &f, &n);
    1241            1 :     ASSERT(n == 0, "find_folders: empty UID yields nothing");
    1242            1 :     local_msg_find_folders(NULL, &f, &n);
    1243            1 :     ASSERT(n == 0, "find_folders: NULL UID yields nothing");
    1244              : 
    1245              :     /* Freeing an empty list is a no-op, not a crash. */
    1246            1 :     local_folder_list_free(NULL, 0);
    1247              : }
        

Generated by: LCOV version 2.0-1