Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YAML, BSON, and MessagePack serialization formats #8

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
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
63 changes: 55 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,34 +27,38 @@ Or install it yourself as:

## Usage

### HTTP
```ruby
require 'test/unit'
require 'tcr'

TCR.configure do |c|
c.cassette_library_dir = 'fixtures/tcr_cassettes'
c.hook_tcp_ports = [25]
c.hook_tcp_ports = [80]
end

class TCRTest < Test::Unit::TestCase
def test_example_dot_com
TCR.use_cassette('google_smtp') do
tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
io = Net::InternetMessageIO.new(tcp_socket)
assert_match /220 mx.google.com ESMTP/, io.readline
TCR.use_cassette('google') do
data = Net::HTTP.get("google.com", "/")
assert_match /301 Moved/, data
end
end
end
```

Run this test once, and TCR will record the tcp interactions to fixtures/tcr_cassettes/google_smtp.json.
Run this test once, and TCR will record the tcp interactions to fixtures/tcr_cassettes/google.json.

```json
[
[
[
"write",
"GET / HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: google.com\r\n\r\n"
],
[
"read",
"220 mx.google.com ESMTP x3si2474860qas.18 - gsmtp\r\n"
"HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Sun, 08 Feb 2015 02:42:29 GMT\r\nExpires: Tue, 10 Mar 2015 02:42:29 GMT\r\nCache-Control: public, max-age=2592000\r\nServer: gws\r\nContent-Length: 219\r\nX-XSS-Protection: 1; mode=block\r\nX-Frame-Options: SAMEORIGIN\r\nAlternate-Protocol: 80:quic,p=0.02\r\n\r\n<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF=\"http://www.google.com/\">here</A>.\r\n</BODY></HTML>\r\n"
]
]
]
Expand All @@ -66,10 +70,53 @@ You can disable TCR hooking TCPSocket ports for a given block via `turned_off`:

```ruby
TCR.turned_off do
tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
data = Net::HTTP.get("google.com", "/")
end
```

### SMTP
You can use TCR to record any TCP interaction. Here we record the start of an SMTP session. **Note that many residential ISPs block port 25 outbound, so this may not work for you.**

```ruby
require 'test/unit'
require 'tcr'

TCR.configure do |c|
c.cassette_library_dir = 'fixtures/tcr_cassettes'
c.hook_tcp_ports = [25]
end

class TCRTest < Test::Unit::TestCase
def test_example_dot_com
TCR.use_cassette('google_smtp') do
tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
io = Net::InternetMessageIO.new(tcp_socket)
assert_match /220 mx.google.com ESMTP/, io.readline
end
end
end
```

TCR will record the tcp interactions to fixtures/tcr_cassettes/google_smtp.json.

```json
[
[
[
"read",
"220 mx.google.com ESMTP x3si2474860qas.18 - gsmtp\r\n"
]
]
]
```

## Configuration
TCR accepts the following configuration parameters:
* **cassette_library_directory**: the directory, relative to your current directory, to save and read recordings from
* **hook_tcp_ports**: the TCP ports that will be intercepted for recording and playback
* **block_for_reads**: when reading data from a cassette, whether TCR should wait for matching "write" data to be written to the socket before allowing a read
* **recording_format**: the format of the cassettes. Can be :json, :yaml, :bson, or :msgpack

## Contributing

1. Fork it
Expand Down
5 changes: 4 additions & 1 deletion lib/tcr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
require "tcr/version"
require "socket"
require "json"
require "yaml"
require "bson"
require "msgpack"


module TCR
Expand Down Expand Up @@ -39,7 +42,7 @@ def save_session

def use_cassette(name, options = {}, &block)
raise ArgumentError, "`TCR.use_cassette` requires a block." unless block
TCR.cassette = Cassette.new(name)
TCR.cassette = Cassette.get_cassette(name, configuration.recording_format)
yield
TCR.cassette.save
TCR.cassette = nil
Expand Down
92 changes: 90 additions & 2 deletions lib/tcr/cassette.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def initialize(name)
if File.exists?(filename)
@recording = false
@contents = File.open(filename) { |f| f.read }
@sessions = JSON.parse(@contents)
@sessions = parse
else
@recording = true
@sessions = []
Expand All @@ -31,14 +31,102 @@ def next_session

def save
if recording?
File.open(filename, "w") { |f| f.write(JSON.pretty_generate(@sessions)) }
File.open(filename, "w") { |f| f.write(dump) }
end
end

def self.get_cassette(name, recording_format)
if recording_format == :json
JSONCassette.new(name)
elsif recording_format == :yaml
YAMLCassette.new(name)
elsif recording_format == :bson
BSONCassette.new(name)
elsif recording_format == :msgpack
MsgpackCassette.new(name)
else
raise TCR::FormatError.new
end
end
end

class JSONCassette < Cassette
def parse
JSON.parse(@contents)
end

def dump
JSON.pretty_generate(@sessions)
end

protected

def filename
"#{TCR.configuration.cassette_library_dir}/#{name}.json"
end
end

class YAMLCassette < Cassette
def parse
YAML.load(@contents)
end

def dump
YAML.dump(@sessions)
end

protected

def filename
"#{TCR.configuration.cassette_library_dir}/#{name}.yaml"
end
end

class BSONCassette < Cassette
def parse
data = Array.from_bson(StringIO.new(@contents))
self.class.debinaryize(data)
end

def dump
self.class.binaryize(@sessions).to_bson
end

def self.binaryize(data)
if Array === data
data.map { |item| binaryize(item) }
elsif String === data
BSON::Binary.new(data)
end
end

def self.debinaryize(data)
if Array === data
data.map { |item| debinaryize(item) }
elsif BSON::Binary === data
data.data
end
end

protected
def filename
"#{TCR.configuration.cassette_library_dir}/#{name}.bson"
end
end

class MsgpackCassette < Cassette
def parse
MessagePack.unpack(@contents)
end

def dump
@sessions.to_msgpack
end

protected

def filename
"#{TCR.configuration.cassette_library_dir}/#{name}.msgpack"
end
end
end
3 changes: 2 additions & 1 deletion lib/tcr/configuration.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module TCR
class Configuration
attr_accessor :cassette_library_dir, :hook_tcp_ports, :block_for_reads
attr_accessor :cassette_library_dir, :hook_tcp_ports, :block_for_reads, :recording_format

def initialize
reset_defaults!
Expand All @@ -10,6 +10,7 @@ def reset_defaults!
@cassette_library_dir = "fixtures/tcr_cassettes"
@hook_tcp_ports = []
@block_for_reads = false
@recording_format = :json
end
end
end
1 change: 1 addition & 0 deletions lib/tcr/errors.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module TCR
class TCRError < StandardError; end
class FormatError < TCRError; end
class NoCassetteError < TCRError; end
class NoMoreSessionsError < TCRError; end
class DirectionMismatchError < TCRError; end
Expand Down
Binary file added spec/fixtures/binary_data.bson
Binary file not shown.
Binary file added spec/fixtures/binary_data.msgpack
Binary file not shown.
Loading