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

            Line data    Source code
       1              : /**
       2              :  * @file test_resolver_cache.c
       3              :  * @brief TEST-85 — functional coverage for the @username resolver cache.
       4              :  *
       5              :  * Drives src/domain/read/user_info.c::domain_resolve_username through the
       6              :  * mock Telegram server and verifies:
       7              :  *
       8              :  *   1. Cold call — first resolve of a @peer issues exactly one
       9              :  *      contacts.resolveUsername RPC.
      10              :  *   2. Warm call — a second call for the same @peer within the positive
      11              :  *      TTL is served from the in-process cache (zero new RPC).
      12              :  *   3. Different peer — a separate @peer adds one fresh RPC.
      13              :  *   4. TTL expiry — advancing the injected clock past the positive TTL
      14              :  *      causes the next call to re-fire the RPC.
      15              :  *   5. Logout flush — the logout path (auth_logout with the registered
      16              :  *      flush callback) invalidates all cached entries so the next call
      17              :  *      fires the RPC again.
      18              :  *   6. Negative caching — USERNAME_NOT_OCCUPIED / USERNAME_INVALID
      19              :  *      responses are remembered under a shorter TTL; a second call
      20              :  *      within that TTL does NOT re-fire the RPC, but after the negative
      21              :  *      TTL elapses the RPC is re-issued.
      22              :  *   7. Eviction — filling the cache past its capacity evicts the oldest
      23              :  *      entry, so that re-resolving the first-inserted @peer fires a
      24              :  *      fresh RPC while the newer entries stay cached.
      25              :  *
      26              :  * Timing is controlled by resolve_cache_set_now_fn() (a compile-time
      27              :  * seam in user_info.c), so no real sleeps are required.
      28              :  */
      29              : 
      30              : #include "test_helpers.h"
      31              : 
      32              : #include "mock_socket.h"
      33              : #include "mock_tel_server.h"
      34              : 
      35              : #include "api_call.h"
      36              : #include "mtproto_session.h"
      37              : #include "transport.h"
      38              : #include "app/session_store.h"
      39              : #include "tl_registry.h"
      40              : #include "tl_serial.h"
      41              : #include "domain/read/user_info.h"
      42              : #include "infrastructure/auth_logout.h"
      43              : 
      44              : #include <stdio.h>
      45              : #include <stdlib.h>
      46              : #include <string.h>
      47              : #include <unistd.h>
      48              : #include <time.h>
      49              : 
      50              : /* ---- CRCs (not surfaced by public headers) ---- */
      51              : #define CRC_contacts_resolveUsername  0xf93ccba3U
      52              : 
      53              : /* ---- Injected clock for resolver-cache TTL ---- */
      54              : 
      55              : static time_t s_fake_time = 0;
      56              : 
      57          182 : static time_t fake_now(void) { return s_fake_time; }
      58              : 
      59              : /* ---- Helpers ---- */
      60              : 
      61           14 : static void with_tmp_home(const char *tag) {
      62              :     char tmp[256];
      63           14 :     snprintf(tmp, sizeof(tmp), "/tmp/tg-cli-ft-rcache-%s", tag);
      64              :     char bin[512];
      65           14 :     snprintf(bin, sizeof(bin), "%s/.config/tg-cli/session.bin", tmp);
      66           14 :     (void)unlink(bin);
      67           14 :     setenv("HOME", tmp, 1);
      68              :     /* CI runners (GitHub Actions) may export XDG_{CONFIG,CACHE}_HOME,
      69              :      * which makes platform_*_dir() ignore our redirected HOME. Force the
      70              :      * HOME-based fallback so production code and these tests agree. */
      71           14 :     unsetenv("XDG_CONFIG_HOME");
      72           14 :     unsetenv("XDG_CACHE_HOME");
      73           14 : }
      74              : 
      75           14 : static void connect_mock(Transport *t) {
      76           14 :     transport_init(t);
      77           14 :     ASSERT(transport_connect(t, "127.0.0.1", 443) == 0, "connect");
      78              : }
      79              : 
      80           14 : static void init_cfg(ApiConfig *cfg) {
      81           14 :     api_config_init(cfg);
      82           14 :     cfg->api_id   = 12345;
      83           14 :     cfg->api_hash = "deadbeefcafebabef00dbaadfeedc0de";
      84           14 : }
      85              : 
      86           14 : static void load_session(MtProtoSession *s) {
      87           14 :     ASSERT(mt_server_seed_session(2, NULL, NULL, NULL) == 0, "seed");
      88           14 :     mtproto_session_init(s);
      89           14 :     int dc = 0;
      90           14 :     ASSERT(session_store_load(s, &dc) == 0, "load session");
      91              : }
      92              : 
      93              : /* ---- Responders ---- */
      94              : 
      95              : /** contacts.resolvedPeer → user id 8001 with access_hash. */
      96           14 : static void on_resolve_user_8001(MtRpcContext *ctx) {
      97              :     TlWriter w;
      98           14 :     tl_writer_init(&w);
      99           14 :     tl_write_uint32(&w, TL_contacts_resolvedPeer);
     100           14 :     tl_write_uint32(&w, TL_peerUser);
     101           14 :     tl_write_int64 (&w, 8001LL);
     102              :     /* chats vector: empty */
     103           14 :     tl_write_uint32(&w, TL_vector);
     104           14 :     tl_write_uint32(&w, 0);
     105              :     /* users vector: one user with access_hash */
     106           14 :     tl_write_uint32(&w, TL_vector);
     107           14 :     tl_write_uint32(&w, 1);
     108           14 :     tl_write_uint32(&w, TL_user);
     109           14 :     tl_write_uint32(&w, 1u);                    /* flags.0 → access_hash */
     110           14 :     tl_write_uint32(&w, 0);                     /* flags2 */
     111           14 :     tl_write_int64 (&w, 8001LL);
     112           14 :     tl_write_int64 (&w, 0xDEADBEEFCAFEBABEULL);
     113           14 :     mt_server_reply_result(ctx, w.data, w.len);
     114           14 :     tl_writer_free(&w);
     115           14 : }
     116              : 
     117              : /** contacts.resolvedPeer → user id 8002 (distinct @peer). */
     118            2 : static void on_resolve_user_8002(MtRpcContext *ctx) {
     119              :     TlWriter w;
     120            2 :     tl_writer_init(&w);
     121            2 :     tl_write_uint32(&w, TL_contacts_resolvedPeer);
     122            2 :     tl_write_uint32(&w, TL_peerUser);
     123            2 :     tl_write_int64 (&w, 8002LL);
     124            2 :     tl_write_uint32(&w, TL_vector);
     125            2 :     tl_write_uint32(&w, 0);
     126            2 :     tl_write_uint32(&w, TL_vector);
     127            2 :     tl_write_uint32(&w, 1);
     128            2 :     tl_write_uint32(&w, TL_user);
     129            2 :     tl_write_uint32(&w, 1u);
     130            2 :     tl_write_uint32(&w, 0);
     131            2 :     tl_write_int64 (&w, 8002LL);
     132            2 :     tl_write_int64 (&w, 0x1122334455667788ULL);
     133            2 :     mt_server_reply_result(ctx, w.data, w.len);
     134            2 :     tl_writer_free(&w);
     135            2 : }
     136              : 
     137              : /** Parametric responder for eviction / capacity stress — walks the key
     138              :  *  inside the incoming request to derive a unique id. */
     139           68 : static void on_resolve_capture_id(MtRpcContext *ctx) {
     140              :     /* Request layout after the CRC: string @username (tl_read_string).
     141              :      * Strings under 254 bytes begin with a 1-byte length prefix. We use
     142              :      * keys like "p0"..."p31" so the short-form always applies. */
     143           68 :     const uint8_t *p = ctx->req_body + 4;
     144           68 :     size_t n = (size_t)p[0];
     145           68 :     int64_t id = 7000 + (int64_t)atoi((const char *)(p + 2));
     146              :     TlWriter w;
     147           68 :     tl_writer_init(&w);
     148           68 :     tl_write_uint32(&w, TL_contacts_resolvedPeer);
     149           68 :     tl_write_uint32(&w, TL_peerUser);
     150           68 :     tl_write_int64 (&w, id);
     151           68 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 0);
     152           68 :     tl_write_uint32(&w, TL_vector); tl_write_uint32(&w, 1);
     153           68 :     tl_write_uint32(&w, TL_user);
     154           68 :     tl_write_uint32(&w, 1u);
     155           68 :     tl_write_uint32(&w, 0);
     156           68 :     tl_write_int64 (&w, id);
     157           68 :     tl_write_int64 (&w, (int64_t)(0xA000000000000000ULL | (uint64_t)id));
     158           68 :     mt_server_reply_result(ctx, w.data, w.len);
     159           68 :     tl_writer_free(&w);
     160              :     (void)n;
     161           68 : }
     162              : 
     163              : /** Error path: USERNAME_NOT_OCCUPIED. */
     164            4 : static void on_resolve_not_occupied(MtRpcContext *ctx) {
     165            4 :     mt_server_reply_error(ctx, 400, "USERNAME_NOT_OCCUPIED");
     166            4 : }
     167              : 
     168              : /** auth.loggedOut#c3a2835f flags=0 — canonical happy-path logout reply. */
     169            2 : static void on_logout_ok(MtRpcContext *ctx) {
     170              :     TlWriter w;
     171            2 :     tl_writer_init(&w);
     172            2 :     tl_write_uint32(&w, CRC_auth_loggedOut);
     173            2 :     tl_write_uint32(&w, 0);
     174            2 :     mt_server_reply_result(ctx, w.data, w.len);
     175            2 :     tl_writer_free(&w);
     176            2 : }
     177              : 
     178              : /* ================================================================ */
     179              : /* Tests                                                            */
     180              : /* ================================================================ */
     181              : 
     182              : /**
     183              :  * @brief First `info @foo` must issue exactly one resolveUsername RPC.
     184              :  */
     185            2 : static void test_cold_call_resolves_once(void) {
     186            2 :     with_tmp_home("cold");
     187            2 :     mt_server_init(); mt_server_reset();
     188            2 :     resolve_cache_set_now_fn(fake_now);
     189            2 :     s_fake_time = 1000;
     190            2 :     resolve_cache_flush();
     191              : 
     192            2 :     MtProtoSession s; load_session(&s);
     193            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     194              : 
     195            2 :     ApiConfig cfg; init_cfg(&cfg);
     196            2 :     Transport t; connect_mock(&t);
     197              : 
     198            2 :     ResolvedPeer rp = {0};
     199            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp) == 0,
     200              :            "cold resolve ok");
     201            2 :     ASSERT(rp.id == 8001LL, "peer id matches mock reply");
     202            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     203              :            "exactly one resolveUsername RPC was sent");
     204              : 
     205            2 :     resolve_cache_set_now_fn(NULL);
     206            2 :     transport_close(&t);
     207            2 :     mt_server_reset();
     208              : }
     209              : 
     210              : /**
     211              :  * @brief Second `info @foo` within the positive TTL must be cache-served.
     212              :  */
     213            2 : static void test_warm_call_skips_rpc(void) {
     214            2 :     with_tmp_home("warm");
     215            2 :     mt_server_init(); mt_server_reset();
     216            2 :     resolve_cache_set_now_fn(fake_now);
     217            2 :     s_fake_time = 2000;
     218            2 :     resolve_cache_flush();
     219              : 
     220            2 :     MtProtoSession s; load_session(&s);
     221            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     222              : 
     223            2 :     ApiConfig cfg; init_cfg(&cfg);
     224            2 :     Transport t; connect_mock(&t);
     225              : 
     226            2 :     ResolvedPeer rp1 = {0};
     227            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp1) == 0,
     228              :            "1st resolve ok");
     229            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     230              :            "cold path → 1 RPC");
     231              : 
     232              :     /* +10 s — still well within the positive TTL. */
     233            2 :     s_fake_time += 10;
     234              : 
     235            2 :     ResolvedPeer rp2 = {0};
     236            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp2) == 0,
     237              :            "2nd resolve ok (from cache)");
     238            2 :     ASSERT(rp2.id == rp1.id, "cached id matches");
     239            2 :     ASSERT(rp2.access_hash == rp1.access_hash, "cached access_hash matches");
     240            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     241              :            "warm path added 0 RPCs");
     242              : 
     243            2 :     resolve_cache_set_now_fn(NULL);
     244            2 :     transport_close(&t);
     245            2 :     mt_server_reset();
     246              : }
     247              : 
     248              : /**
     249              :  * @brief A different @peer must still issue its own RPC, not reuse the
     250              :  *        previous cache entry.
     251              :  */
     252            2 : static void test_different_peer_does_not_hit_cache(void) {
     253            2 :     with_tmp_home("diff");
     254            2 :     mt_server_init(); mt_server_reset();
     255            2 :     resolve_cache_set_now_fn(fake_now);
     256            2 :     s_fake_time = 3000;
     257            2 :     resolve_cache_flush();
     258              : 
     259            2 :     MtProtoSession s; load_session(&s);
     260              : 
     261            2 :     ApiConfig cfg; init_cfg(&cfg);
     262            2 :     Transport t; connect_mock(&t);
     263              : 
     264              :     /* Fill cache with @foo → 8001. */
     265            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     266            2 :     ResolvedPeer rp_foo = {0};
     267            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp_foo) == 0,
     268              :            "resolve @foo ok");
     269            2 :     ASSERT(rp_foo.id == 8001LL, "@foo id == 8001");
     270            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     271              :            "after @foo → 1 RPC");
     272              : 
     273              :     /* Swap responder: @bar → 8002 distinct id. */
     274            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8002, NULL);
     275            2 :     ResolvedPeer rp_bar = {0};
     276            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@bar", &rp_bar) == 0,
     277              :            "resolve @bar ok");
     278            2 :     ASSERT(rp_bar.id == 8002LL, "@bar id == 8002 (not from @foo cache)");
     279            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 2,
     280              :            "second @peer fires a separate RPC");
     281              : 
     282            2 :     resolve_cache_set_now_fn(NULL);
     283            2 :     transport_close(&t);
     284            2 :     mt_server_reset();
     285              : }
     286              : 
     287              : /**
     288              :  * @brief After the positive TTL lapses the next call must fire a fresh
     289              :  *        RPC and refresh the cache.
     290              :  */
     291            2 : static void test_ttl_expiry_refreshes(void) {
     292            2 :     with_tmp_home("ttl");
     293            2 :     mt_server_init(); mt_server_reset();
     294            2 :     resolve_cache_set_now_fn(fake_now);
     295            2 :     s_fake_time = 4000;
     296            2 :     resolve_cache_flush();
     297              : 
     298            2 :     MtProtoSession s; load_session(&s);
     299            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     300              : 
     301            2 :     ApiConfig cfg; init_cfg(&cfg);
     302            2 :     Transport t; connect_mock(&t);
     303              : 
     304            2 :     ResolvedPeer rp = {0};
     305            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp) == 0,
     306              :            "cold resolve ok");
     307            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     308              :            "after cold → 1 RPC");
     309              : 
     310              :     /* Fast-forward past the positive TTL (plus a safety margin). */
     311            2 :     s_fake_time += resolve_cache_positive_ttl() + 1;
     312              : 
     313            2 :     ResolvedPeer rp2 = {0};
     314            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp2) == 0,
     315              :            "refresh resolve ok");
     316            2 :     ASSERT(rp2.id == 8001LL, "refreshed id matches mock reply");
     317            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 2,
     318              :            "TTL expiry triggers second RPC");
     319              : 
     320            2 :     resolve_cache_set_now_fn(NULL);
     321            2 :     transport_close(&t);
     322            2 :     mt_server_reset();
     323              : }
     324              : 
     325              : /**
     326              :  * @brief auth_logout() must invoke the registered cache-flush callback so
     327              :  *        that a follow-up resolve re-hits the server.
     328              :  */
     329            2 : static void test_logout_flushes_cache(void) {
     330            2 :     with_tmp_home("logout");
     331            2 :     mt_server_init(); mt_server_reset();
     332            2 :     resolve_cache_set_now_fn(fake_now);
     333            2 :     s_fake_time = 5000;
     334            2 :     resolve_cache_flush();
     335              : 
     336            2 :     MtProtoSession s; load_session(&s);
     337              :     /* Prime the cache. */
     338            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     339              : 
     340            2 :     ApiConfig cfg; init_cfg(&cfg);
     341            2 :     Transport t; connect_mock(&t);
     342              : 
     343            2 :     ResolvedPeer rp = {0};
     344            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp) == 0,
     345              :            "cold resolve ok");
     346            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     347              :            "after cold → 1 RPC");
     348              : 
     349              :     /* Register the production flush callback and arm a happy-path
     350              :      * auth.logOut responder so auth_logout() returns cleanly. */
     351            2 :     auth_logout_set_cache_flush_cb(resolve_cache_flush);
     352            2 :     mt_server_expect(CRC_auth_logOut, on_logout_ok, NULL);
     353              : 
     354            2 :     auth_logout(&cfg, &s, &t);
     355              : 
     356              :     /* Swap the resolver responder back in for the post-logout call. */
     357            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_user_8001, NULL);
     358              : 
     359              :     /* The cache must be empty now — the next resolve must fire an RPC. */
     360            2 :     int rpc_before = mt_server_request_crc_count(CRC_contacts_resolveUsername);
     361            2 :     ResolvedPeer rp2 = {0};
     362            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@foo", &rp2) == 0,
     363              :            "post-logout resolve ok");
     364            2 :     int rpc_after = mt_server_request_crc_count(CRC_contacts_resolveUsername);
     365            2 :     ASSERT(rpc_after == rpc_before + 1,
     366              :            "post-logout resolve issued a fresh RPC");
     367              : 
     368              :     /* Unregister the callback so later tests start clean. */
     369            2 :     auth_logout_set_cache_flush_cb(NULL);
     370              : 
     371            2 :     resolve_cache_set_now_fn(NULL);
     372            2 :     transport_close(&t);
     373            2 :     mt_server_reset();
     374              : }
     375              : 
     376              : /**
     377              :  * @brief USERNAME_NOT_OCCUPIED must be remembered: a repeat call inside
     378              :  *        the negative TTL must not re-fire the RPC; after the negative
     379              :  *        TTL elapses the RPC fires once more.
     380              :  */
     381            2 : static void test_negative_result_cached_with_shorter_ttl(void) {
     382            2 :     with_tmp_home("neg");
     383            2 :     mt_server_init(); mt_server_reset();
     384            2 :     resolve_cache_set_now_fn(fake_now);
     385            2 :     s_fake_time = 6000;
     386            2 :     resolve_cache_flush();
     387              : 
     388            2 :     MtProtoSession s; load_session(&s);
     389            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_not_occupied, NULL);
     390              : 
     391            2 :     ApiConfig cfg; init_cfg(&cfg);
     392            2 :     Transport t; connect_mock(&t);
     393              : 
     394              :     /* First miss: fires one RPC, caches the negative result. */
     395            2 :     ResolvedPeer rp1 = {0};
     396            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@nobody", &rp1) == -1,
     397              :            "1st resolve returns not-found");
     398            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     399              :            "negative cold path → 1 RPC");
     400              : 
     401              :     /* Second call within the negative TTL: still fails, NO new RPC. */
     402            2 :     s_fake_time += 5; /* +5 s, inside the negative TTL */
     403            2 :     ResolvedPeer rp2 = {0};
     404            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@nobody", &rp2) == -1,
     405              :            "2nd resolve (neg-cached) returns not-found");
     406            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 1,
     407              :            "negative cache suppressed 2nd RPC");
     408              : 
     409              :     /* Sanity: the positive TTL is strictly greater than the negative
     410              :      * TTL — otherwise the whole "shorter" premise breaks. */
     411            2 :     ASSERT(resolve_cache_positive_ttl() > resolve_cache_negative_ttl(),
     412              :            "positive TTL > negative TTL by construction");
     413              : 
     414              :     /* Fast-forward past the negative TTL: RPC must fire again. */
     415            2 :     s_fake_time += resolve_cache_negative_ttl() + 1;
     416            2 :     ResolvedPeer rp3 = {0};
     417            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@nobody", &rp3) == -1,
     418              :            "3rd resolve after neg-TTL still not-found");
     419            2 :     ASSERT(mt_server_request_crc_count(CRC_contacts_resolveUsername) == 2,
     420              :            "neg-TTL expiry triggers second RPC");
     421              : 
     422            2 :     resolve_cache_set_now_fn(NULL);
     423            2 :     transport_close(&t);
     424            2 :     mt_server_reset();
     425              : }
     426              : 
     427              : /**
     428              :  * @brief Fill the cache past its fixed capacity; the first-inserted
     429              :  *        entry must be evicted.  After eviction re-resolving the first
     430              :  *        @peer fires a fresh RPC, while one of the more recently
     431              :  *        inserted entries stays cached (zero new RPC).
     432              :  */
     433            2 : static void test_cache_eviction_oldest_first(void) {
     434            2 :     with_tmp_home("evict");
     435            2 :     mt_server_init(); mt_server_reset();
     436            2 :     resolve_cache_set_now_fn(fake_now);
     437            2 :     s_fake_time = 10000;
     438            2 :     resolve_cache_flush();
     439              : 
     440            2 :     MtProtoSession s; load_session(&s);
     441            2 :     mt_server_expect(CRC_contacts_resolveUsername, on_resolve_capture_id, NULL);
     442              : 
     443            2 :     ApiConfig cfg; init_cfg(&cfg);
     444            2 :     Transport t; connect_mock(&t);
     445              : 
     446            2 :     int cap = resolve_cache_capacity();
     447            2 :     ASSERT(cap >= 4, "cache capacity is non-trivial");
     448              : 
     449              :     /* Insert cap+1 distinct entries, advancing the clock so each entry
     450              :      * has a unique fetched_at (the eviction policy is oldest-first). */
     451           68 :     for (int i = 0; i <= cap; i++) {
     452              :         char key[16];
     453           66 :         snprintf(key, sizeof(key), "@p%d", i);
     454           66 :         ResolvedPeer rp = {0};
     455           66 :         ASSERT(domain_resolve_username(&cfg, &s, &t, key, &rp) == 0,
     456              :                "insert resolve ok");
     457           66 :         s_fake_time += 1;   /* spread fetched_at so eviction order is deterministic */
     458              :     }
     459              : 
     460              :     int rpc_after_fill =
     461            2 :         mt_server_request_crc_count(CRC_contacts_resolveUsername);
     462            2 :     ASSERT(rpc_after_fill == cap + 1,
     463              :            "cap+1 distinct peers fired cap+1 RPCs");
     464              : 
     465              :     /* The first inserted entry (@p0) must have been evicted → resolving
     466              :      * it again fires a fresh RPC. */
     467            2 :     ResolvedPeer rp_first = {0};
     468            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, "@p0", &rp_first) == 0,
     469              :            "re-resolve evicted entry ok");
     470              :     int rpc_after_re_first =
     471            2 :         mt_server_request_crc_count(CRC_contacts_resolveUsername);
     472            2 :     ASSERT(rpc_after_re_first == rpc_after_fill + 1,
     473              :            "re-resolving evicted @p0 fires RPC");
     474              : 
     475              :     /* A recently-inserted entry (@p<cap>) must still be cached → zero
     476              :      * new RPC. */
     477              :     char last_key[16];
     478            2 :     snprintf(last_key, sizeof(last_key), "@p%d", cap);
     479            2 :     ResolvedPeer rp_last = {0};
     480            2 :     ASSERT(domain_resolve_username(&cfg, &s, &t, last_key, &rp_last) == 0,
     481              :            "re-resolve recent entry ok");
     482              :     int rpc_after_re_last =
     483            2 :         mt_server_request_crc_count(CRC_contacts_resolveUsername);
     484            2 :     ASSERT(rpc_after_re_last == rpc_after_re_first,
     485              :            "recent entry still cached — no new RPC");
     486              : 
     487            2 :     resolve_cache_set_now_fn(NULL);
     488            2 :     transport_close(&t);
     489            2 :     mt_server_reset();
     490              : }
     491              : 
     492              : /* ---- Suite entry ---- */
     493              : 
     494            2 : void run_resolver_cache_tests(void) {
     495            2 :     RUN_TEST(test_cold_call_resolves_once);
     496            2 :     RUN_TEST(test_warm_call_skips_rpc);
     497            2 :     RUN_TEST(test_different_peer_does_not_hit_cache);
     498            2 :     RUN_TEST(test_ttl_expiry_refreshes);
     499            2 :     RUN_TEST(test_logout_flushes_cache);
     500            2 :     RUN_TEST(test_negative_result_cached_with_shorter_ttl);
     501            2 :     RUN_TEST(test_cache_eviction_oldest_first);
     502            2 : }
        

Generated by: LCOV version 2.0-1