Reqord.RedactCassette (reqord v0.4.0)

View Source

Macro for applying custom redaction to HTTP requests and responses within a test.

This module provides a redact_cassette macro that allows users to define custom redaction functions that are applied to cassette data during recording and replay.

Basic Usage

defmodule MyApp.APITest do
  use Reqord.Case
  import Reqord.RedactCassette

  test "fetches user data with redacted sensitive info" do
    redact_cassette redactor: :user_data do
      client = Req.new(plug: {Req.Test, MyApp.ReqStub})
      {:ok, response} = Req.get(client, url: "https://api.example.com/users/123")

      assert response.status == 200
      # Email and SSN will be redacted in cassette but preserved in test
      assert response.body["email"] =~ "@"
    end
  end

  # Define redaction function
  defp redactor(:user_data, _context) do
    %{
      response_body: fn body ->
        body
        |> Jason.decode!()
        |> put_in(["email"], "[EMAIL_REDACTED]")
        |> put_in(["ssn"], "[SSN_REDACTED]")
        |> Jason.encode!()
      end,
      request_headers: fn headers ->
        Map.put(headers, "authorization", "[AUTH_REDACTED]")
      end
    }
  end
end

Named Redactors

You can also define named redactors in config and reference them:

# config/test.exs
config :reqord,
  redactors: %{
    user_api: fn _context ->
      %{
        response_body: &MyApp.Redactors.redact_user_data/1,
        request_headers: &MyApp.Redactors.redact_auth_headers/1
      }
    end,
    financial_api: fn _context ->
      %{
        response_body: &MyApp.Redactors.redact_financial_data/1
      }
    end
  }

# Then use in tests
redact_cassette redactor: :user_api do
  # test code
end

Redaction Function Format

Redaction functions receive data and return the redacted version:

  • response_body_json: (map) -> map - Redacts decoded JSON response body
  • response_body_raw: (binary) -> binary - Redacts raw binary response body
  • request_headers: (map) -> map - Redacts request headers
  • response_headers: (map) -> map - Redacts response headers
  • url: (string) -> string - Redacts URL (including query params)

Note: For JSON responses, prefer response_body_json which automatically handles encoding/decoding using the configured JSON library. Use response_body_raw for non-JSON content or when you need full control over the binary data.

Advanced Usage

defmodule MyApp.APITest do
  use Reqord.Case
  import Reqord.RedactCassette

  test "complex redaction example" do
    redact_cassette redactor: fn _context ->
      %{
        response_body_json: &redact_nested_secrets/1,
        request_headers: fn headers ->
          headers
          |> Map.put("authorization", "[REDACTED]")
          |> Map.put("x-api-key", "[REDACTED]")
        end,
        url: fn url ->
          URI.parse(url)
          |> Map.update(:query, nil, fn query ->
            if query do
              query
              |> URI.decode_query()
              |> Map.put("token", "[REDACTED]")
              |> URI.encode_query()
            else
              nil
            end
          end)
          |> URI.to_string()
        end
      }
    end do
      # test code
    end
  end

  defp redact_nested_secrets(data) when is_map(data) do
    Enum.reduce(data, %{}, fn {key, value}, acc ->
      cond do
        key in ["api_key", "secret", "token"] ->
          Map.put(acc, key, "[REDACTED]")

        is_map(value) ->
          Map.put(acc, key, redact_nested_secrets(value))

        is_list(value) ->
          Map.put(acc, key, Enum.map(value, &redact_nested_secrets/1))

        true ->
          Map.put(acc, key, value)
      end
    end)
  end

  defp redact_nested_secrets(data), do: data
end

Integration with Reqord.Case

The macro works seamlessly with existing Reqord.Case functionality. It temporarily installs redaction hooks that are applied during cassette recording and replay.

Summary

Functions

Applies custom redaction within a test block.

Functions

redact_cassette(opts, list)

(macro)

Applies custom redaction within a test block.

Options

  • :redactor - The redaction function or named redactor to use
  • :cassette - Optional cassette name override (inherits from test context by default)

Examples

# Using a named redactor from config
redact_cassette redactor: :user_api do
  # test code
end

# Using an inline function
redact_cassette redactor: fn _context -> %{response_body: &redact_body/1} end do
  # test code
end

# With custom cassette name
redact_cassette redactor: :user_api, cassette: "custom_name" do
  # test code
end