LCOV - code coverage report
Current view: top level - tests/functional - test_tui_e2e.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 99.4 % 173 172
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 11 11

            Line data    Source code
       1              : /**
       2              :  * @file test_tui_e2e.c
       3              :  * @brief TEST-11 — TUI end-to-end functional tests.
       4              :  *
       5              :  * Drives the TUI view-model layer (dialog_pane, history_pane, tui_app_paint)
       6              :  * against the in-process mock Telegram server.  This sits one level above the
       7              :  * existing unit tests (test_tui_app.c etc.) which exercise the state machine
       8              :  * in total isolation: here the domain calls actually fire MTProto RPCs against
       9              :  * the mock, so the full path
      10              :  *
      11              :  *   dialog_pane_refresh → domain_get_dialogs → MTProto → mock responder
      12              :  *   history_pane_load   → domain_get_history → MTProto → mock responder
      13              :  *   tui_app_paint       → back-buffer contains dialog title + message text
      14              :  *
      15              :  * is exercised with real MTProto framing and TL parsing.
      16              :  *
      17              :  * No PTY is required: the Screen writes to a FILE* that is swapped to
      18              :  * /dev/null (we only inspect the back buffer, not the ANSI byte stream).
      19              :  */
      20              : 
      21              : #include "test_helpers.h"
      22              : 
      23              : #include "mock_socket.h"
      24              : #include "mock_tel_server.h"
      25              : 
      26              : #include "api_call.h"
      27              : #include "mtproto_session.h"
      28              : #include "transport.h"
      29              : #include "app/session_store.h"
      30              : #include "tl_registry.h"
      31              : #include "tl_serial.h"
      32              : 
      33              : #include "domain/read/dialogs.h"
      34              : #include "domain/read/history.h"
      35              : 
      36              : #include "tui/app.h"
      37              : #include "tui/dialog_pane.h"
      38              : #include "tui/history_pane.h"
      39              : 
      40              : #include <stdio.h>
      41              : #include <stdlib.h>
      42              : #include <string.h>
      43              : #include <unistd.h>
      44              : 
      45              : /* ---- CRCs not surfaced in tl_registry.h ---- */
      46              : #define CRC_dialog                0xd58a08c6U
      47              : #define CRC_peerNotifySettings    0xa83b0426U
      48              : #define CRC_messages_getDialogs   0xa0f4cb4fU
      49              : #define CRC_messages_getHistory   0x4423e6c5U
      50              : 
      51              : /* ---- Helpers (same pattern as test_read_path.c) ---- */
      52              : 
      53            6 : static void with_tmp_home_tui(const char *tag) {
      54              :     char tmp[256];
      55            6 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-tui-%s", tag);
      56              :     char bin[512];
      57            6 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      58            6 :     (void)unlink(bin);
      59            6 :     setenv("HOME", tmp, 1);
      60            6 : }
      61              : 
      62            6 : static void connect_mock_tui(Transport *t) {
      63            6 :     transport_init(t);
      64            6 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
      65              : }
      66              : 
      67            6 : static void init_cfg_tui(ApiConfig *cfg) {
      68            6 :     api_config_init(cfg);
      69            6 :     cfg->api_id   = 12345;
      70            6 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      71            6 : }
      72              : 
      73            6 : static void load_session_tui(MtProtoSession *s) {
      74            6 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
      75            6 :     mtproto_session_init(s);
      76            6 :     int dc = 0;
      77            6 :     ASSERT(session_store_load(s, &dc) == 0, "load session");
      78              : }
      79              : 
      80              : /* ---- Mock responders ---- */
      81              : 
      82              : /**
      83              :  * messages.dialogs with one user-peer dialog (id=555, unread=2, title="Alice").
      84              :  *
      85              :  * The users vector carries one user with:
      86              :  *   flags.0  → access_hash present
      87              :  *   flags.1  → first_name present  (used as title for user dialogs)
      88              :  * so the domain layer will join title = "Alice".
      89              :  */
      90            6 : static void on_dialogs_alice(MtRpcContext *ctx) {
      91              :     TlWriter w;
      92            6 :     tl_writer_init(&w);
      93            6 :     tl_write_uint32(&w, TL_messages_dialogs);
      94              : 
      95              :     /* dialogs: Vector<Dialog> with 1 entry */
      96            6 :     tl_write_uint32(&w, TL_vector);
      97            6 :     tl_write_uint32(&w, 1);
      98            6 :     tl_write_uint32(&w, CRC_dialog);
      99            6 :     tl_write_uint32(&w, 0);              /* flags = 0, no optional fields */
     100            6 :     tl_write_uint32(&w, TL_peerUser);
     101            6 :     tl_write_int64 (&w, 555LL);
     102            6 :     tl_write_int32 (&w, 1200);           /* top_message */
     103            6 :     tl_write_int32 (&w, 0);             /* read_inbox_max_id */
     104            6 :     tl_write_int32 (&w, 0);             /* read_outbox_max_id */
     105            6 :     tl_write_int32 (&w, 2);             /* unread_count */
     106            6 :     tl_write_int32 (&w, 0);             /* unread_mentions_count */
     107            6 :     tl_write_int32 (&w, 0);             /* unread_reactions_count */
     108            6 :     tl_write_uint32(&w, CRC_peerNotifySettings);
     109            6 :     tl_write_uint32(&w, 0);
     110              : 
     111              :     /* messages vector: empty */
     112            6 :     tl_write_uint32(&w, TL_vector);
     113            6 :     tl_write_uint32(&w, 0);
     114              : 
     115              :     /* chats vector: empty */
     116            6 :     tl_write_uint32(&w, TL_vector);
     117            6 :     tl_write_uint32(&w, 0);
     118              : 
     119              :     /* users vector: one user — flags.0 (access_hash) | flags.1 (first_name) */
     120            6 :     tl_write_uint32(&w, TL_vector);
     121            6 :     tl_write_uint32(&w, 1);
     122            6 :     tl_write_uint32(&w, TL_user);
     123            6 :     tl_write_uint32(&w, (1u << 0) | (1u << 1)); /* flags */
     124            6 :     tl_write_uint32(&w, 0);                      /* flags2 */
     125            6 :     tl_write_int64 (&w, 555LL);
     126            6 :     tl_write_int64 (&w, 0xAABBCCDDEEFF0011LL);   /* access_hash (flags.0) */
     127            6 :     tl_write_string(&w, "Alice");                 /* first_name  (flags.1) */
     128              : 
     129            6 :     mt_server_reply_result(ctx, w.data, w.len);
     130            6 :     tl_writer_free(&w);
     131            6 : }
     132              : 
     133              : /**
     134              :  * messages.messages with one plain-text inbound message:
     135              :  *   id=42, text="Hello TUI world", date=1700000000
     136              :  */
     137            4 : static void on_history_one_text(MtRpcContext *ctx) {
     138              :     TlWriter w;
     139            4 :     tl_writer_init(&w);
     140            4 :     tl_write_uint32(&w, TL_messages_messages);
     141              : 
     142              :     /* messages vector: 1 entry */
     143            4 :     tl_write_uint32(&w, TL_vector);
     144            4 :     tl_write_uint32(&w, 1);
     145            4 :     tl_write_uint32(&w, TL_message);
     146            4 :     tl_write_uint32(&w, 0);              /* flags  = 0 (no from_id etc.) */
     147            4 :     tl_write_uint32(&w, 0);              /* flags2 = 0 */
     148            4 :     tl_write_int32 (&w, 42);            /* id */
     149              :     /* peer_id: peerUser id=555 (flags.28 off → no saved_peer) */
     150            4 :     tl_write_uint32(&w, TL_peerUser);
     151            4 :     tl_write_int64 (&w, 555LL);
     152            4 :     tl_write_int32 (&w, 1700000000);    /* date */
     153            4 :     tl_write_string(&w, "Hello TUI world"); /* message */
     154              : 
     155              :     /* chats vector: empty */
     156            4 :     tl_write_uint32(&w, TL_vector);
     157            4 :     tl_write_uint32(&w, 0);
     158              :     /* users vector: empty */
     159            4 :     tl_write_uint32(&w, TL_vector);
     160            4 :     tl_write_uint32(&w, 0);
     161              : 
     162            4 :     mt_server_reply_result(ctx, w.data, w.len);
     163            4 :     tl_writer_free(&w);
     164            4 : }
     165              : 
     166              : /* ---- Utility: scan the back buffer for a substring ---- */
     167              : 
     168              : /**
     169              :  * Return 1 if the string @p needle appears as consecutive codepoints in any
     170              :  * row of the Screen back buffer, 0 otherwise.
     171              :  */
     172            8 : static int screen_back_contains(const Screen *sc, const char *needle) {
     173            8 :     int nlen = (int)strlen(needle);
     174            8 :     if (nlen == 0) return 1;
     175            8 :     int total = sc->rows * sc->cols;
     176          172 :     for (int start = 0; start <= total - nlen; start++) {
     177          172 :         int match = 1;
     178          416 :         for (int k = 0; k < nlen && match; k++) {
     179          244 :             if (sc->back[start + k].cp != (uint32_t)(unsigned char)needle[k])
     180          164 :                 match = 0;
     181              :         }
     182          172 :         if (match) return 1;
     183              :     }
     184            0 :     return 0;
     185              : }
     186              : 
     187              : /* ================================================================ */
     188              : /* Tests                                                            */
     189              : /* ================================================================ */
     190              : 
     191              : /**
     192              :  * TEST-11a: dialog_pane_refresh fires messages.getDialogs, parses the
     193              :  * response and stores the dialog entry.  The back buffer rendered by
     194              :  * tui_app_paint contains the dialog title "Alice".
     195              :  */
     196            2 : static void test_tui_dialog_refresh_paints_title(void) {
     197            2 :     with_tmp_home_tui("dlg-paint");
     198            2 :     mt_server_init(); mt_server_reset();
     199            2 :     dialogs_cache_flush();
     200            2 :     MtProtoSession s; load_session_tui(&s);
     201            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_alice, NULL);
     202              : 
     203            2 :     ApiConfig cfg; init_cfg_tui(&cfg);
     204            2 :     Transport t; connect_mock_tui(&t);
     205              : 
     206              :     TuiApp app;
     207            2 :     ASSERT(tui_app_init(&app, 24, 80) == 0, "app init");
     208            2 :     app.screen.out = fopen("/dev/null", "w");   /* silence ANSI bytes */
     209              : 
     210            2 :     ASSERT(dialog_pane_refresh(&app.dialogs, &cfg, &s, &t) == 0,
     211              :            "dialog_pane_refresh ok");
     212            2 :     app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
     213              : 
     214            2 :     ASSERT(app.dialogs.count == 1, "one dialog loaded");
     215            2 :     ASSERT(strcmp(app.dialogs.entries[0].title, "Alice") == 0,
     216              :            "title == Alice");
     217            2 :     ASSERT(app.dialogs.entries[0].peer_id == 555LL, "peer_id == 555");
     218            2 :     ASSERT(app.dialogs.entries[0].unread_count == 2, "unread_count == 2");
     219              : 
     220            2 :     tui_app_paint(&app);
     221              : 
     222            2 :     ASSERT(screen_back_contains(&app.screen, "Alice"),
     223              :            "back buffer contains 'Alice' after paint");
     224              : 
     225            2 :     if (app.screen.out != stdout) fclose(app.screen.out);
     226            2 :     tui_app_free(&app);
     227            2 :     transport_close(&t);
     228            2 :     mt_server_reset();
     229              : }
     230              : 
     231              : /**
     232              :  * TEST-11b: history_pane_load fires messages.getHistory for the selected
     233              :  * dialog's peer and stores the message.  After tui_app_paint the back buffer
     234              :  * contains the message text.
     235              :  */
     236            2 : static void test_tui_history_load_paints_message(void) {
     237            2 :     with_tmp_home_tui("hist-paint");
     238            2 :     mt_server_init(); mt_server_reset();
     239            2 :     dialogs_cache_flush();
     240            2 :     MtProtoSession s; load_session_tui(&s);
     241              : 
     242              :     /* Seed two expected RPC calls: dialogs then history. */
     243            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_alice, NULL);
     244            2 :     mt_server_expect(CRC_messages_getHistory, on_history_one_text, NULL);
     245              : 
     246            2 :     ApiConfig cfg; init_cfg_tui(&cfg);
     247            2 :     Transport t; connect_mock_tui(&t);
     248              : 
     249              :     TuiApp app;
     250            2 :     ASSERT(tui_app_init(&app, 24, 80) == 0, "app init");
     251            2 :     app.screen.out = fopen("/dev/null", "w");
     252              : 
     253              :     /* Phase 1: refresh dialogs. */
     254            2 :     ASSERT(dialog_pane_refresh(&app.dialogs, &cfg, &s, &t) == 0,
     255              :            "dialog_pane_refresh ok");
     256            2 :     app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
     257              : 
     258              :     /* Phase 2: simulate Enter — open first dialog's history. */
     259            2 :     const DialogEntry *d = dialog_pane_selected(&app.dialogs);
     260            2 :     ASSERT(d != NULL, "a dialog is selected");
     261              : 
     262            2 :     HistoryPeer peer = {0};
     263            2 :     peer.kind        = HISTORY_PEER_USER;
     264            2 :     peer.peer_id     = d->peer_id;
     265            2 :     peer.access_hash = d->access_hash;
     266              : 
     267            2 :     ASSERT(history_pane_load(&app.history, &cfg, &s, &t, &peer) == 0,
     268              :            "history_pane_load ok");
     269            2 :     app.history.lv.rows_visible = app.layout.history.rows;
     270              : 
     271            2 :     ASSERT(app.history.count == 1, "one message loaded");
     272            2 :     ASSERT(app.history.entries[0].id == 42, "message id == 42");
     273            2 :     ASSERT(strcmp(app.history.entries[0].text, "Hello TUI world") == 0,
     274              :            "message text correct");
     275              : 
     276            2 :     tui_app_paint(&app);
     277              : 
     278            2 :     ASSERT(screen_back_contains(&app.screen, "Hello TUI world"),
     279              :            "back buffer contains message text after paint");
     280              : 
     281            2 :     if (app.screen.out != stdout) fclose(app.screen.out);
     282            2 :     tui_app_free(&app);
     283            2 :     transport_close(&t);
     284            2 :     mt_server_reset();
     285              : }
     286              : 
     287              : /**
     288              :  * TEST-11c: keypress sequence j → Enter → q exercises the full TUI event
     289              :  * loop against real MTProto data.  After Enter the history pane is loaded
     290              :  * (via the expected server responders), 'q' returns TUI_EVENT_QUIT, and
     291              :  * the final paint keeps both panes populated.
     292              :  */
     293            2 : static void test_tui_keypress_sequence_j_enter_q(void) {
     294            2 :     with_tmp_home_tui("key-seq");
     295            2 :     mt_server_init(); mt_server_reset();
     296            2 :     dialogs_cache_flush();
     297            2 :     MtProtoSession s; load_session_tui(&s);
     298              : 
     299            2 :     mt_server_expect(CRC_messages_getDialogs, on_dialogs_alice, NULL);
     300            2 :     mt_server_expect(CRC_messages_getHistory, on_history_one_text, NULL);
     301              : 
     302            2 :     ApiConfig cfg; init_cfg_tui(&cfg);
     303            2 :     Transport t; connect_mock_tui(&t);
     304              : 
     305              :     TuiApp app;
     306            2 :     ASSERT(tui_app_init(&app, 24, 80) == 0, "app init");
     307            2 :     app.screen.out = fopen("/dev/null", "w");
     308              : 
     309              :     /* Refresh dialogs (simulates TUI startup). */
     310            2 :     ASSERT(dialog_pane_refresh(&app.dialogs, &cfg, &s, &t) == 0,
     311              :            "dialog refresh");
     312            2 :     app.dialogs.lv.rows_visible = app.layout.dialogs.rows;
     313              : 
     314              :     /* 'j' — move selection down (from 0 to 1, clamped to 0 since only one). */
     315            2 :     TuiEvent ev = tui_app_handle_char(&app, 'j');
     316            2 :     ASSERT(ev == TUI_EVENT_REDRAW || ev == TUI_EVENT_NONE,
     317              :            "'j' returns REDRAW or NONE");
     318              : 
     319              :     /* Enter — open the selected dialog. */
     320            2 :     ev = tui_app_handle_key(&app, TERM_KEY_ENTER);
     321            2 :     ASSERT(ev == TUI_EVENT_OPEN_DIALOG, "Enter returns OPEN_DIALOG");
     322            2 :     ASSERT(app.focus == TUI_FOCUS_HISTORY, "focus shifted to history");
     323              : 
     324              :     /* Simulate the caller's response to OPEN_DIALOG: load history. */
     325            2 :     const DialogEntry *d = &app.dialogs.entries[0];
     326            2 :     HistoryPeer peer = {0};
     327            2 :     peer.kind        = HISTORY_PEER_USER;
     328            2 :     peer.peer_id     = d->peer_id;
     329            2 :     peer.access_hash = d->access_hash;
     330            2 :     ASSERT(history_pane_load(&app.history, &cfg, &s, &t, &peer) == 0,
     331              :            "history loaded after OPEN_DIALOG");
     332            2 :     app.history.lv.rows_visible = app.layout.history.rows;
     333              : 
     334              :     /* Final paint — both panes should now carry real data. */
     335            2 :     tui_app_paint(&app);
     336            2 :     ASSERT(screen_back_contains(&app.screen, "Alice"),
     337              :            "back buffer has dialog title");
     338            2 :     ASSERT(screen_back_contains(&app.screen, "Hello TUI world"),
     339              :            "back buffer has message text");
     340              : 
     341              :     /* 'q' — quit. */
     342            2 :     ev = tui_app_handle_char(&app, 'q');
     343            2 :     ASSERT(ev == TUI_EVENT_QUIT, "'q' returns QUIT");
     344              : 
     345            2 :     if (app.screen.out != stdout) fclose(app.screen.out);
     346            2 :     tui_app_free(&app);
     347            2 :     transport_close(&t);
     348            2 :     mt_server_reset();
     349              : }
     350              : 
     351              : /* ================================================================ */
     352              : /* Suite entry point                                                */
     353              : /* ================================================================ */
     354              : 
     355            2 : void run_tui_e2e_tests(void) {
     356            2 :     RUN_TEST(test_tui_dialog_refresh_paints_title);
     357            2 :     RUN_TEST(test_tui_history_load_paints_message);
     358            2 :     RUN_TEST(test_tui_keypress_sequence_j_enter_q);
     359            2 : }
        

Generated by: LCOV version 2.0-1