-module(telega@testing@mock). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/telega/testing/mock.gleam"). -export([ok_response/1, bool_response/0, message_response/0, client/0, message_client/0, client_with/1, route/2, route_with_body/2, route_with_response/2, routed_client/1, stateful_client/1, get_calls/1, assert_call_count/2, assert_no_calls/1, assert_called_with_path/2, assert_called_with_body/3]). -export_type([api_call/0, mock_route/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Mock client and assertion utilities for testing bot API interactions.\n" "\n" " ```gleam\n" " import telega/testing/mock\n" "\n" " let #(client, calls) = mock.client()\n" " // ... use client in your bot setup ...\n" " mock.assert_call_count(from: calls, expected: 1)\n" " ```\n" ). -type api_call() :: {api_call, gleam@http@request:request(binary())}. -type mock_route() :: {mock_route, binary(), fun((gleam@http@request:request(binary())) -> {ok, gleam@http@response:response(binary())} | {error, telega@error:telega_error()})}. -file("src/telega/testing/mock.gleam", 32). ?DOC(" Wraps a `Json` value into `{\"ok\":true,\"result\":...}` Telegram API response format.\n"). -spec ok_response(gleam@json:json()) -> binary(). ok_response(Result) -> gleam@json:to_string( gleam@json:object( [{<<"ok"/utf8>>, gleam@json:bool(true)}, {<<"result"/utf8>>, Result}] ) ). -file("src/telega/testing/mock.gleam", 38). ?DOC( " Returns a `{\"ok\":true,\"result\":true}` response string.\n" " Use for endpoints that return a boolean (e.g. `answerCallbackQuery`, `deleteMessage`).\n" ). -spec bool_response() -> binary(). bool_response() -> ok_response(gleam@json:bool(true)). -file("src/telega/testing/mock.gleam", 44). ?DOC( " Returns a JSON string representing a valid `{\"ok\":true,\"result\":{...message...}}` response.\n" " Useful for building custom mock clients that need to return valid Message responses.\n" ). -spec message_response() -> binary(). message_response() -> ok_response( telega@model@encoder:encode_message( telega@testing@factory:message(<<""/utf8>>) ) ). -file("src/telega/testing/mock.gleam", 52). ?DOC( " Creates a mock Telegram client that records all API calls\n" " and returns 200 OK with an empty successful response.\n" " Note: This response will NOT decode as a valid Message.\n" " Use `message_client()` if your handlers call reply functions.\n" ). -spec client() -> {telega@client:telegram_client(), gleam@erlang@process:subject(api_call())}. client() -> Calls = gleam@erlang@process:new_subject(), Fetch_client = fun(Req) -> gleam@erlang@process:send(Calls, {api_call, Req}), {ok, begin _pipe = gleam@http@response:new(200), gleam@http@response:set_body( _pipe, <<"{\"ok\":true,\"result\":{}}"/utf8>> ) end} end, Telegram_client = telega@client:new(<<"test_token"/utf8>>, Fetch_client), {Telegram_client, Calls}. -file("src/telega/testing/mock.gleam", 65). ?DOC( " Creates a mock Telegram client that records all API calls\n" " and returns 200 OK with a valid Message response.\n" " Use this when your handlers call `reply.with_text` or other reply functions.\n" ). -spec message_client() -> {telega@client:telegram_client(), gleam@erlang@process:subject(api_call())}. message_client() -> Response_body = message_response(), Calls = gleam@erlang@process:new_subject(), Fetch_client = fun(Req) -> gleam@erlang@process:send(Calls, {api_call, Req}), {ok, begin _pipe = gleam@http@response:new(200), gleam@http@response:set_body(_pipe, Response_body) end} end, Telegram_client = telega@client:new(<<"test_token"/utf8>>, Fetch_client), {Telegram_client, Calls}. -file("src/telega/testing/mock.gleam", 77). ?DOC(" Creates a mock client with a custom response handler.\n"). -spec client_with( fun((gleam@http@request:request(binary())) -> {ok, gleam@http@response:response(binary())} | {error, telega@error:telega_error()}) ) -> {telega@client:telegram_client(), gleam@erlang@process:subject(api_call())}. client_with(Handler) -> Calls = gleam@erlang@process:new_subject(), Fetch_client = fun(Req) -> gleam@erlang@process:send(Calls, {api_call, Req}), Handler(Req) end, Telegram_client = telega@client:new(<<"test_token"/utf8>>, Fetch_client), {Telegram_client, Calls}. -file("src/telega/testing/mock.gleam", 104). ?DOC( " Creates a route that matches requests whose path contains `path_contains`\n" " and delegates to the given handler function.\n" ). -spec route( binary(), fun((gleam@http@request:request(binary())) -> {ok, gleam@http@response:response(binary())} | {error, telega@error:telega_error()}) ) -> mock_route(). route(Path_contains, Handler) -> {mock_route, Path_contains, Handler}. -file("src/telega/testing/mock.gleam", 114). ?DOC( " Creates a route that matches requests whose path contains `path_contains`\n" " and returns a 200 OK with the given body string.\n" ). -spec route_with_body(binary(), binary()) -> mock_route(). route_with_body(Path_contains, Body) -> {mock_route, Path_contains, fun(_) -> {ok, begin _pipe = gleam@http@response:new(200), gleam@http@response:set_body(_pipe, Body) end} end}. -file("src/telega/testing/mock.gleam", 126). ?DOC( " Creates a route that matches requests whose path contains `path_contains`\n" " and returns a 200 OK with the given response string (from `ok_response`, `bool_response`, etc.).\n" " Prefer this over `route_with_body` for type-safe response building.\n" ). -spec route_with_response(binary(), binary()) -> mock_route(). route_with_response(Path_contains, Response_body) -> {mock_route, Path_contains, fun(_) -> {ok, begin _pipe = gleam@http@response:new(200), gleam@http@response:set_body(_pipe, Response_body) end} end}. -file("src/telega/testing/mock.gleam", 154). -spec find_matching_route( list(mock_route()), gleam@http@request:request(binary()) ) -> {ok, mock_route()} | {error, nil}. find_matching_route(Routes, Req) -> gleam@list:find( Routes, fun(R) -> gleam_stdlib:contains_string( erlang:element(8, Req), erlang:element(2, R) ) end ). -file("src/telega/testing/mock.gleam", 138). ?DOC( " Creates a mock client that routes requests through a list of `MockRoute`s.\n" " Routes are checked in order; first match wins. Unmatched requests fall back\n" " to a default `message_response()`.\n" ). -spec routed_client(list(mock_route())) -> {telega@client:telegram_client(), gleam@erlang@process:subject(api_call())}. routed_client(Routes) -> Default_body = message_response(), Calls = gleam@erlang@process:new_subject(), Fetch_client = fun(Req) -> gleam@erlang@process:send(Calls, {api_call, Req}), case find_matching_route(Routes, Req) of {ok, Matched_route} -> (erlang:element(3, Matched_route))(Req); {error, _} -> {ok, begin _pipe = gleam@http@response:new(200), gleam@http@response:set_body(_pipe, Default_body) end} end end, Telegram_client = telega@client:new(<<"test_token"/utf8>>, Fetch_client), {Telegram_client, Calls}. -file("src/telega/testing/mock.gleam", 167). ?DOC( " Creates a mock client that passes a 0-based call counter to the handler.\n" " Useful for returning different responses based on call order.\n" ). -spec stateful_client( fun((gleam@http@request:request(binary()), integer()) -> {ok, gleam@http@response:response(binary())} | {error, telega@error:telega_error()}) ) -> {telega@client:telegram_client(), gleam@erlang@process:subject(api_call())}. stateful_client(Handler) -> Calls = gleam@erlang@process:new_subject(), Counter = gleam@erlang@process:new_subject(), gleam@erlang@process:send(Counter, 0), Fetch_client = fun(Req) -> N@1 = case gleam@erlang@process:'receive'(Counter, 1000) of {ok, N} -> N; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"telega/testing/mock"/utf8>>, function => <<"stateful_client"/utf8>>, line => 175, value => _assert_fail, start => 6324, 'end' => 6373, pattern_start => 6335, pattern_end => 6340}) end, gleam@erlang@process:send(Counter, N@1 + 1), gleam@erlang@process:send(Calls, {api_call, Req}), Handler(Req, N@1) end, Telegram_client = telega@client:new(<<"test_token"/utf8>>, Fetch_client), {Telegram_client, Calls}. -file("src/telega/testing/mock.gleam", 189). -spec drain_calls(gleam@erlang@process:subject(api_call()), list(api_call())) -> list(api_call()). drain_calls(Subject, Acc) -> case gleam@erlang@process:'receive'(Subject, 0) of {ok, Call} -> drain_calls(Subject, [Call | Acc]); {error, _} -> lists:reverse(Acc) end. -file("src/telega/testing/mock.gleam", 185). ?DOC(" Drains all recorded API calls from the subject.\n"). -spec get_calls(gleam@erlang@process:subject(api_call())) -> list(api_call()). get_calls(Subject) -> drain_calls(Subject, []). -file("src/telega/testing/mock.gleam", 197). ?DOC(" Asserts the number of recorded API calls equals the expected count.\n"). -spec assert_call_count(gleam@erlang@process:subject(api_call()), integer()) -> list(api_call()). assert_call_count(Subject, Expected) -> Calls = get_calls(Subject), Count = erlang:length(Calls), case Count =:= Expected of true -> Calls; false -> erlang:error(#{gleam_error => panic, message => (<<<<<<"Expected "/utf8, (gleam@string:inspect(Expected))/binary>>/binary, " API calls, got "/utf8>>/binary, (gleam@string:inspect(Count))/binary>>), file => <>, module => <<"telega/testing/mock"/utf8>>, function => <<"assert_call_count"/utf8>>, line => 206}) end. -file("src/telega/testing/mock.gleam", 216). ?DOC(" Asserts no API calls were recorded.\n"). -spec assert_no_calls(gleam@erlang@process:subject(api_call())) -> nil. assert_no_calls(Subject) -> Calls = get_calls(Subject), case gleam@list:is_empty(Calls) of true -> nil; false -> erlang:error(#{gleam_error => panic, message => (<<"Expected no API calls, got "/utf8, (gleam@string:inspect(erlang:length(Calls)))/binary>>), file => <>, module => <<"telega/testing/mock"/utf8>>, function => <<"assert_no_calls"/utf8>>, line => 221}) end. -file("src/telega/testing/mock.gleam", 228). ?DOC(" Asserts at least one API call was made to a path containing the given string.\n"). -spec assert_called_with_path( gleam@erlang@process:subject(api_call()), binary() ) -> api_call(). assert_called_with_path(Subject, Path_contains) -> Calls = get_calls(Subject), case gleam@list:find( Calls, fun(Call) -> gleam_stdlib:contains_string( erlang:element(8, erlang:element(2, Call)), Path_contains ) end ) of {ok, Call@1} -> Call@1; {error, _} -> erlang:error(#{gleam_error => panic, message => (<<<<<<"No API call found with path containing '"/utf8, Path_contains/binary>>/binary, "'. Calls: "/utf8>>/binary, (gleam@string:inspect(Calls))/binary>>), file => <>, module => <<"telega/testing/mock"/utf8>>, function => <<"assert_called_with_path"/utf8>>, line => 240}) end. -file("src/telega/testing/mock.gleam", 251). ?DOC( " Asserts at least one API call was made to a path containing `path_contains`\n" " and whose request body contains `body_contains`.\n" ). -spec assert_called_with_body( gleam@erlang@process:subject(api_call()), binary(), binary() ) -> api_call(). assert_called_with_body(Subject, Path_contains, Body_contains) -> Calls = get_calls(Subject), case gleam@list:find( Calls, fun(Call) -> gleam_stdlib:contains_string( erlang:element(8, erlang:element(2, Call)), Path_contains ) andalso gleam_stdlib:contains_string( erlang:element(4, erlang:element(2, Call)), Body_contains ) end ) of {ok, Call@1} -> Call@1; {error, _} -> erlang:error(#{gleam_error => panic, message => (<<<<<<<<<<"No API call found with path containing '"/utf8, Path_contains/binary>>/binary, "' and body containing '"/utf8>>/binary, Body_contains/binary>>/binary, "'. Calls: "/utf8>>/binary, (gleam@string:inspect(Calls))/binary>>), file => <>, module => <<"telega/testing/mock"/utf8>>, function => <<"assert_called_with_body"/utf8>>, line => 265}) end.