Line data Source code
1 : /**
2 : * @file test_watch_json.c
3 : * @brief Unit tests for FEAT-04: --json flag on `watch` subcommand.
4 : *
5 : * Tests cover:
6 : * 1. Argument parsing: `watch --json` sets args.json = 1.
7 : * 2. JSON string escaping via json_escape_str().
8 : */
9 :
10 : #include "test_helpers.h"
11 : #include "arg_parse.h"
12 : #include "json_util.h"
13 :
14 : #include <string.h>
15 : #include <stdlib.h>
16 :
17 : /* ---- Test: `--json watch` (global flag before subcommand) ---- */
18 1 : static void test_watch_json_global_flag(void) {
19 1 : char *argv[] = {"tg-cli", "--json", "watch", NULL};
20 : ArgResult r;
21 1 : int rc = arg_parse(3, argv, &r);
22 1 : ASSERT(rc == ARG_OK, "--json watch: must return ARG_OK");
23 1 : ASSERT(r.command == CMD_WATCH, "--json watch: command must be CMD_WATCH");
24 1 : ASSERT(r.json == 1, "--json watch: json flag must be 1");
25 : }
26 :
27 : /* ---- Test: `watch` without --json keeps json=0 ---- */
28 1 : static void test_watch_no_json_flag(void) {
29 1 : char *argv[] = {"tg-cli", "watch", NULL};
30 : ArgResult r;
31 1 : int rc = arg_parse(2, argv, &r);
32 1 : ASSERT(rc == ARG_OK, "watch (no --json): must return ARG_OK");
33 1 : ASSERT(r.command == CMD_WATCH, "watch (no --json): command must be CMD_WATCH");
34 1 : ASSERT(r.json == 0, "watch (no --json): json flag must be 0");
35 : }
36 :
37 : /* ---- Test: `--json watch --peers X --interval 5` combined ---- */
38 1 : static void test_watch_json_with_peers_and_interval(void) {
39 1 : char *argv[] = {"tg-cli", "--json", "watch", "--peers", "@news",
40 : "--interval", "10", NULL};
41 : ArgResult r;
42 1 : int rc = arg_parse(7, argv, &r);
43 1 : ASSERT(rc == ARG_OK, "watch json+peers+interval: ARG_OK");
44 1 : ASSERT(r.command == CMD_WATCH, "watch json+peers+interval: CMD_WATCH");
45 1 : ASSERT(r.json == 1, "watch json+peers+interval: json=1");
46 1 : ASSERT(r.watch_interval == 10, "watch json+peers+interval: interval=10");
47 1 : ASSERT(r.watch_peers != NULL, "watch json+peers+interval: watch_peers set");
48 1 : ASSERT(strcmp(r.watch_peers, "@news") == 0, "watch json+peers+interval: watch_peers=@news");
49 : }
50 :
51 : /* ---- Test: json_escape_str — plain ASCII passes through unchanged ---- */
52 1 : static void test_json_escape_plain(void) {
53 : char buf[64];
54 1 : size_t n = json_escape_str(buf, sizeof(buf), "hello world");
55 1 : ASSERT(strcmp(buf, "hello world") == 0, "json_escape plain: output matches");
56 1 : ASSERT(n == 11, "json_escape plain: length correct");
57 : }
58 :
59 : /* ---- Test: json_escape_str — double-quote and backslash are escaped ---- */
60 1 : static void test_json_escape_special_chars(void) {
61 : char buf[64];
62 : /* Input: say "hi"\path → say \"hi\"\\path */
63 1 : json_escape_str(buf, sizeof(buf), "say \"hi\"\\path");
64 1 : ASSERT(strcmp(buf, "say \\\"hi\\\"\\\\path") == 0,
65 : "json_escape special: quotes and backslashes escaped");
66 : }
67 :
68 : /* ---- Test: json_escape_str — control characters use shorthand/unicode ---- */
69 1 : static void test_json_escape_control_chars(void) {
70 : char buf[64];
71 : /* newline, tab, carriage-return */
72 1 : json_escape_str(buf, sizeof(buf), "a\nb\tc\r");
73 1 : ASSERT(strcmp(buf, "a\\nb\\tc\\r") == 0,
74 : "json_escape control: \\n \\t \\r expanded");
75 : }
76 :
77 : /* ---- Test: json_escape_str — other C0 control characters → \\uXXXX ---- */
78 1 : static void test_json_escape_c0_unicode(void) {
79 : char buf[32];
80 : /* ASCII 0x01 (SOH) and 0x1f (US) */
81 1 : char input[3] = {'\x01', '\x1f', '\0'};
82 1 : json_escape_str(buf, sizeof(buf), input);
83 1 : ASSERT(strcmp(buf, "\\u0001\\u001f") == 0,
84 : "json_escape c0: \\u0001 and \\u001f");
85 : }
86 :
87 : /* ---- Test: json_escape_str — UTF-8 multibyte passes through ---- */
88 1 : static void test_json_escape_utf8_passthrough(void) {
89 : char buf[64];
90 : /* UTF-8 encoded em-dash U+2014: 0xE2 0x80 0x94 */
91 1 : const char *input = "\xe2\x80\x94";
92 1 : json_escape_str(buf, sizeof(buf), input);
93 1 : ASSERT(strcmp(buf, "\xe2\x80\x94") == 0,
94 : "json_escape utf8: high bytes pass through");
95 : }
96 :
97 : /* ---- Test: json_escape_str — NULL input treated as empty string ---- */
98 1 : static void test_json_escape_null_input(void) {
99 : char buf[8];
100 1 : size_t n = json_escape_str(buf, sizeof(buf), NULL);
101 1 : ASSERT(n == 0, "json_escape NULL: length is 0");
102 1 : ASSERT(buf[0] == '\0', "json_escape NULL: NUL-terminated empty string");
103 : }
104 :
105 : /* ---- Test: json_escape_str — output truncated when buffer too small ---- */
106 1 : static void test_json_escape_truncation(void) {
107 : char buf[5]; /* only room for 4 chars + NUL */
108 1 : size_t n = json_escape_str(buf, sizeof(buf), "abcdefgh");
109 : /* Must be NUL-terminated within the 5-byte buffer. */
110 1 : ASSERT(buf[4] == '\0', "json_escape trunc: NUL-terminated");
111 1 : ASSERT(n > 4, "json_escape trunc: returns true length (>= cap)");
112 : (void)n;
113 : }
114 :
115 1 : void run_watch_json_tests(void) {
116 1 : RUN_TEST(test_watch_json_global_flag);
117 1 : RUN_TEST(test_watch_no_json_flag);
118 1 : RUN_TEST(test_watch_json_with_peers_and_interval);
119 1 : RUN_TEST(test_json_escape_plain);
120 1 : RUN_TEST(test_json_escape_special_chars);
121 1 : RUN_TEST(test_json_escape_control_chars);
122 1 : RUN_TEST(test_json_escape_c0_unicode);
123 1 : RUN_TEST(test_json_escape_utf8_passthrough);
124 1 : RUN_TEST(test_json_escape_null_input);
125 1 : RUN_TEST(test_json_escape_truncation);
126 1 : }
|