Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions examples/recorder/config.ru
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
# Simple Rack application demonstrating async-http-capture recording
# This application provides several endpoints that generate different types of responses

require 'json'
require 'time'
require "json"
require "time"

# Simple hello world application with multiple endpoints
app = lambda do |env|
request = Rack::Request.new(env)

case request.path_info
when '/'
when "/"
# Simple homepage
body = <<~HTML
<html>
Expand All @@ -39,25 +39,25 @@ app = lambda do |env|
</html>
HTML

[200, {'Content-Type' => 'text/html'}, [body]]
[200, {"Content-Type" => "text/html"}, [body]]

when '/hello'
when "/hello"
# Simple text response
[200, {'Content-Type' => 'text/plain'}, ["Hello, World! #{Time.now}"]]
[200, {"Content-Type" => "text/plain"}, ["Hello, World! #{Time.now}"]]

when '/json'
when "/json"
# JSON response
data = {
message: "Hello from JSON API",
timestamp: Time.now.iso8601,
random: rand(1000)
}
[200, {'Content-Type' => 'application/json'}, [JSON.generate(data)]]
[200, {"Content-Type" => "application/json"}, [JSON.generate(data)]]

when '/headers'
when "/headers"
# Show request headers
headers_info = env.select { |k, _| k.start_with?('HTTP_') }
.transform_keys { |k| k.sub('HTTP_', '').split('_').map(&:capitalize).join('-') }
headers_info = env.select {|k, _| k.start_with?("HTTP_")}
.transform_keys {|k| k.sub("HTTP_", "").split("_").map(&:capitalize).join("-")}

response = {
method: request.request_method,
Expand All @@ -66,11 +66,11 @@ app = lambda do |env|
headers: headers_info
}

[200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(response)]]
[200, {"Content-Type" => "application/json"}, [JSON.pretty_generate(response)]]

when '/post'
when "/post"
# Handle POST requests
if request.request_method == 'POST'
if request.request_method == "POST"
# Read request body
body = request.body.read

Expand All @@ -82,30 +82,30 @@ app = lambda do |env|
timestamp: Time.now.iso8601
}

[200, {'Content-Type' => 'application/json'}, [JSON.pretty_generate(response)]]
[200, {"Content-Type" => "application/json"}, [JSON.pretty_generate(response)]]
else
[405, {'Content-Type' => 'text/plain'}, ['Method Not Allowed - Use POST']]
[405, {"Content-Type" => "text/plain"}, ["Method Not Allowed - Use POST"]]
end

when '/error'
when "/error"
# Generate an error for testing error recording
raise "Intentional error for testing"

when '/slow'
when "/slow"
# Simulate a slow response
sleep(1)
[200, {'Content-Type' => 'text/plain'}, ["Slow response after 1 second delay"]]
[200, {"Content-Type" => "text/plain"}, ["Slow response after 1 second delay"]]

else
# 404 Not Found
[404, {'Content-Type' => 'text/plain'}, ['Not Found']]
[404, {"Content-Type" => "text/plain"}, ["Not Found"]]
end
end

# If running with plain rackup (not Falcon), we can add the middleware here
# But when using Falcon with falcon.rb, the middleware is configured there
if ENV['CAPTURE_ENABLED'] == 'true'
require 'async/http/capture'
if ENV["CAPTURE_ENABLED"] == "true"
require "async/http/capture"

# Create stores for recording
store = Async::HTTP::Capture::CassetteStore.new("recordings")
Expand Down
12 changes: 6 additions & 6 deletions examples/recorder/falcon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
# Configure the host and port for the server
service "recorder-example" do
include Falcon::Environment::Application

# Bind to localhost on port 9292
endpoint Async::HTTP::Endpoint.parse(
"http://localhost:9292"
)

# This is where we set up the recording middleware
# The middleware will automatically capture all requests and responses
middleware do
Expand All @@ -28,24 +28,24 @@
store.call(interaction)
console_store.call(interaction)
end

application = Protocol::HTTP::Middleware.build do
run ::Protocol::HTTP::Middleware::HelloWorld
end

middleware = Async::HTTP::Capture::Middleware.new(
application,
store: combined_store
)

# Replay interactions:
cassette = store.cassette
cassette.each do |interaction|
puts "Replaying interaction: #{interaction}"
response = application.call(interaction.request)
response.finish
end

middleware
end
end
2 changes: 2 additions & 0 deletions examples/recorder/gems.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem "falcon"
Expand Down
2 changes: 1 addition & 1 deletion lib/async/http/capture/interaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def serialize

data
end

# Provide a reasonable string representation of this interaction.
# @returns [String] A human-readable description of the interaction.
def to_s
Expand Down
13 changes: 13 additions & 0 deletions lib/async/http/capture/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ def initialize(app, store:)
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
# @returns [Protocol::HTTP::Response] The response from the next middleware.
def call(request)
# Check if we should capture this request:
unless capture?(request)
return super(request)
end

# Create completion tracker for this interaction:
tracker = create_interaction_tracker(request)

Expand All @@ -42,6 +47,14 @@ def call(request)
capture_response_with_completion(captured_request, response, tracker)
end

# Determine whether to capture the given request.
# Override this method in subclasses to implement custom filtering logic.
# @parameter request [Protocol::HTTP::Request] The incoming HTTP request.
# @returns [Boolean] True if the request should be captured, false otherwise.
def capture?(request)
true
end

private

# Create a completion tracker for a single interaction.
Expand Down
78 changes: 78 additions & 0 deletions test/async/http/capture/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,82 @@
expect(interaction.to_h[:response][:status]).to be == 200
end
end

with "#capture?" do
let(:get_request) do
Protocol::HTTP::Request["GET", "/test", {"User-Agent" => "Test"}]
end

let(:post_request) do
Protocol::HTTP::Request[
"POST",
"/users",
{"Content-Type" => "application/json"},
['{"name": "John"}']
]
end

let(:selective_middleware) do
Class.new(subject) do
def initialize(app, store:)
super(app, store: store)
@captured_requests = []
end

# Custom filtering logic: only capture GET requests
def capture?(request)
request.method == "GET"
end
end
end

it "defaults to true for base middleware" do
middleware = subject.new(simple_app, store: cassette_store)
expect(middleware.capture?(get_request)).to be == true
expect(middleware.capture?(post_request)).to be == true
end

it "can be overridden to filter requests" do
middleware = selective_middleware.new(simple_app, store: cassette_store)
expect(middleware.capture?(get_request)).to be == true
expect(middleware.capture?(post_request)).to be == false
end

it "skips capturing when capture? returns false" do
middleware = selective_middleware.new(simple_app, store: cassette_store)

# Make a POST request - should not be captured
response = middleware.call(post_request)
response.body.each {|chunk|} if response.body # Consume body

# Verify no interactions were recorded:
expect(File).not.to be(:directory?, cassette_path)

# Make a GET request - should be captured
response = middleware.call(get_request)
response.body.each {|chunk|} if response.body # Consume body

# Verify one interaction was recorded:
expect(File).to be(:directory?, cassette_path)
cassette = Async::HTTP::Capture::Cassette.load(cassette_path)
expect(cassette.interactions).to have_attributes(length: be == 1)
expect(cassette.interactions.first.to_h[:request][:method]).to be == "GET"
end

it "still processes requests normally when not capturing" do
middleware = selective_middleware.new(echo_app, store: cassette_store)

# POST request should not be captured but should still work normally
response = middleware.call(post_request)

# Echo app should still return the request body:
body_content = []
response.body.each {|chunk| body_content << chunk}
expect(body_content).to be == ['{"name": "John"}']
expect(response.status).to be == 200

# But no interaction should be recorded:
expect(File).not.to be(:directory?, cassette_path)
end
end
end
Loading