Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/gems.locked
/.covered.db
/external
/vendor
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