LCOV - code coverage report
Current view: top level - tests/unit - test_updates_state_store.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 96.5 % 86 83
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 7 7

            Line data    Source code
       1              : /**
       2              :  * @file test_updates_state_store.c
       3              :  * @brief Unit tests for updates_state_load / updates_state_save.
       4              :  *
       5              :  * These tests do NOT go through platform_cache_dir(); instead they exercise
       6              :  * the INI read/write logic by writing temporary files directly and calling
       7              :  * the public API with a custom HOME so that platform_cache_dir() resolves
       8              :  * to a directory we control.
       9              :  */
      10              : 
      11              : #include "test_helpers.h"
      12              : #include "infrastructure/updates_state_store.h"
      13              : #include "fs_util.h"
      14              : 
      15              : #include <stdio.h>
      16              : #include <stdlib.h>
      17              : #include <string.h>
      18              : #include <unistd.h>
      19              : #include <sys/stat.h>
      20              : #include <sys/wait.h>
      21              : 
      22              : /* ---- Helpers ------------------------------------------------------------ */
      23              : 
      24              : /** Set HOME to a writable temp directory and return it (caller must free). */
      25            4 : static char *setup_tmp_home(void) {
      26            4 :     char tmpl[] = "/tmp/tg-cli-state-test-XXXXXX";
      27            4 :     char *dir = mkdtemp(tmpl);
      28            4 :     if (!dir) return NULL;
      29            4 :     char *copy = strdup(dir);
      30            4 :     setenv("HOME", copy, 1);
      31              :     /* Also clear XDG_CACHE_HOME so platform_cache_dir() uses $HOME/.cache */
      32            4 :     unsetenv("XDG_CACHE_HOME");
      33            4 :     return copy;
      34              : }
      35              : 
      36              : /** Remove directory tree (shallow — we only create one level of files). */
      37            4 : static void rm_rf(const char *dir) {
      38              :     /* Use execvp to avoid shell quoting and snprintf size issues. */
      39            4 :     char *args[] = { "rm", "-rf", (char *)dir, NULL };
      40            4 :     pid_t pid = fork();
      41            8 :     if (pid == 0) {
      42            4 :         execvp("rm", args);
      43            4 :         _exit(1);
      44            4 :     } else if (pid > 0) {
      45              :         int status;
      46            4 :         waitpid(pid, &status, 0);
      47              :     }
      48            4 : }
      49              : 
      50              : /* ---- Tests -------------------------------------------------------------- */
      51              : 
      52              : /** Happy path: save then load round-trips all fields. */
      53            1 : static void test_save_load_roundtrip(void) {
      54            1 :     char *home = setup_tmp_home();
      55            1 :     ASSERT(home != NULL, "mkdtemp must succeed");
      56              : 
      57            1 :     UpdatesState orig = {
      58              :         .pts          = 12345,
      59              :         .qts          = 67,
      60              :         .date         = 1700000000,
      61              :         .seq          = 99,
      62              :         .unread_count = 7,
      63              :     };
      64              : 
      65            1 :     int rc = updates_state_save(&orig);
      66            1 :     ASSERT(rc == 0, "save must succeed");
      67              : 
      68              :     /* Verify the file was created. */
      69              :     char path[512];
      70            1 :     snprintf(path, sizeof(path), "%s/.cache/tg-cli/updates.state", home);
      71            1 :     ASSERT(access(path, F_OK) == 0, "state file must exist after save");
      72              : 
      73              :     /* Verify mode 0600. */
      74              :     struct stat st;
      75            1 :     ASSERT(stat(path, &st) == 0, "stat must succeed");
      76            1 :     ASSERT((st.st_mode & 0777) == 0600, "state file must have mode 0600");
      77              : 
      78            1 :     UpdatesState loaded = {0};
      79            1 :     rc = updates_state_load(&loaded);
      80            1 :     ASSERT(rc == 0, "load must succeed");
      81              : 
      82            1 :     ASSERT(loaded.pts  == orig.pts,  "pts round-trips");
      83            1 :     ASSERT(loaded.qts  == orig.qts,  "qts round-trips");
      84            1 :     ASSERT(loaded.date == orig.date, "date round-trips");
      85            1 :     ASSERT(loaded.seq  == orig.seq,  "seq round-trips");
      86            1 :     ASSERT(loaded.unread_count == orig.unread_count, "unread_count round-trips");
      87              : 
      88            1 :     rm_rf(home);
      89            1 :     free(home);
      90              : }
      91              : 
      92              : /** Missing file returns -1 (not a hard error). */
      93            1 : static void test_load_missing_file(void) {
      94            1 :     char *home = setup_tmp_home();
      95            1 :     ASSERT(home != NULL, "mkdtemp must succeed");
      96              : 
      97              :     /* Do NOT create the state file. */
      98            1 :     UpdatesState out = {0};
      99            1 :     int rc = updates_state_load(&out);
     100            1 :     ASSERT(rc == -1, "load of missing file must return -1");
     101              : 
     102            1 :     rm_rf(home);
     103            1 :     free(home);
     104              : }
     105              : 
     106              : /** Overwrite: a second save replaces the first. */
     107            1 : static void test_save_overwrites(void) {
     108            1 :     char *home = setup_tmp_home();
     109            1 :     ASSERT(home != NULL, "mkdtemp must succeed");
     110              : 
     111            1 :     UpdatesState first  = { .pts=100, .qts=1, .date=1000, .seq=5 };
     112            1 :     UpdatesState second = { .pts=200, .qts=2, .date=2000, .seq=6 };
     113              : 
     114            1 :     ASSERT(updates_state_save(&first)  == 0, "first save ok");
     115            1 :     ASSERT(updates_state_save(&second) == 0, "second save ok");
     116              : 
     117            1 :     UpdatesState loaded = {0};
     118            1 :     ASSERT(updates_state_load(&loaded) == 0, "load after overwrite ok");
     119            1 :     ASSERT(loaded.pts == 200, "pts reflects second save");
     120            1 :     ASSERT(loaded.seq == 6,   "seq reflects second save");
     121              : 
     122            1 :     rm_rf(home);
     123            1 :     free(home);
     124              : }
     125              : 
     126              : /** Corrupt file (no '=' separator) returns -2. */
     127            1 : static void test_load_corrupt_file(void) {
     128            1 :     char *home = setup_tmp_home();
     129            1 :     ASSERT(home != NULL, "mkdtemp must succeed");
     130              : 
     131              :     /* Create directory and write a bad file. */
     132            1 :     char *dir  = NULL;
     133            1 :     char *path = NULL;
     134            2 :     if (asprintf(&dir,  "%s/.cache/tg-cli",          home) == -1 ||
     135            1 :         asprintf(&path, "%s/.cache/tg-cli/updates.state", home) == -1) {
     136            0 :         free(dir);
     137            0 :         rm_rf(home); free(home);
     138            0 :         return;
     139              :     }
     140            1 :     fs_mkdir_p(dir, 0700);
     141            1 :     free(dir);
     142              : 
     143            1 :     FILE *fp = fopen(path, "w");
     144            1 :     free(path);
     145            1 :     ASSERT(fp != NULL, "can create corrupt file");
     146            1 :     fprintf(fp, "this is not valid ini\n");
     147            1 :     fclose(fp);
     148              : 
     149            1 :     UpdatesState out = {0};
     150            1 :     int rc = updates_state_load(&out);
     151            1 :     ASSERT(rc == -2, "corrupt file must return -2");
     152              : 
     153            1 :     rm_rf(home);
     154            1 :     free(home);
     155              : }
     156              : 
     157              : /* ---- Entry point -------------------------------------------------------- */
     158              : 
     159            1 : void run_updates_state_store_tests(void) {
     160            1 :     RUN_TEST(test_save_load_roundtrip);
     161            1 :     RUN_TEST(test_load_missing_file);
     162            1 :     RUN_TEST(test_save_overwrites);
     163            1 :     RUN_TEST(test_load_corrupt_file);
     164            1 : }
        

Generated by: LCOV version 2.0-1