### Installation of ReqCassette Source: https://hexdocs.pm/req_cassette Add Req and ReqCassette to your `mix.exs` dependencies to install the library. ```elixir def deps do [ {:req, "~> 0.5.15"}, {:req_cassette, "~> 0.5.0"} ] end ``` -------------------------------- ### ExUnit Setup for Shared Sessions Source: https://hexdocs.pm/req_cassette Use ExUnit's setup to manage shared sessions for multiple tests requiring them. Ensure sessions are ended on exit. ```elixir defmodule MyApp.ParallelAPITest do use ExUnit.Case, async: true import ReqCassette setup do session = ReqCassette.start_shared_session() on_exit(fn -> ReqCassette.end_shared_session(session) end) %{session: session} end test "parallel API calls", %{session: session} do with_cassette "parallel_test", [session: session], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end end end ``` -------------------------------- ### Starting and Using a Shared Session for Spawning Processes Source: https://hexdocs.pm/req_cassette Illustrates creating a shared session with `start_shared_session/0` and passing it to `with_cassette/3` for use in spawned processes. This ensures correct sequential matching across processes. Remember to call `end_shared_session/1`. ```elixir session = ReqCassette.start_shared_session() try do with_cassette "my_test", [session: session, template: [preset: :common]], fn plug -> Task.async(fn -> Req.post!(..., plug: plug) end) |> Task.await() end after ReqCassette.end_shared_session(session) end ``` -------------------------------- ### Basic Cassette Usage Source: https://hexdocs.pm/req_cassette A simple example demonstrating how to use `with_cassette` to record and replay a single HTTP request. ```elixir # Basic usage with_cassette "github_user", fn plug -> Req.get!("https://api.github.com/users/wojtekmach", plug: plug) end ``` -------------------------------- ### ReqCassette v1.0 JSON Format Example Source: https://hexdocs.pm/req_cassette Illustrates the structure of a ReqCassette v1.0 file, which is a pretty-printed JSON containing version information and a list of recorded interactions. ```json { "version": "1.0", "interactions": [ { "request": { "method": "GET", "uri": "https://api.example.com/users/1", "body_type": "text", "body": "" }, "response": { "status": 200, "body_type": "json", "body_json": { "id": 1, "name": "Alice" } }, "recorded_at": "2025-10-16T12:00:00Z" } ] } ``` -------------------------------- ### Convenience Option: Shared Session with `with_shared_cassette/3` Source: https://hexdocs.pm/req_cassette Presents an alternative helper function, `with_shared_cassette/3`, for simplifying shared session management in ReqCassette tests. This helper automatically starts and ends the shared session, making cross-process request handling more concise. ```elixir # Option 2: with_shared_cassette helper with_shared_cassette "parallel_test", fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end ``` -------------------------------- ### Problem: Cross-Process Requests Without Shared Session Source: https://hexdocs.pm/req_cassette Highlights the issue when making HTTP requests from spawned processes (e.g., Task.async) without a shared session. Each spawned process starts its own interaction count, leading to incorrect cassette matching. ```elixir # ❌ WITHOUT shared session - spawned processes don't share state with_cassette "parallel", fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) # Each task matches interaction 0 independently! end ``` -------------------------------- ### Product Lookup with SKU Templating Source: https://hexdocs.pm/req_cassette Demonstrates using ReqCassette to record and replay requests with dynamic SKUs. It uses regex patterns to extract SKUs and substitutes them during replay, allowing a single cassette to handle multiple product lookups with different SKUs. ```elixir test "product lookup with any SKU" do with_cassette "product_lookup", [ template: [ patterns: [sku: ~r/\d{4}-\d{4}/] ] ], fn plug -> # First call: Records response1 = Req.get!("https://api.example.com/products/1234-5678", plug: plug) assert response1.body["sku"] == "1234-5678" # Second call: Replays with DIFFERENT SKU! response2 = Req.get!("https://api.example.com/products/9999-8888", plug: plug) assert response2.body["sku"] == "9999-8888" # ✅ Substituted! assert response2.body["name"] == "Widget" # ✅ Same static data end end ``` -------------------------------- ### Product Lookup with Dynamic SKU Templating Source: https://hexdocs.pm/req_cassette Demonstrates recording and replaying requests with dynamic values like SKUs using templating patterns. The first call records, and the second call replays with a different SKU, showing successful substitution. ```elixir test "product lookup" do with_cassette "product", [template: [patterns: [sku: ~r/\d{4}-\d{4}/]]], fn plug -> # First call: records r1 = Req.get!("https://api.example.com/products/1234-5678", plug: plug) assert r1.body["sku"] == "1234-5678" # Second call: replays with DIFFERENT SKU! r2 = Req.get!("https://api.example.com/products/9999-8888", plug: plug) assert r2.body["sku"] == "9999-8888" # ✅ Substituted! end end ``` -------------------------------- ### start_shared_session/0 Source: https://hexdocs.pm/req_cassette Creates a shared session for cross-process cassette matching. Use this when making HTTP requests from spawned processes (Task.async, Task.async_stream, GenServer, etc.). Pass the returned session to `with_cassette/3` via the `:session` option. Always call `end_shared_session/1` when done, preferably in an `after` block. ```APIDOC ## start_shared_session() ### Description Creates a shared session for cross-process cassette matching. Use this when making HTTP requests from spawned processes (`Task.async`, `Task.async_stream`, `GenServer`, etc.). Pass the returned session to `with_cassette/3` via the `:session` option. Always call `end_shared_session/1` when done, preferably in an `after` block. ### Example ```elixir session = ReqCassette.start_shared_session() try do with_cassette "my_test", [session: session, template: [preset: :common]], fn plug -> Task.async(fn -> Req.post!(..., plug: plug) end) |> Task.await() end after ReqCassette.end_shared_session(session) end ``` ### Why is this needed? By default, sequential matching state is stored in the process dictionary, which is per-process. Spawned processes can't see or update the parent's state, so each would match from interaction 0. Shared sessions use an Agent process for cross-process state sharing, allowing all processes to coordinate sequential matching correctly. ``` -------------------------------- ### Solution: Cross-Process Requests With Shared Session Source: https://hexdocs.pm/req_cassette Demonstrates the correct way to handle cross-process HTTP requests with ReqCassette by using `start_shared_session/0` and `end_shared_session/1`. This ensures all spawned processes share the same cassette interaction state. ```elixir # ✅ WITH shared session - all processes share state session = ReqCassette.start_shared_session() try do with_cassette "parallel", [session: session], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) # Tasks correctly get interactions 0, 1, 2 (in execution order) end after ReqCassette.end_shared_session(session) end ``` -------------------------------- ### Basic Usage of ReqCassette Source: https://hexdocs.pm/req_cassette Use `with_cassette/3` to record and replay HTTP requests. The first run records to a cassette file, and subsequent runs replay from it, eliminating network calls. ```elixir import ReqCassette test "fetches user data" do with_cassette "github_user", fn plug -> response = Req.get!("https://api.github.com/users/wojtekmach", plug: plug) assert response.status == 200 assert response.body["login"] == "wojtekmach" end end ``` -------------------------------- ### Cassette Usage with Options Source: https://hexdocs.pm/req_cassette Shows how to configure cassette recording behavior using options like `:mode` and `:match_requests_on`. ```elixir # With options with_cassette "api_call", mode: :replay, match_requests_on: [:method, :uri], fn plug -> Req.get!("https://api.example.com/data", plug: plug) end ``` -------------------------------- ### Executing Code with Explicit Plug Control Source: https://hexdocs.pm/req_cassette Demonstrates using `with_cassette/3` to execute code with explicit control over the plug. The plug is passed as an argument to the provided function, useful for helper functions, reusable utilities, and functional programming styles. ```elixir with_cassette(name, opts, fun) ``` -------------------------------- ### Default Request Matching Source: https://hexdocs.pm/req_cassette Demonstrates default first-match behavior where identical requests always return the same response. ```elixir with_cassette "api_test", fn plug -> Req.get!("/users/1", plug: plug) # → Alice Req.get!("/users/2", plug: plug) # → Bob Req.get!("/users/1", plug: plug) # → Alice (same as first call) end ``` -------------------------------- ### Passing Plug to Helper Functions Source: https://hexdocs.pm/req_cassette Illustrates how to pass the cassette plug to helper functions within the `with_cassette` block to manage external API calls. ```elixir # Pass plug to helper functions with_cassette "api_operations", fn plug -> user = MyApp.API.fetch_user(1, plug: plug) new_user = MyApp.API.create_user(%{name: "Bob"}, plug: plug) {user, new_user} end ``` -------------------------------- ### Execute Code with Shared Cassette Session Source: https://hexdocs.pm/req_cassette Use `with_shared_cassette` as a cleaner alternative to manually managing shared sessions with `ReqCassette.start_shared_session/0` and `ReqCassette.end_shared_session/1`. This is recommended for tests involving concurrent processes. ```elixir with_shared_cassette "parallel_api", [template: [preset: :common]], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end ``` -------------------------------- ### with_cassette/3 Source: https://hexdocs.pm/req_cassette Execute code with a cassette, providing the plug explicitly. This function gives you explicit control over where and how the plug is used, which is useful for passing the plug to helper functions, building reusable test utilities, or adopting a functional programming style. ```APIDOC ## with_cassette(name, opts, fun) ### Description Execute code with a cassette, providing the plug explicitly. Unlike `use_cassette/2` which auto-injects the plug, `with_cassette/3` provides the plug configuration as an argument to your function, giving you explicit control over where and how it's used. This is particularly useful for: * Passing plug to helper functions * Building reusable test utilities * Functional programming style * Clear visibility of what's being recorded ### Parameters * `name` - Human-readable cassette name (e.g., "github_user") * `opts` - Keyword list of options (optional) * `fun` - Function that takes the plug and returns a result ``` -------------------------------- ### Nested Cassettes for Different APIs Source: https://hexdocs.pm/req_cassette Demonstrates nesting `with_cassette` blocks to manage interactions with multiple distinct APIs, each with its own cassette. ```elixir # Nested cassettes for different APIs with_cassette "github", fn github_plug -> user = Req.get!("https://api.github.com/users/alice", plug: github_plug) with_cassette "stripe", fn stripe_plug -> charge = Req.post!( "https://api.stripe.com/v1/charges", json: %{amount: 1000}, plug: stripe_plug ) {user, charge} end end ``` -------------------------------- ### ReqLLM Integration for Cost Savings Source: https://hexdocs.pm/req_cassette Use ReqCassette with ReqLLM to save costs on LLM API calls during testing. The first call records the interaction, subsequent runs replay from the cassette for free. ```elixir test "LLM generation" do with_cassette "claude_response", fn plug -> {:ok, response} = ReqLLM.generate_text( "anthropic:claude-sonnet-4-20250514", "Explain recursion", max_tokens: 100, req_http_options: [plug: plug] ) assert response.choices[0].message.content =~ "function calls itself" end end ``` -------------------------------- ### with_cassette/3 Source: https://hexdocs.pm/req_cassette Execute code with a cassette, filtering sensitive data and recording requests. ```APIDOC ## with_cassette/3 ### Description Execute code with a cassette, filtering sensitive data and recording requests. ### Parameters * `name` - Human-readable cassette name * `opts` - Keyword list of options, including `filter_request_headers` and `filter_sensitive_data`. * `fun` - Function that takes the plug and returns a result. ### Example ```elixir with_cassette "auth", filter_request_headers: ["authorization"], filter_sensitive_data: [{~r/api_key=[\w-]+/, "api_key="}], fn plug -> Req.post!("https://api.example.com/login", json: %{username: "alice", password: "secret"}, plug: plug) end ``` ``` -------------------------------- ### with_shared_cassette/3 Source: https://hexdocs.pm/req_cassette Execute code with a cassette using a shared session for cross-process support. Handles session start/end boilerplate. ```APIDOC ## with_shared_cassette/3 ### Description Execute code with a cassette using a shared session for cross-process support. This is a convenience wrapper that handles the try/after boilerplate for shared sessions. Use this when your tests spawn processes that make HTTP requests (Task.async, GenServer, etc.). ### Parameters * `name` - Human-readable cassette name * `opts` - Keyword list of options (same as `with_cassette/3`, but `:session` is auto-managed) * `fun` - Function that takes the plug and returns a result ### Example ```elixir # Before (verbose): session = ReqCassette.start_shared_session() try do with_cassette "parallel_api", [session: session, template: [preset: :common]], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end after ReqCassette.end_shared_session(session) end # After (clean): with_shared_cassette "parallel_api", [template: [preset: :common]], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end ``` ### When to Use Use `with_shared_cassette` when: * Using `Task.async/1` or `Task.async_stream/3` * Making requests from a GenServer * Using `spawn/1` or `spawn_link/1` * Any HTTP request from a different process For single-process tests, regular `with_cassette/3` is sufficient. ``` -------------------------------- ### Cross-Process Requests with Shared Session Source: https://hexdocs.pm/req_cassette Utilize `with_cassette` with a shared session to manage HTTP requests made from multiple asynchronous tasks. This ensures that all requests within the scope are recorded or replayed using the same cassette and session. ```elixir session = ReqCassette.start_shared_session() try do with_cassette "parallel_api", [session: session], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end after ReqCassette.end_shared_session(session) end ``` -------------------------------- ### LLM Chat with Varying Conversation IDs Source: https://hexdocs.pm/req_cassette Illustrates using ReqCassette to test LLM API interactions where conversation IDs change. It demonstrates how to template both `conversation_id` and `message_id` to ensure the cassette replays correctly despite dynamic identifiers. ```elixir test "LLM chat with varying conversation IDs" do with_cassette "llm_chat", [ filter_request_headers: ["authorization"], # Security first! template: [ patterns: [ conversation_id: ~r/conv_[a-zA-Z0-9]+/, message_id: ~r/msg_[a-zA-Z0-9]+/ ] ] ], fn plug -> # Different conversation IDs - same cassette! {:ok, response} = ReqLLM.generate_text( "anthropic:claude-sonnet-4-20250514", "Explain recursion", conversation_id: "conv_xyz789", # Works with any ID req_http_options: [plug: plug] ) assert response.choices[0].message.content =~ "function calls itself" end end ``` -------------------------------- ### Helper Function for Reusable API Calls Source: https://hexdocs.pm/req_cassette Define helper functions to encapsulate reusable API calls, making it easy to pass the plug to them within `with_cassette` blocks. ```elixir defmodule MyApp.API do def fetch_user(id, opts \ []) do Req.get!("https://api.example.com/users/#{id}", plug: opts[:plug]) end end test "user operations" do with_cassette "user_workflow", fn plug -> user = MyApp.API.fetch_user(1, plug: plug) assert user.body["id"] == 1 end end ``` -------------------------------- ### Cross-Process Sequential Matching Source: https://hexdocs.pm/req_cassette Facilitates sequential matching across multiple processes by using a shared session, essential for concurrent operations. ```elixir session = ReqCassette.start_shared_session() try do with_cassette "my_test", [session: session, sequential: true], fn plug -> # All requests share the session, even from spawned processes tasks = for i <- 1..3 do Task.async(fn -> Req.post!("https://api.example.com", plug: plug, json: %{id: i}) end) end Task.await_many(tasks) end after ReqCassette.end_shared_session(session) end ``` -------------------------------- ### Common ReqCassette Templating Patterns Source: https://hexdocs.pm/req_cassette Provides a collection of common regex patterns for templating dynamic values in ReqCassette. These patterns can be used to match and substitute various data types like SKUs, UUIDs, timestamps, and conversation IDs. ```elixir template: [ patterns: [ # Product SKUs sku: ~r/\d{4}-\d{4}/, # UUIDs uuid: ~r/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i, # Timestamps timestamp: ~r/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z/, # LLM conversation IDs conversation_id: ~r/conv_[a-zA-Z0-9]+/ ] ] ``` -------------------------------- ### Convenience Option: Shared Session with `shared: true` Source: https://hexdocs.pm/req_cassette Shows a shorthand for managing shared sessions in ReqCassette tests. Setting `shared: true` in the options automatically handles the lifecycle of a shared session for cross-process requests. ```elixir # Option 1: shared: true shorthand with_cassette "parallel_test", [shared: true], fn plug -> tasks = for i <- 1..3 do Task.async(fn -> Req.get!("https://api.example.com/#{i}", plug: plug) end) end Task.await_many(tasks) end ``` -------------------------------- ### ReqCassette Recording Modes Source: https://hexdocs.pm/req_cassette Control recording and replaying behavior using the `mode` option. Use `:record` to record if the cassette is missing, `:replay` to only replay (error if missing), and `:bypass` to always use the network. ```elixir # :record (default) - Record if cassette doesn't exist or interaction not found, otherwise replay with_cassette "api_call", [mode: :record], fn plug -> Req.get!("https://api.example.com/data", plug: plug) end ``` ```elixir # :replay - Only replay from cassette, error if missing (great for CI) with_cassette "api_call", [mode: :replay], fn plug -> Req.get!("https://api.example.com/data", plug: plug) end ``` ```elixir # :bypass - Ignore cassettes entirely, always use network with_cassette "api_call", [mode: :bypass], fn plug -> Req.get!("https://api.example.com/data", plug: plug) end ``` ```elixir # To re-record a cassette: delete it first File.rm!("test/cassettes/api_call.json") with_cassette "api_call", [mode: :record], fn plug -> Req.get!("https://api.example.com/data", plug: plug) end ``` -------------------------------- ### Ending a Shared Session Source: https://hexdocs.pm/req_cassette Shows how to properly end a shared session using `end_shared_session/1`. This should be called in an `after` block to ensure cleanup even if errors occur within the `try` block. ```elixir session = ReqCassette.start_shared_session() try do with_cassette "my_test", [session: session], fn plug -> ... end after ReqCassette.end_shared_session(session) end ``` -------------------------------- ### Sequential Request Matching Source: https://hexdocs.pm/req_cassette Enables sequential matching for scenarios where identical requests expect different responses, such as polling APIs or state changes. ```elixir # Polling API that returns different states over time with_cassette "polling_test", [sequential: true], fn plug -> Req.get!("/job/status", plug: plug) # → {"status": "pending"} Req.get!("/job/status", plug: plug) # → {"status": "running"} Req.get!("/job/status", plug: plug) # → {"status": "completed"} end ``` -------------------------------- ### Dangerous before_record Anti-Pattern Source: https://hexdocs.pm/req_cassette Demonstrates a dangerous anti-pattern of modifying request fields within the :before_record hook, which breaks replay matching. Use filter_request instead for request modifications. ```elixir with_cassette "api_call", [ before_record: fn interaction -> # ❌ DANGER: Modifying request breaks replay matching! update_in(interaction, ["request", "body_json", "timestamp"], fn _ -> "" end) end ], fn plug -> # This will fail on replay - request won't match saved cassette! Req.post!("https://api.example.com/data", json: %{...}, plug: plug) end ``` -------------------------------- ### Safe Response Enrichment with before_record Hook Source: https://hexdocs.pm/req_cassette Safely enrich response data using the :before_record hook by only modifying the response based on request data. Avoid modifying request fields to prevent replay mismatches. ```elixir with_cassette "api_call", [ before_record: fn interaction -> # ✅ SAFE: Only modifying response based on request request_id = interaction["request"]["body_json"]["id"] put_in( interaction, ["response", "body_json", "request_ref"], request_id ) end ], fn plug -> Req.post!("https://api.example.com/process", json: %{id: 123}, plug: plug) end ``` -------------------------------- ### Correct Request Filtering with filter_request Source: https://hexdocs.pm/req_cassette Correctly modifies request data using the `filter_request` option, which is applied during both recording and matching. This ensures replay compatibility. ```elixir with_cassette "api_call", [ # ✅ CORRECT: filter_request is applied during both recording and matching filter_request: fn request -> update_in(request, ["body_json", "timestamp"], fn _ -> "" end) end ], fn plug -> Req.post!("https://api.example.com/data", json: %{...}, plug: plug) end ``` -------------------------------- ### Filter Sensitive Data in Requests Source: https://hexdocs.pm/req_cassette Use `with_cassette` to filter sensitive request headers like 'authorization' and redact specific patterns like API keys from the cassette data. This is useful for security and privacy when recording requests. ```elixir with_cassette "auth", filter_request_headers: ["authorization"], filter_sensitive_data: [ {~r/api_key=[\w-]+/, "api_key="} ], fn plug -> Req.post!("https://api.example.com/login", json: %{username: "alice", password: "secret"}, plug: plug) end ``` -------------------------------- ### end_shared_session/1 Source: https://hexdocs.pm/req_cassette Ends a shared session by stopping its Agent process. Should be called in an `after` block to ensure cleanup even on errors. ```APIDOC ## end_shared_session(session) ### Description Ends a shared session by stopping its Agent process. Should be called in an `after` block to ensure cleanup even on errors. ### Example ```elixir session = ReqCassette.start_shared_session() try do with_cassette "my_test", [session: session], fn plug -> ... end after ReqCassette.end_shared_session(session) end ``` ``` -------------------------------- ### ReqCassette Sensitive Data Filtering Source: https://hexdocs.pm/req_cassette Protect sensitive data in requests and responses using various filtering options. Filters include regex replacement, header removal, and custom request/response callbacks. ```elixir with_cassette "auth", [ filter_request_headers: ["authorization", "x-api-key"], filter_response_headers: ["set-cookie"], filter_sensitive_data: [ {~r/api_key=[\w-]+/, "api_key="} ], filter_request: fn request -> update_in(request, ["body_json", "timestamp"], fn _ -> "" end) end, filter_response: fn response -> update_in(response, ["body_json", "secret"], fn _ -> "" end) end ], fn plug -> Req.post!("https://api.example.com/login", json: %{...}, plug: plug) end ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.