LCOV - code coverage report
Current view: top level - tests/functional/pty - test_password_prompt.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 93.6 % 125 117
Test Date: 2026-04-20 19:54:22 Functions: 100.0 % 8 8

            Line data    Source code
       1              : /* SPDX-License-Identifier: GPL-3.0-or-later */
       2              : /* Copyright 2026 Peter Csaszar */
       3              : 
       4              : /**
       5              :  * @file test_password_prompt.c
       6              :  * @brief TEST-87 — PTY tests for terminal_read_password() masking/echo.
       7              :  *
       8              :  * Exercises src/platform/posix/terminal.c::terminal_read_password via a
       9              :  * minimal harness binary (password_harness) that calls it directly and
      10              :  * prints the result.  Using a harness — rather than tg-tui's 2FA path —
      11              :  * keeps these tests fast and independent from the mock server used by
      12              :  * the other PTY suites.
      13              :  *
      14              :  * Scenarios (matches TEST-87 ticket):
      15              :  *   1. echo off during masked input
      16              :  *        type "hunter2"; the PTY screen must NOT show "hunter2" before
      17              :  *        Enter is pressed.
      18              :  *   2. echo restored on return
      19              :  *        after the password prompt returns, a subsequent fgets() prompt
      20              :  *        echoes typed characters normally.
      21              :  *   3. echo restored after SIGINT
      22              :  *        Ctrl-C kills the child; a fresh child's /bin/echo-style check
      23              :  *        shows echoed characters.  Proves the previous process did not
      24              :  *        leave the terminal in a stuck no-echo state (SIGINT handlers
      25              :  *        in terminal.c restore termios).
      26              :  *   4. backspace edits the hidden buffer
      27              :  *        "bad<BS><BS><BS>hunter2<Enter>" → ACCEPTED:hunter2.
      28              :  *   5. Ctrl-D on empty reports error
      29              :  *        0x04 on an empty line → harness prints "ERROR" and exits 1.
      30              :  *   6. long input (256 chars) accepted without truncation
      31              :  *        ACCEPTED:<256 chars>, LEN:256.
      32              :  *
      33              :  * Each test uses "goto cleanup" labels for ASAN-safe resource release on
      34              :  * failure paths (same pattern as tests/functional/pty/test_readline.c).
      35              :  */
      36              : 
      37              : #include "ptytest.h"
      38              : #include "pty_assert.h"
      39              : 
      40              : #include <signal.h>
      41              : #include <stdio.h>
      42              : #include <stdlib.h>
      43              : #include <string.h>
      44              : #include <unistd.h>
      45              : 
      46              : /* ── Globals required by ASSERT / ASSERT_WAIT_FOR macros ──────────────── */
      47              : 
      48              : static int g_tests_run    = 0;
      49              : static int g_tests_failed = 0;
      50              : 
      51              : #ifndef PASSWORD_HARNESS_BINARY
      52              : #define PASSWORD_HARNESS_BINARY "password_harness"
      53              : #endif
      54              : 
      55              : /* ── Helpers ──────────────────────────────────────────────────────────── */
      56              : 
      57              : #define RUN_TEST(fn) do { \
      58              :     printf("  running %s ...\n", #fn); \
      59              :     fn(); \
      60              : } while(0)
      61              : 
      62              : /** CHECK_WAIT_FOR: jumps to label on timeout instead of returning. */
      63              : #define CHECK_WAIT_FOR(s, text, timeout_ms, label) do { \
      64              :     g_tests_run++; \
      65              :     if (pty_wait_for((s), (text), (timeout_ms)) != 0) { \
      66              :         printf("  [FAIL] %s:%d: wait_for(\"%s\", %d ms) timed out\n", \
      67              :                __FILE__, __LINE__, (text), (timeout_ms)); \
      68              :         g_tests_failed++; \
      69              :         goto label; \
      70              :     } \
      71              : } while(0)
      72              : 
      73              : /** CHECK: jumps to label instead of returning. */
      74              : #define CHECK(cond, msg, label) do { \
      75              :     g_tests_run++; \
      76              :     if (!(cond)) { \
      77              :         printf("  [FAIL] %s:%d: %s\n", __FILE__, __LINE__, (msg)); \
      78              :         g_tests_failed++; \
      79              :         goto label; \
      80              :     } \
      81              : } while(0)
      82              : 
      83              : /** Open an 80x24 PTY and start password_harness with the given mode. */
      84           33 : static PtySession *open_harness(const char *mode) {
      85           33 :     PtySession *s = pty_open(80, 24);
      86           33 :     if (!s) return NULL;
      87           33 :     const char *argv[] = { PASSWORD_HARNESS_BINARY, mode, NULL };
      88           33 :     if (pty_run(s, argv) != 0) {
      89            0 :         pty_close(s);
      90            0 :         return NULL;
      91              :     }
      92              :     /* Wait for the prompt string emitted by terminal_read_password ("Password: "). */
      93           27 :     if (pty_wait_for(s, "Password", 3000) != 0) {
      94            0 :         pty_close(s);
      95            0 :         return NULL;
      96              :     }
      97           27 :     return s;
      98              : }
      99              : 
     100              : /* ── Tests ────────────────────────────────────────────────────────────── */
     101              : 
     102              : /**
     103              :  * @brief Scenario 1: typed characters are NOT echoed during masked input.
     104              :  *
     105              :  * After the prompt appears, type "hunter2" but do NOT press Enter yet.
     106              :  * The PTY master should not have received any echo of those bytes
     107              :  * (because terminal_read_password cleared ECHO).  Once Enter is pressed
     108              :  * the harness prints "ACCEPTED:hunter2" on its own stdout — which IS
     109              :  * visible.  We assert "hunter2" is absent between the prompt and Enter
     110              :  * and present only in the "ACCEPTED:" line.
     111              :  */
     112            8 : static void test_echo_off_during_input(void) {
     113            8 :     PtySession *s = open_harness("prompt");
     114            7 :     CHECK(s != NULL, "open_harness(prompt) should succeed", done_no_session);
     115              : 
     116              :     /* Type the secret (no Enter yet). */
     117            7 :     pty_send_str(s, "hunter2");
     118              :     /* Let any terminal echo land on the master — if it were going to. */
     119            7 :     pty_settle(s, 150);
     120              : 
     121              :     /* The literal "hunter2" must NOT be visible on screen yet. */
     122            7 :     CHECK(pty_screen_contains(s, "hunter2") == 0,
     123              :           "typed password must not be echoed while ECHO is cleared",
     124              :           done);
     125              : 
     126              :     /* Submit so the child exits cleanly. */
     127            7 :     pty_send_key(s, PTY_KEY_ENTER);
     128              : 
     129              :     /* After Enter, the harness prints ACCEPTED:hunter2 to its own stdout. */
     130            7 :     CHECK_WAIT_FOR(s, "ACCEPTED:hunter2", 3000, done);
     131              : 
     132            7 :     int exit_code = pty_wait_exit(s, 3000);
     133            7 :     CHECK(exit_code == 0, "harness must exit 0 after successful prompt", done);
     134            7 : done:
     135            7 :     pty_close(s);
     136            7 : done_no_session:
     137            7 :     return;
     138              : }
     139              : 
     140              : /**
     141              :  * @brief Scenario 2: echo is restored after the prompt returns.
     142              :  *
     143              :  * Prompt once, submit, then the harness fgets() a second line.  Typing
     144              :  * "CONFIRM" on that second read MUST become visible on screen (the
     145              :  * kernel tty line discipline echoes it because ECHO was restored).
     146              :  */
     147            7 : static void test_echo_restored_on_return(void) {
     148            7 :     PtySession *s = open_harness("prompt_then_echo");
     149            6 :     CHECK(s != NULL, "open_harness(prompt_then_echo) should succeed",
     150              :           done_no_session);
     151              : 
     152              :     /* First prompt: masked input. */
     153            6 :     pty_send_str(s, "secret");
     154            6 :     pty_send_key(s, PTY_KEY_ENTER);
     155            6 :     CHECK_WAIT_FOR(s, "ACCEPTED:secret", 3000, done);
     156              : 
     157              :     /* Now a plain fgets() is active.  Type CONFIRM — it SHOULD echo. */
     158            6 :     pty_send_str(s, "CONFIRM");
     159              :     /* Give the kernel time to echo the typed bytes back on the master. */
     160            6 :     pty_settle(s, 150);
     161              : 
     162              :     /* The typed text must have been echoed — search for it on screen
     163              :      * BEFORE sending Enter (so we cannot be confused with the harness's
     164              :      * own "ECHO:CONFIRM\n" output). */
     165            6 :     CHECK(pty_screen_contains(s, "CONFIRM") != 0,
     166              :           "plain prompt after password must echo typed chars "
     167              :           "(ECHO bit was not restored)",
     168              :           done);
     169              : 
     170            6 :     pty_send_key(s, PTY_KEY_ENTER);
     171            6 :     CHECK_WAIT_FOR(s, "ECHO:CONFIRM", 3000, done);
     172              : 
     173            6 :     int exit_code = pty_wait_exit(s, 3000);
     174            6 :     CHECK(exit_code == 0, "harness must exit 0 after both reads succeed",
     175              :           done);
     176            6 : done:
     177            6 :     pty_close(s);
     178            6 : done_no_session:
     179            6 :     return;
     180              : }
     181              : 
     182              : /**
     183              :  * @brief Scenario 3: echo is still usable in a fresh process after SIGINT.
     184              :  *
     185              :  * Ctrl-C during the masked prompt raises SIGINT; the process dies.
     186              :  * If the SIGINT handler failed to restore termios we would observe
     187              :  * the problem in a NEW child started on a fresh PTY: ordinary typed
     188              :  * bytes would not echo.  This test confirms SIGINT is not sticky.
     189              :  *
     190              :  * Note: canonical-mode Ctrl-C affects only the owning process.  Because
     191              :  * we open a brand-new PTY for the "after" probe there is no cross-run
     192              :  * termios leakage to catch in a single test host; the assertion here
     193              :  * is really "Ctrl-C kills the prompt cleanly AND a brand-new prompt
     194              :  * still works".  This is the strongest observable behaviour from the
     195              :  * user's perspective.
     196              :  */
     197            6 : static void test_echo_restored_after_sigint(void) {
     198              :     /* First session: start the prompt, then send Ctrl-C. */
     199            6 :     PtySession *s = open_harness("prompt");
     200            5 :     CHECK(s != NULL, "open_harness(prompt) should succeed",
     201              :           done_no_session);
     202              : 
     203              :     /* Start typing, then interrupt before hitting Enter. */
     204            5 :     pty_send_str(s, "hun");
     205            5 :     pty_settle(s, 100);
     206              : 
     207              :     /* Ctrl-C (0x03) — ISIG is still enabled inside terminal_read_password,
     208              :      * so the tty line discipline will send SIGINT to the foreground group. */
     209            5 :     pty_send_key(s, PTY_KEY_CTRL_C);
     210              : 
     211            5 :     int exit_code = pty_wait_exit(s, 3000);
     212              :     /* pty_wait_exit encodes signal exit as 128 + signum; SIGINT = 2 → 130. */
     213            5 :     CHECK(exit_code == 130 || exit_code == -1,
     214              :           "Ctrl-C during masked prompt must terminate the child",
     215              :           done_first);
     216            5 :     pty_close(s);
     217              : 
     218              :     /* Second session: open a fresh PTY and a fresh harness.  Running
     219              :      * through prompt_then_echo verifies that a new process's plain
     220              :      * fgets() echoes typed bytes normally. */
     221            5 :     s = open_harness("prompt_then_echo");
     222            4 :     CHECK(s != NULL, "fresh open_harness after SIGINT must succeed",
     223              :           done_no_session);
     224              : 
     225            4 :     pty_send_str(s, "pw2");
     226            4 :     pty_send_key(s, PTY_KEY_ENTER);
     227            4 :     CHECK_WAIT_FOR(s, "ACCEPTED:pw2", 3000, done);
     228              : 
     229            4 :     pty_send_str(s, "ECHOOK");
     230            4 :     pty_settle(s, 150);
     231            4 :     CHECK(pty_screen_contains(s, "ECHOOK") != 0,
     232              :           "fresh child after SIGINT must echo typed bytes",
     233              :           done);
     234              : 
     235            4 :     pty_send_key(s, PTY_KEY_ENTER);
     236            4 :     CHECK_WAIT_FOR(s, "ECHO:ECHOOK", 3000, done);
     237            4 :     pty_wait_exit(s, 3000);
     238            4 : done:
     239            4 :     pty_close(s);
     240            4 :     return;
     241            0 : done_first:
     242            0 :     pty_close(s);
     243            0 : done_no_session:
     244            0 :     return;
     245              : }
     246              : 
     247              : /**
     248              :  * @brief Scenario 4: Backspace in the hidden buffer corrects the password.
     249              :  *
     250              :  * In canonical mode with ECHO cleared, the tty line discipline still
     251              :  * handles ERASE (DEL / 0x7F): typing "bad" then three ERASE chars then
     252              :  * "hunter2\n" must deliver the string "hunter2" (NOT "badhunter2") to
     253              :  * getline() inside terminal_read_password.
     254              :  */
     255            4 : static void test_backspace_edits_hidden_buffer(void) {
     256            4 :     PtySession *s = open_harness("prompt");
     257            3 :     CHECK(s != NULL, "open_harness(prompt) should succeed", done_no_session);
     258              : 
     259            3 :     pty_send_str(s, "bad");
     260            3 :     pty_send_key(s, PTY_KEY_BACK);   /* 0x7F */
     261            3 :     pty_send_key(s, PTY_KEY_BACK);
     262            3 :     pty_send_key(s, PTY_KEY_BACK);
     263            3 :     pty_send_str(s, "hunter2");
     264            3 :     pty_send_key(s, PTY_KEY_ENTER);
     265              : 
     266            3 :     CHECK_WAIT_FOR(s, "ACCEPTED:hunter2", 3000, done);
     267              : 
     268              :     /* Also assert the wrong string did not slip through. */
     269            3 :     CHECK(pty_screen_contains(s, "ACCEPTED:badhunter2") == 0,
     270              :           "backspace must erase the preceding chars, not append",
     271              :           done);
     272              : 
     273            3 :     CHECK_WAIT_FOR(s, "LEN:7", 3000, done);
     274              : 
     275            3 :     int exit_code = pty_wait_exit(s, 3000);
     276            3 :     CHECK(exit_code == 0, "harness must exit 0", done);
     277            3 : done:
     278            3 :     pty_close(s);
     279            3 : done_no_session:
     280            3 :     return;
     281              : }
     282              : 
     283              : /**
     284              :  * @brief Scenario 5: Ctrl-D on an empty line returns an error.
     285              :  *
     286              :  * In canonical mode, 0x04 (EOT) on an empty line causes read(2) to
     287              :  * return 0.  getline() returns -1, terminal_read_password returns -1,
     288              :  * and the harness prints "ERROR" and exits 1.
     289              :  */
     290            3 : static void test_ctrl_d_on_empty_reports_error(void) {
     291            3 :     PtySession *s = open_harness("prompt");
     292            2 :     CHECK(s != NULL, "open_harness(prompt) should succeed", done_no_session);
     293              : 
     294            2 :     pty_send_key(s, PTY_KEY_CTRL_D);
     295              : 
     296            2 :     CHECK_WAIT_FOR(s, "ERROR", 3000, done);
     297              : 
     298            2 :     int exit_code = pty_wait_exit(s, 3000);
     299            2 :     CHECK(exit_code == 1,
     300              :           "Ctrl-D on empty password line must exit 1 (error)",
     301              :           done);
     302            2 : done:
     303            2 :     pty_close(s);
     304            2 : done_no_session:
     305            2 :     return;
     306              : }
     307              : 
     308              : /**
     309              :  * @brief Scenario 6: a 256-character password is accepted without truncation.
     310              :  *
     311              :  * The harness's "prompt_big" mode uses a 512-byte buffer, so 256 chars
     312              :  * fit with room for NUL.  We assert both the echoed string and the
     313              :  * reported length.
     314              :  */
     315            2 : static void test_long_input_fits_buffer(void) {
     316            2 :     PtySession *s = pty_open(120, 24);
     317            2 :     CHECK(s != NULL, "pty_open should succeed", done_no_session);
     318              : 
     319            2 :     const char *argv[] = { PASSWORD_HARNESS_BINARY, "prompt_big", NULL };
     320            2 :     CHECK(pty_run(s, argv) == 0, "pty_run should succeed", done);
     321              : 
     322            1 :     CHECK_WAIT_FOR(s, "Password", 3000, done);
     323              : 
     324              :     /* Build a 256-char password made of repeating 'a'..'z' so we can
     325              :      * verify the tail arrived intact. */
     326              :     char pw[257];
     327          257 :     for (int i = 0; i < 256; i++) pw[i] = (char)('a' + (i % 26));
     328            1 :     pw[256] = '\0';
     329              : 
     330            1 :     pty_send(s, pw, 256);
     331            1 :     pty_send_key(s, PTY_KEY_ENTER);
     332              : 
     333            1 :     CHECK_WAIT_FOR(s, "LEN:256", 5000, done);
     334              : 
     335            1 :     int exit_code = pty_wait_exit(s, 3000);
     336            1 :     CHECK(exit_code == 0, "harness must exit 0 for 256-char password", done);
     337            1 : done:
     338            1 :     pty_close(s);
     339            1 : done_no_session:
     340            1 :     return;
     341              : }
     342              : 
     343              : /* ── Entry point ──────────────────────────────────────────────────────── */
     344              : 
     345            8 : int main(void) {
     346            8 :     printf("TEST-87 password prompt PTY tests (%s)\n",
     347              :            PASSWORD_HARNESS_BINARY);
     348              : 
     349            8 :     RUN_TEST(test_echo_off_during_input);
     350            7 :     RUN_TEST(test_echo_restored_on_return);
     351            6 :     RUN_TEST(test_echo_restored_after_sigint);
     352            4 :     RUN_TEST(test_backspace_edits_hidden_buffer);
     353            3 :     RUN_TEST(test_ctrl_d_on_empty_reports_error);
     354            2 :     RUN_TEST(test_long_input_fits_buffer);
     355              : 
     356            1 :     printf("\n%d tests run, %d failed\n", g_tests_run, g_tests_failed);
     357            1 :     return g_tests_failed > 0 ? 1 : 0;
     358              : }
        

Generated by: LCOV version 2.0-1