diff --git a/README.md b/README.md
index bba2d39..77a8453 100644
--- a/README.md
+++ b/README.md
@@ -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
\n301 Moved\n301 Moved
\nThe document has moved\nhere.\r\n\r\n"
]
]
]
@@ -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
diff --git a/lib/tcr.rb b/lib/tcr.rb
index a93c6e5..d10272c 100644
--- a/lib/tcr.rb
+++ b/lib/tcr.rb
@@ -5,6 +5,9 @@
require "tcr/version"
require "socket"
require "json"
+require "yaml"
+require "bson"
+require "msgpack"
module TCR
@@ -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
diff --git a/lib/tcr/cassette.rb b/lib/tcr/cassette.rb
index 949bf32..f003e3a 100644
--- a/lib/tcr/cassette.rb
+++ b/lib/tcr/cassette.rb
@@ -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 = []
@@ -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
diff --git a/lib/tcr/configuration.rb b/lib/tcr/configuration.rb
index b60aada..1d642b4 100644
--- a/lib/tcr/configuration.rb
+++ b/lib/tcr/configuration.rb
@@ -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!
@@ -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
diff --git a/lib/tcr/errors.rb b/lib/tcr/errors.rb
index c5bb897..bb1cabd 100644
--- a/lib/tcr/errors.rb
+++ b/lib/tcr/errors.rb
@@ -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
diff --git a/spec/fixtures/binary_data.bson b/spec/fixtures/binary_data.bson
new file mode 100644
index 0000000..c7cc1fc
Binary files /dev/null and b/spec/fixtures/binary_data.bson differ
diff --git a/spec/fixtures/binary_data.msgpack b/spec/fixtures/binary_data.msgpack
new file mode 100644
index 0000000..6a3009e
Binary files /dev/null and b/spec/fixtures/binary_data.msgpack differ
diff --git a/spec/fixtures/binary_data.yaml b/spec/fixtures/binary_data.yaml
new file mode 100644
index 0000000..c274ef5
--- /dev/null
+++ b/spec/fixtures/binary_data.yaml
@@ -0,0 +1,164 @@
+---
+- - - write
+ - "GET /cbzcache/3rdparty/terminal.png 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: c.cyberciti.biz\r\n\r\n"
+ - - read
+ - !binary |-
+ SFRUUC8xLjEgMjAwIE9LDQpDb250ZW50LVR5cGU6IGltYWdlL3BuZw0KQ29u
+ dGVudC1MZW5ndGg6IDY0NDINCkNvbm5lY3Rpb246IGtlZXAtYWxpdmUNClNl
+ cnZlcjogbmdpbngNCkRhdGU6IFR1ZSwgMTggTm92IDIwMTQgMDE6Mjk6NDIg
+ R01UDQpYLVdob206IGwxLW5ldC1pcHY2dA0KRXhwaXJlczogTW9uLCAwNyBO
+ b3YgMjAxNiAwMToyOTo0MiBHTVQNCkNhY2hlLUNvbnRyb2w6IG1heC1hZ2U9
+ NjIyMDgwMDANCkFjY2VwdC1SYW5nZXM6IGJ5dGVzDQpFVGFnOiAiMzEyNzQy
+ Mzg3OCINCkxhc3QtTW9kaWZpZWQ6IFdlZCwgMTQgSmFuIDIwMDkgMTg6NDc6
+ NDAgR01UDQpYLUdhbGF4eTogQW5kcm9tZWRhLTINCkFnZTogNzE1Mzk5NQ0K
+ WC1DYWNoZTogSGl0IGZyb20gY2xvdWRmcm9udA0KVmlhOiAxLjEgOWYyMTk1
+ MmQzM2Q5MTI2Njg5MGYyYWIzNGI4NDlhOWQuY2xvdWRmcm9udC5uZXQgKENs
+ b3VkRnJvbnQpDQpYLUFtei1DZi1JZDogYlZuVDRxMVQ5RmVkOWx0N3ZCUTZs
+ dTVRem5ILWJFeE8wYnpISTBYSnhvZk5DUDdsT2JUNGtnPT0NCg0KiVBORw0K
+ GgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABmJLR0QA/wD/AP+gvaeT
+ AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1wEDCh8dT/Hn+AAAGLdJ
+ REFUeNrtXXtQVOfdfvbs2V12WVhEhMod0Ri0KgpyMVqRYKoNKjZ2HHVs1TaJ
+ U5OxTSpO800SY61TW6cmf3xt0mha05jUiMkksYTPGDUKCOIFowLWiCA3QQ2w
+ y8XdZc9+f7hne/bsue+C2p7fzM5ezmXPe57n/d3e3/seQBVVVFFFFVVUUUUV
+ VVRRRRVVVFFFFVVUUUUVVVRRRZX/WNGI7bB69erpGo1mjUajySMIYqp6yx5M
+ cblcFymKOjk4OPjuwYMHzwEYAuBWTIBVq1bNIElyF0EQ3zMYDEhNTUV4eDji
+ 4+PvSwPdbvd/NcBC7W9ra4PVakVjYyPsdjuGhoYqu7u7Nx86dKgGgBMAJYcA
+ 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
+ +/ZtHDhwQPD6u7q6EBsbi+XLl6OkpMTr
+ - - read
+ - !binary |-
+ jPHF/7T87ne/Q35+PoB7T0l55pln0NXVhffee8977M6dO7F48WKf/0tOTsb6
+ 9etRUFCAwsJC9Pb2+mkAOhyUYgICzQMoNgFsZtLS2dmJjo4OXLt2DcXFxTh4
+ 8CA2bdrkpypLS0uxdOlSTJkyBfPmzUNpaSl0Oh3Wrl3r3WfevHkAgB07dmDO
+ nDnIycnB2rVrcfz4cb9eMnr0aCxbtgxz587Fhx9+CAB48sknRTXAO++8AwB4
+ 9tlncfjwYXz88cfYvn07CgsLvd43U3sxTV1hYSGysrKwb98+APeWdKf3X758
+ ORYvXoyWlhasX78e6enpeOSRR1BYWIiamhqMHz8ev/jFLzg7EF+n4ivECSQT
+ SCiNAqRUBun1ejz22GMoLCz0s3kvvPACGhoa4HK50NXVhW3btnk1AX0T6d44
+ c+ZMrFq1CpmZmaivr8fmzZv9CPD73/8e7e3tcDqd2Lt3r7e3iRHg888/x3PP
+ PYfy8nIMDg4iPj4e8+fPx5YtW/Dee+8hKiqKU1X/9re/RVtbGxwOB3bv3g3g
+ 3mLO9H4/+tGPAAC//OUvcfToUfT19cHlcqGurg7PPfccAGDBggWC9p7P6R6W
+ sYBgmYD4+Hjv40pTUlJQXFyMrVu3wmAwYO/evd7yszVr1mDRokVITk72WVmE
+ vuEajQY7duzA1q1bkZ+f71W3LS0tKC4uRlNTk8//NjU1eY+jH2ppNBoFZyTR
+ N+fChQuora0FQRBISEjA1KlTsWrVKowfPx4bN27Eli1b/CKBxsZGrxdPE5X2
+ deg8AQCUlJTw/n9cXBynE8hnAvgSXgFHAYEUhbAbQH+nKArXr1/HSy+9BAD4
+ 6U9/6t1/8+bNKC4uRlpamt+yMvTzbjUaDU6ePIknn3wSmzdvxgcffICuri4k
+ JCTg17/+tZ8GYEcCSqID4N5U7EOHDuGFF14AAD/HlD2ELKSuxYQ2MVJNAJcT
+ eF/mBgpdLNtW0T0wMjLS+9uSJUsAAJs2bUJlZSUGBgZgNptx8uRJvxDLbrfj
+ xIkTOHnyJN5//3189tlnmDRpkt+NY9YY8OUH5BSBDAwM+PRqtgZgJ4LYv1+7
+ dg2TJ0/GkiVLcPXqVZ80MfMz+/qYHUhqscuI5gG48txcbNVqtUhNTfXa9qtX
+ r3q30Q+pdjqdcDqdiI+Px8svv+zXa19//XVkZ2fDZDIhNDQUc+bM8Q68iPV0
+ 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
+ /w8NO4Xb+BTdJAAAAABJRU5ErkJggg==
diff --git a/spec/fixtures/block_for_reads.bson b/spec/fixtures/block_for_reads.bson
new file mode 100644
index 0000000..558158a
Binary files /dev/null and b/spec/fixtures/block_for_reads.bson differ
diff --git a/spec/fixtures/block_for_reads.msgpack b/spec/fixtures/block_for_reads.msgpack
new file mode 100644
index 0000000..c328465
--- /dev/null
+++ b/spec/fixtures/block_for_reads.msgpack
@@ -0,0 +1,2 @@
+‘’’¥write¦hello
+’¤read§world!
diff --git a/spec/fixtures/block_for_reads.yaml b/spec/fixtures/block_for_reads.yaml
new file mode 100644
index 0000000..f6e7ad1
--- /dev/null
+++ b/spec/fixtures/block_for_reads.yaml
@@ -0,0 +1,7 @@
+---
+- - - write
+ - |
+ hello
+ - - read
+ - |
+ world!
diff --git a/spec/fixtures/google_https.bson b/spec/fixtures/google_https.bson
new file mode 100644
index 0000000..9fae4f5
Binary files /dev/null and b/spec/fixtures/google_https.bson differ
diff --git a/spec/fixtures/google_https.msgpack b/spec/fixtures/google_https.msgpack
new file mode 100644
index 0000000..37270dc
Binary files /dev/null and b/spec/fixtures/google_https.msgpack differ
diff --git a/spec/fixtures/google_https.yaml b/spec/fixtures/google_https.yaml
new file mode 100644
index 0000000..e6131f7
--- /dev/null
+++ b/spec/fixtures/google_https.yaml
@@ -0,0 +1,90 @@
+---
+- - - 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\nConnection: close\r\nHost: www.google.com\r\n\r\n"
+ - - read
+ - "HTTP/1.1 200 OK\r\nDate: Thu, 15 May 2014 15:04:03 GMT\r\nExpires: -1\r\nCache-Control:
+ private, max-age=0\r\nContent-Type: text/html; charset=ISO-8859-1\r\nSet-Cookie:
+ PREF=ID=46c9c47b277ecf04:FF=0:TM=1400166243:LM=1400166243:S=godo17lIPQRSk5e6;
+ expires=Sat, 14-May-2016 15:04:03 GMT; path=/; domain=.google.com\r\nSet-Cookie:
+ NID=67=hkRuWOEJ-mwgTBbt5NpMcYlD7G38o1tTA_mF_l2CvgY9BPVHzf3wom6KaSnvy5Ln6EqfhpD5t0KSia4b-yBj0AUFJbjfZpf_6487tcmyQMw9PtdewdXO1ZtMDD7McNeB;
+ expires=Fri, 14-Nov-2014 15:04:03 GMT; path=/; domain=.google.com; HttpOnly\r\nP3P:
+ CP=\"This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657
+ for more info.\"\r\nServer: gws\r\nX-XSS-Protection: 1; mode=block\r\nX-Frame-Options:
+ SAMEORIGIN\r\nAlternate-Protocol: 443:quic\r\nConnection: close\r\n\r\nGoogle
diff --git a/spec/fixtures/google_imap.bson b/spec/fixtures/google_imap.bson
new file mode 100644
index 0000000..2d7fe3d
Binary files /dev/null and b/spec/fixtures/google_imap.bson differ
diff --git a/spec/fixtures/google_imap.msgpack b/spec/fixtures/google_imap.msgpack
new file mode 100644
index 0000000..e4303fa
Binary files /dev/null and b/spec/fixtures/google_imap.msgpack differ
diff --git a/spec/fixtures/google_imap.yaml b/spec/fixtures/google_imap.yaml
new file mode 100644
index 0000000..21d8e62
--- /dev/null
+++ b/spec/fixtures/google_imap.yaml
@@ -0,0 +1,46 @@
+---
+- - - read
+ - "* OK Gimap ready for requests from 72.16.218.22 n58mb218005052yhi\r\n"
+ - - write
+ - RUBY0001 LOGIN
+ - - write
+ - " "
+ - - write
+ - ben.olive@example.net
+ - - write
+ - " "
+ - - write
+ - password
+ - - write
+ - "\r\n"
+ - - read
+ - "* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1
+ UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH\r\n"
+ - - read
+ - "RUBY0001 OK ben.olive@example.net Ben Olive authenticated (Success)\r\n"
+ - - write
+ - RUBY0002 EXAMINE
+ - - write
+ - " "
+ - - write
+ - INBOX
+ - - write
+ - "\r\n"
+ - - read
+ - "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $Phishing $NotPhishing)\r\n"
+ - - read
+ - "* OK [PERMANENTFLAGS ()] Flags permitted.\r\n"
+ - - read
+ - "* OK [UIDVALIDITY 1] UIDs valid.\r\n"
+ - - read
+ - "* 206 EXISTS\r\n"
+ - - read
+ - "* 0 RECENT\r\n"
+ - - read
+ - "* OK [UIDNEXT 5504] Predicted next UID.\r\n"
+ - - read
+ - "* OK [HIGHESTMODSEQ 609503]\r\n"
+ - - read
+ - "RUBY0002 OK [READ-ONLY] INBOX selected. (Success)\r\n"
+ - - read
+ -
diff --git a/spec/fixtures/google_smtp.bson b/spec/fixtures/google_smtp.bson
new file mode 100644
index 0000000..e26075a
Binary files /dev/null and b/spec/fixtures/google_smtp.bson differ
diff --git a/spec/fixtures/google_smtp.msgpack b/spec/fixtures/google_smtp.msgpack
new file mode 100644
index 0000000..947a2d1
Binary files /dev/null and b/spec/fixtures/google_smtp.msgpack differ
diff --git a/spec/fixtures/google_smtp.yaml b/spec/fixtures/google_smtp.yaml
new file mode 100644
index 0000000..a24dd54
--- /dev/null
+++ b/spec/fixtures/google_smtp.yaml
@@ -0,0 +1,3 @@
+---
+- - - read
+ - "220 mx.google.com ESMTP x3si2474860qas.18 - gsmtp\r\n"
diff --git a/spec/fixtures/multitest-smtp.bson b/spec/fixtures/multitest-smtp.bson
new file mode 100644
index 0000000..786a0a8
Binary files /dev/null and b/spec/fixtures/multitest-smtp.bson differ
diff --git a/spec/fixtures/multitest-smtp.msgpack b/spec/fixtures/multitest-smtp.msgpack
new file mode 100644
index 0000000..097ace6
Binary files /dev/null and b/spec/fixtures/multitest-smtp.msgpack differ
diff --git a/spec/fixtures/multitest-smtp.yaml b/spec/fixtures/multitest-smtp.yaml
new file mode 100644
index 0000000..12322ba
--- /dev/null
+++ b/spec/fixtures/multitest-smtp.yaml
@@ -0,0 +1,23 @@
+---
+- - - read
+ - "220 mx.google.com ESMTP d8si2472149qai.124 - gsmtp\r\n"
+ - - write
+ - "EHLO localhost\r\n"
+ - - read
+ - "250-mx.google.com at your service, [54.227.243.167]\r\n250-SIZE 35882577\r\n250-8BITMIME\r\n250-STARTTLS\r\n250
+ ENHANCEDSTATUSCODES\r\n"
+ - - write
+ - "QUIT\r\n"
+ - - read
+ - "221 2.0.0 closing connection d8si2472149qai.124 - gsmtp\r\n"
+- - - read
+ - "220 mta1579.mail.gq1.yahoo.com ESMTP YSmtpProxy service ready\r\n"
+ - - write
+ - "EHLO localhost\r\n"
+ - - read
+ - "250-mta1579.mail.gq1.yahoo.com\r\n250-8BITMIME\r\n250-SIZE 41943040\r\n250
+ PIPELINING\r\n"
+ - - write
+ - "QUIT\r\n"
+ - - read
+ - "221 mta1579.mail.gq1.yahoo.com\r\n"
diff --git a/spec/fixtures/multitest.bson b/spec/fixtures/multitest.bson
new file mode 100644
index 0000000..7400ecc
Binary files /dev/null and b/spec/fixtures/multitest.bson differ
diff --git a/spec/fixtures/multitest.msgpack b/spec/fixtures/multitest.msgpack
new file mode 100644
index 0000000..66f8cb8
Binary files /dev/null and b/spec/fixtures/multitest.msgpack differ
diff --git a/spec/fixtures/multitest.yaml b/spec/fixtures/multitest.yaml
new file mode 100644
index 0000000..c28604a
--- /dev/null
+++ b/spec/fixtures/multitest.yaml
@@ -0,0 +1,5 @@
+---
+- - - read
+ - "220 mx.google.com ESMTP h5si2286277qec.54 - gsmtp\r\n"
+- - - read
+ - "220 mta1009.mail.gq1.yahoo.com ESMTP YSmtpProxy service ready\r\n"
diff --git a/spec/fixtures/starwars_telnet.bson b/spec/fixtures/starwars_telnet.bson
new file mode 100644
index 0000000..b4b63c9
Binary files /dev/null and b/spec/fixtures/starwars_telnet.bson differ
diff --git a/spec/fixtures/starwars_telnet.msgpack b/spec/fixtures/starwars_telnet.msgpack
new file mode 100644
index 0000000..c664fbb
Binary files /dev/null and b/spec/fixtures/starwars_telnet.msgpack differ
diff --git a/spec/fixtures/starwars_telnet.yaml b/spec/fixtures/starwars_telnet.yaml
new file mode 100644
index 0000000..73f6a1e
--- /dev/null
+++ b/spec/fixtures/starwars_telnet.yaml
@@ -0,0 +1,3 @@
+---
+- - - read
+ - "\e[H\e[J\e[H\r\n\r\n\r\n\r\n\r\n\r\n "
diff --git a/spec/tcr_spec.rb b/spec/tcr_spec.rb
index a45940d..ae35a11 100644
--- a/spec/tcr_spec.rb
+++ b/spec/tcr_spec.rb
@@ -12,12 +12,6 @@
TCR.configuration.reset_defaults!
end
- around(:each) do |example|
- File.unlink("test.json") if File.exists?("test.json")
- example.run
- File.unlink("test.json") if File.exists?("test.json")
- end
-
describe ".configuration" do
it "has a default cassette location configured" do
TCR.configuration.cassette_library_dir.should == "fixtures/tcr_cassettes"
@@ -28,7 +22,11 @@
end
it "defaults to erroring on read/write mismatch access" do
- TCR.configuration.block_for_reads.should be_false
+ TCR.configuration.block_for_reads.should be_falsey
+ end
+
+ it "defaults to JSON" do
+ TCR.configuration.recording_format.should == :json
end
end
@@ -39,6 +37,12 @@
}.to change{ TCR.configuration.cassette_library_dir }.from("fixtures/tcr_cassettes").to("some/dir")
end
+ it "configures cassette format" do
+ expect {
+ TCR.configure { |c| c.recording_format = :yaml }
+ }.to change{ TCR.configuration.recording_format }.from(:json).to(:yaml)
+ end
+
it "configures tcp ports to hook" do
expect {
TCR.configure { |c| c.hook_tcp_ports = [25] }
@@ -120,7 +124,7 @@
end
end
- describe ".use_cassette" do
+ shared_examples "a cassette" do
before(:each) {
TCR.configure { |c|
c.hook_tcp_ports = [25]
@@ -128,6 +132,12 @@
}
}
+ around(:each) do |example|
+ File.unlink(test_file_name) if File.exists?(test_file_name)
+ example.run
+ File.unlink(test_file_name) if File.exists?(test_file_name)
+ end
+
it "requires a block to call" do
expect {
TCR.use_cassette("test")
@@ -141,22 +151,25 @@
end
it "creates a cassette file on use" do
+ TCR.configure { |c| c.hook_tcp_ports = [25] }
expect {
TCR.use_cassette("test") do
- tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
+ tcp_socket = TCPSocket.open("google.com", 80)
end
- }.to change{ File.exists?("./test.json") }.from(false).to(true)
+ }.to change{ File.exists?(test_file_name) }.from(false).to(true)
end
it "records the tcp session data into the file" do
+ TCR.configure { |c| c.hook_tcp_ports = [80] }
TCR.use_cassette("test") do
- tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
+ tcp_socket = TCPSocket.open("google.com", 80)
io = Net::InternetMessageIO.new(tcp_socket)
+ io.write("GET /\n")
line = io.readline
tcp_socket.close
end
- cassette_contents = File.open("test.json") { |f| f.read }
- cassette_contents.include?("220 mx.google.com ESMTP").should == true
+ cassette_contents = File.open(test_file_name) { |f| f.read }
+ cassette_contents.include?("HTTP/1.0 200 OK").should == true
end
it "plays back tcp sessions without opening a real connection" do
@@ -218,15 +231,14 @@
context "multiple connections" do
it "records multiple sessions per cassette" do
+ TCR.configure { |c| c.hook_tcp_ports = [80] }
TCR.use_cassette("test") do
- smtp = Net::SMTP.start("aspmx.l.google.com", 25)
- smtp.finish
- smtp = Net::SMTP.start("mta6.am0.yahoodns.net", 25)
- smtp.finish
+ Net::HTTP.get("google.com", "/")
+ Net::HTTP.get("yahoo.com", "/")
end
- cassette_contents = File.open("test.json") { |f| f.read }
- cassette_contents.include?("google.com ESMTP").should == true
- cassette_contents.include?("yahoo.com ESMTP").should == true
+ cassette_contents = File.open(test_file_name) { |f| f.read }
+ cassette_contents.include?("Host: google.com").should == true
+ cassette_contents.include?("Host: yahoo.com").should == true
end
it "plays back multiple sessions per cassette in order" do
@@ -281,4 +293,98 @@
end
end
end
+
+ shared_examples "a binary compatible cassette" do
+ before(:each) {
+ TCR.configure { |c|
+ c.hook_tcp_ports = [25]
+ c.cassette_library_dir = "."
+ }
+ }
+
+ around(:each) do |example|
+ File.unlink(test_file_name) if File.exists?(test_file_name)
+ example.run
+ File.unlink(test_file_name) if File.exists?(test_file_name)
+ end
+
+ it "handles recording binary data properly" do
+ TCR.configure { |c| c.hook_tcp_ports = [80] }
+ TCR.use_cassette("test") do
+ uri = URI("http://c.cyberciti.biz/cbzcache/3rdparty/terminal.png")
+ data = Net::HTTP.get(uri)
+ end
+ cassette_contents = File.open(test_file_name) { |f| f.read }
+ cassette_contents.include?("User-Agent: Ruby").should == true
+ end
+
+ it "handles reading binary data properly" do
+ TCR.configure { |c| c.hook_tcp_ports = [80] }
+ TCR.use_cassette("spec/fixtures/binary_data") do
+ uri = URI("http://c.cyberciti.biz/cbzcache/3rdparty/terminal.png")
+ data = Net::HTTP.get(uri)
+ data.include?("PNG").should == true
+ end
+ end
+ end
+
+ describe "a JSON cassette" do
+ let(:test_file_name) { "test.json" }
+ # we default to JSON so no need to specify recording_format
+
+ it_behaves_like "a cassette"
+ end
+
+ describe "a YAML cassette" do
+ let(:test_file_name) { "test.yaml" }
+
+ before(:each) do
+ TCR.configure do |c|
+ c.recording_format = :yaml
+ end
+ end
+
+ it_behaves_like "a cassette"
+ it_behaves_like "a binary compatible cassette"
+ end
+
+ describe "a BSON cassette" do
+ let(:test_file_name) { "test.bson" }
+
+ before(:each) do
+ TCR.configure do |c|
+ c.recording_format = :bson
+ end
+ end
+
+ it_behaves_like "a cassette"
+ it_behaves_like "a binary compatible cassette"
+ end
+
+ describe "a msgpack cassette" do
+ let(:test_file_name) { "test.msgpack" }
+
+ before(:each) do
+ TCR.configure do |c|
+ c.recording_format = :msgpack
+ end
+ end
+
+ it_behaves_like "a cassette"
+ it_behaves_like "a binary compatible cassette"
+ end
+
+ describe "an invalid format cassette" do
+ before(:each) do
+ TCR.configure do |c|
+ c.recording_format = :foobar
+ end
+ end
+
+ it "raises an error" do
+ expect {
+ TCR.use_cassette("test") {}
+ }.to raise_error(TCR::FormatError)
+ end
+ end
end
diff --git a/tcr.gemspec b/tcr.gemspec
index abb60c2..8021376 100644
--- a/tcr.gemspec
+++ b/tcr.gemspec
@@ -17,6 +17,8 @@ Gem::Specification.new do |gem|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]
+ gem.add_dependency "bson"
+ gem.add_dependency "msgpack"
gem.add_development_dependency "rspec"
gem.add_development_dependency "geminabox"
end