Line data Source code
1 : /**
2 : * @file test_logout_rpc.c
3 : * @brief TEST-23 — functional tests for auth.logOut RPC + session wipe.
4 : *
5 : * Covers two scenarios:
6 : * 1. Happy path: server returns auth.loggedOut → RPC fires once, session.bin
7 : * removed, auth_logout() returns.
8 : * 2. Error path: server returns rpc_error → session.bin still wiped
9 : * (best-effort logout).
10 : */
11 :
12 : #include "test_helpers.h"
13 :
14 : #include "mock_socket.h"
15 : #include "mock_tel_server.h"
16 :
17 : #include "infrastructure/auth_logout.h"
18 : #include "mtproto_session.h"
19 : #include "transport.h"
20 : #include "api_call.h"
21 : #include "app/session_store.h"
22 : #include "tl_serial.h"
23 : #include "tl_registry.h"
24 :
25 : #include <stdio.h>
26 : #include <stdlib.h>
27 : #include <string.h>
28 : #include <sys/stat.h>
29 : #include <unistd.h>
30 :
31 : /* ---- helpers ---- */
32 :
33 4 : static void with_tmp_home_logout(const char *tag) {
34 : char tmp[256];
35 4 : snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-logout-%s", tag);
36 : char bin[512];
37 4 : snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
38 4 : (void)unlink(bin);
39 4 : setenv("HOME", tmp, 1);
40 : /* CI runners (GitHub Actions) may export XDG_CONFIG_HOME, which would
41 : * make platform_config_dir() ignore our redirected HOME. Force the
42 : * HOME-based fallback so prod code and session_bin_exists() agree. */
43 4 : unsetenv("XDG_CONFIG_HOME");
44 4 : }
45 :
46 8 : static int session_bin_exists(void) {
47 : char path[512];
48 8 : const char *home = getenv("HOME");
49 8 : snprintf(path, sizeof(path), "%s/.config/tg-cli/session.bin", home);
50 : struct stat st;
51 8 : return stat(path, &st) == 0;
52 : }
53 :
54 : /** Connect a fresh Transport to the mock-socket loopback. */
55 4 : static void connect_mock_logout(Transport *t) {
56 4 : transport_init(t);
57 4 : ASSERT(transport_connect(t, "127.0.0.1", 443) == 0,
58 : "transport connects for logout test");
59 : }
60 :
61 : /** Populate an ApiConfig with fake credentials. */
62 4 : static void init_cfg_logout(ApiConfig *cfg) {
63 4 : api_config_init(cfg);
64 4 : cfg->api_id = 12345;
65 4 : cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
66 4 : }
67 :
68 : /* ================================================================ */
69 : /* Responders */
70 : /* ================================================================ */
71 :
72 : /** Happy path: auth.loggedOut#c3a2835f with flags=0. */
73 2 : static void on_logout_ok(MtRpcContext *ctx) {
74 : TlWriter w;
75 2 : tl_writer_init(&w);
76 2 : tl_write_uint32(&w, CRC_auth_loggedOut); /* 0xc3a2835f */
77 2 : tl_write_uint32(&w, 0); /* flags = 0 (no future_auth_token) */
78 2 : mt_server_reply_result(ctx, w.data, w.len);
79 2 : tl_writer_free(&w);
80 2 : }
81 :
82 : /** Error path: generic rpc_error 500 INTERNAL. */
83 2 : static void on_logout_error(MtRpcContext *ctx) {
84 2 : mt_server_reply_error(ctx, 500, "INTERNAL");
85 2 : }
86 :
87 : /* ================================================================ */
88 : /* Test cases */
89 : /* ================================================================ */
90 :
91 : /**
92 : * @brief TEST-23a — happy path: auth.loggedOut returned, session.bin wiped.
93 : */
94 2 : static void test_logout_rpc_happy(void) {
95 2 : with_tmp_home_logout("happy");
96 2 : mt_server_init();
97 2 : mt_server_reset();
98 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed session");
99 :
100 : /* Confirm session.bin was created by the seed. */
101 2 : ASSERT(session_bin_exists(), "session.bin exists before logout");
102 :
103 2 : mt_server_expect(CRC_auth_logOut, on_logout_ok, NULL);
104 :
105 2 : ApiConfig cfg; init_cfg_logout(&cfg);
106 2 : MtProtoSession s; mtproto_session_init(&s);
107 2 : int dc = 0;
108 2 : ASSERT(session_store_load(&s, &dc) == 0, "session loaded");
109 :
110 2 : Transport t; connect_mock_logout(&t);
111 :
112 : /* auth_logout() = auth_logout_rpc() + session_store_clear(). */
113 2 : auth_logout(&cfg, &s, &t);
114 :
115 : /* Responder must have fired exactly once. */
116 2 : ASSERT(mt_server_request_crc_count(CRC_auth_logOut) == 1,
117 : "auth.logOut sent exactly once");
118 :
119 : /* session.bin must be gone. */
120 2 : ASSERT(!session_bin_exists(), "session.bin removed after logout");
121 :
122 2 : transport_close(&t);
123 2 : mt_server_reset();
124 : }
125 :
126 : /**
127 : * @brief TEST-23b — error path: server returns rpc_error, session.bin still
128 : * wiped (best-effort logout).
129 : */
130 2 : static void test_logout_rpc_error_still_clears_session(void) {
131 2 : with_tmp_home_logout("error");
132 2 : mt_server_init();
133 2 : mt_server_reset();
134 2 : ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed session");
135 :
136 2 : ASSERT(session_bin_exists(), "session.bin exists before logout");
137 :
138 2 : mt_server_expect(CRC_auth_logOut, on_logout_error, NULL);
139 :
140 2 : ApiConfig cfg; init_cfg_logout(&cfg);
141 2 : MtProtoSession s; mtproto_session_init(&s);
142 2 : int dc = 0;
143 2 : ASSERT(session_store_load(&s, &dc) == 0, "session loaded");
144 :
145 2 : Transport t; connect_mock_logout(&t);
146 :
147 : /* auth_logout() must wipe the file even when the RPC fails. */
148 2 : auth_logout(&cfg, &s, &t);
149 :
150 : /* Responder fired. */
151 2 : ASSERT(mt_server_request_crc_count(CRC_auth_logOut) == 1,
152 : "auth.logOut sent exactly once (error variant)");
153 :
154 : /* File still gone regardless of RPC error. */
155 2 : ASSERT(!session_bin_exists(),
156 : "session.bin removed even after RPC error (best-effort)");
157 :
158 2 : transport_close(&t);
159 2 : mt_server_reset();
160 : }
161 :
162 : /* ================================================================ */
163 : /* Suite entry point */
164 : /* ================================================================ */
165 :
166 2 : void run_logout_rpc_tests(void) {
167 2 : RUN_TEST(test_logout_rpc_happy);
168 2 : RUN_TEST(test_logout_rpc_error_still_clears_session);
169 2 : }
|