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