tcr 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +6 -0
- data/README.md +15 -0
- data/lib/tcr.rb +14 -1
- data/lib/tcr/cassette.rb +12 -8
- data/lib/tcr/configuration.rb +3 -2
- data/lib/tcr/recordable_tcp_socket.rb +110 -23
- data/lib/tcr/version.rb +1 -1
- data/spec/fixtures/block_for_reads.json +12 -0
- data/spec/fixtures/google_https.json +48 -0
- data/spec/fixtures/google_imap.json +92 -0
- data/spec/fixtures/starwars_telnet.json +8 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/tcr_spec.rb +124 -6
- metadata +22 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: afe506ab1f3f57f65d4b9131d0b90a19f3392e00
|
4
|
+
data.tar.gz: a1b9bab2f8f7692c7be4a641c2d10c06743fccd8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e6107c9636806831a366f43607651f6613e557026fb9a520cc630d20f327abfc935ded60fa0d410492b018b123a2d83e48843b1d004b6ab82e962b9f9f84725c
|
7
|
+
data.tar.gz: ccdc0808bed751308838b9f992ad00fecaab8917bef9cc3bc29e2eabeed9b49b759d7fa33f4c0b9bd4be31b5c299f3c304110b883825c184b4ec138e6785c4ec
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# TCR (TCP + VCR)
|
2
2
|
|
3
|
+
[![Build Status](https://travis-ci.org/robforman/tcr.png?branch=master)](https://travis-ci.org/robforman/tcr)
|
4
|
+
|
5
|
+
[ ![Codeship Status for SalesLoft/melody](https://www.codeship.io/projects/9fcbda40-6859-0132-9920-3ad5c353d440/status?branch=master)](https://www.codeship.io/projects/53337)
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
|
3
10
|
TCR is a *very* lightweight version of [VCR](https://github.com/vcr/vcr) for TCP sockets.
|
4
11
|
|
5
12
|
Currently used for recording 'net/smtp' interactions so only a few of the TCPSocket methods are recorded out.
|
@@ -55,6 +62,14 @@ Run this test once, and TCR will record the tcp interactions to fixtures/tcr_cas
|
|
55
62
|
|
56
63
|
Run it again, and TCR will replay the interactions from json when the tcp request is made. This test is now fast (no real TCP requests are made anymore), deterministic and accurate.
|
57
64
|
|
65
|
+
You can disable TCR hooking TCPSocket ports for a given block via `turned_off`:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
TCR.turned_off do
|
69
|
+
tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
58
73
|
## Contributing
|
59
74
|
|
60
75
|
1. Fork it
|
data/lib/tcr.rb
CHANGED
@@ -41,6 +41,7 @@ module TCR
|
|
41
41
|
raise ArgumentError, "`TCR.use_cassette` requires a block." unless block
|
42
42
|
TCR.cassette = Cassette.new(name)
|
43
43
|
yield
|
44
|
+
TCR.cassette.save
|
44
45
|
TCR.cassette = nil
|
45
46
|
end
|
46
47
|
|
@@ -59,7 +60,7 @@ class TCPSocket
|
|
59
60
|
class << self
|
60
61
|
alias_method :real_open, :open
|
61
62
|
|
62
|
-
def open(address, port)
|
63
|
+
def open(address, port, *args)
|
63
64
|
if TCR.configuration.hook_tcp_ports.include?(port)
|
64
65
|
TCR::RecordableTCPSocket.new(address, port, TCR.cassette)
|
65
66
|
else
|
@@ -68,3 +69,15 @@ class TCPSocket
|
|
68
69
|
end
|
69
70
|
end
|
70
71
|
end
|
72
|
+
|
73
|
+
class OpenSSL::SSL::SSLSocket
|
74
|
+
class << self
|
75
|
+
def new(io, *args)
|
76
|
+
if TCR::RecordableTCPSocket === io
|
77
|
+
TCR::RecordableSSLSocket.new(io)
|
78
|
+
else
|
79
|
+
super
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/tcr/cassette.rb
CHANGED
@@ -20,15 +20,19 @@ module TCR
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def next_session
|
23
|
-
|
24
|
-
|
25
|
-
|
23
|
+
if recording?
|
24
|
+
@sessions << []
|
25
|
+
@sessions.last
|
26
|
+
else
|
27
|
+
raise NoMoreSessionsError if @sessions.empty?
|
28
|
+
@sessions.shift
|
29
|
+
end
|
26
30
|
end
|
27
31
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
+
def save
|
33
|
+
if recording?
|
34
|
+
File.open(filename, "w") { |f| f.write(JSON.pretty_generate(@sessions)) }
|
35
|
+
end
|
32
36
|
end
|
33
37
|
|
34
38
|
protected
|
@@ -37,4 +41,4 @@ module TCR
|
|
37
41
|
"#{TCR.configuration.cassette_library_dir}/#{name}.json"
|
38
42
|
end
|
39
43
|
end
|
40
|
-
end
|
44
|
+
end
|
data/lib/tcr/configuration.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module TCR
|
2
2
|
class Configuration
|
3
|
-
attr_accessor :cassette_library_dir, :hook_tcp_ports
|
3
|
+
attr_accessor :cassette_library_dir, :hook_tcp_ports, :block_for_reads
|
4
4
|
|
5
5
|
def initialize
|
6
6
|
reset_defaults!
|
@@ -9,6 +9,7 @@ module TCR
|
|
9
9
|
def reset_defaults!
|
10
10
|
@cassette_library_dir = "fixtures/tcr_cassettes"
|
11
11
|
@hook_tcp_ports = []
|
12
|
+
@block_for_reads = false
|
12
13
|
end
|
13
14
|
end
|
14
|
-
end
|
15
|
+
end
|
@@ -1,45 +1,46 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
require 'openssl'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
|
1
6
|
module TCR
|
2
7
|
class RecordableTCPSocket
|
3
|
-
attr_reader :live, :
|
8
|
+
attr_reader :live, :socket
|
4
9
|
attr_accessor :recording
|
5
10
|
|
6
11
|
def initialize(address, port, cassette)
|
7
12
|
raise TCR::NoCassetteError.new unless TCR.cassette
|
8
13
|
|
14
|
+
@read_lock = Queue.new
|
15
|
+
|
9
16
|
if cassette.recording?
|
10
17
|
@live = true
|
11
18
|
@socket = TCPSocket.real_open(address, port)
|
12
|
-
@recording = []
|
13
19
|
else
|
14
20
|
@live = false
|
15
|
-
@recording = cassette.next_session
|
16
21
|
end
|
17
|
-
@
|
22
|
+
@recording = cassette.next_session
|
23
|
+
end
|
24
|
+
|
25
|
+
def read(bytes)
|
26
|
+
_read(:read, bytes)
|
27
|
+
end
|
28
|
+
|
29
|
+
def gets(*args)
|
30
|
+
_read(:gets, *args)
|
18
31
|
end
|
19
32
|
|
20
33
|
def read_nonblock(bytes)
|
21
|
-
|
22
|
-
|
23
|
-
recording << ["read", data]
|
24
|
-
else
|
25
|
-
direction, data = recording.shift
|
26
|
-
raise TCR::DirectionMismatchError.new("Expected to 'read' but next in recording was 'write'") unless direction == "read"
|
27
|
-
end
|
34
|
+
_read(:read_nonblock, bytes, blocking: false)
|
35
|
+
end
|
28
36
|
|
29
|
-
|
37
|
+
def print(str)
|
38
|
+
_write(:print, str)
|
30
39
|
end
|
31
40
|
|
32
41
|
def write(str)
|
33
|
-
|
34
|
-
|
35
|
-
recording << ["write", str]
|
36
|
-
else
|
37
|
-
direction, data = recording.shift
|
38
|
-
raise TCR::DirectionMismatchError.new("Expected to 'write' but next in recording was 'read'") unless direction == "write"
|
39
|
-
len = data.length
|
40
|
-
end
|
41
|
-
|
42
|
-
len
|
42
|
+
_write(:write, str)
|
43
|
+
str.length
|
43
44
|
end
|
44
45
|
|
45
46
|
def to_io
|
@@ -59,8 +60,94 @@ module TCR
|
|
59
60
|
def close
|
60
61
|
if live
|
61
62
|
@socket.close
|
62
|
-
cassette.append(recording)
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def _intercept_socket
|
69
|
+
if @socket
|
70
|
+
@socket = yield @socket
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def _block_for_read_data
|
75
|
+
while recording.first && recording.first.first != "read"
|
76
|
+
@read_lock.pop
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def _check_for_blocked_reads
|
81
|
+
@read_lock << 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def _write(method, data)
|
85
|
+
if live
|
86
|
+
@socket.__send__(method, data)
|
87
|
+
recording << ["write", data]
|
88
|
+
else
|
89
|
+
direction, data = recording.shift
|
90
|
+
_ensure_direction("write", direction)
|
91
|
+
_check_for_blocked_reads
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def _read(method, *args, blocking: true)
|
96
|
+
if live
|
97
|
+
data = @socket.__send__(method, *args)
|
98
|
+
recording << ["read", data]
|
99
|
+
else
|
100
|
+
_block_for_read_data if blocking && TCR.configuration.block_for_reads
|
101
|
+
raise EOFError if recording.empty?
|
102
|
+
direction, data = recording.shift
|
103
|
+
_ensure_direction("read", direction)
|
104
|
+
end
|
105
|
+
data
|
106
|
+
end
|
107
|
+
|
108
|
+
def _ensure_direction(desired, actual)
|
109
|
+
raise TCR::DirectionMismatchError.new("Expected to '#{desired}' but next in recording was '#{actual}'") unless desired == actual
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class RecordableSSLSocket < SimpleDelegator
|
114
|
+
def initialize(tcr_socket)
|
115
|
+
super(tcr_socket)
|
116
|
+
tcr_socket.send(:_intercept_socket) do |sock|
|
117
|
+
socket = OpenSSL::SSL::SSLSocket.new(sock, OpenSSL::SSL::SSLContext.new)
|
118
|
+
socket.sync_close = true
|
119
|
+
socket.connect
|
120
|
+
socket
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def sync_close=(arg)
|
125
|
+
true
|
126
|
+
end
|
127
|
+
|
128
|
+
def session
|
129
|
+
self
|
130
|
+
end
|
131
|
+
|
132
|
+
def session=(args)
|
133
|
+
end
|
134
|
+
|
135
|
+
def io
|
136
|
+
self
|
137
|
+
end
|
138
|
+
|
139
|
+
def shutdown
|
140
|
+
if live
|
141
|
+
socket.io.shutdown
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def connect
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
def post_connection_check(*args)
|
150
|
+
true
|
151
|
+
end
|
65
152
|
end
|
66
153
|
end
|
data/lib/tcr/version.rb
CHANGED
@@ -0,0 +1,48 @@
|
|
1
|
+
[
|
2
|
+
[
|
3
|
+
[
|
4
|
+
"write",
|
5
|
+
"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"
|
6
|
+
],
|
7
|
+
[
|
8
|
+
"read",
|
9
|
+
"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\n<!doctype html><html itemscope=\"\" itemtype=\"http://schema.org/WebPage\" lang=\"en\"><head><meta content=\"Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for.\" name=\"description\"><meta content=\"noodp\" name=\"robots\"><meta content=\"/images/google_favicon_128.png\" itemprop=\"image\"><title>Google</title><script>(function(){\nwindow.google={kEI:\"Y9d0U_rDLo-jqAaU0oDoDA\",getEI:function(a){for(var b;a&&(!a.getAttribute||!(b=a.getAttribute(\"eid\")));)a=a.pare"
|
10
|
+
],
|
11
|
+
[
|
12
|
+
"read",
|
13
|
+
"ntNode;return b||google.kEI},https:function(){return\"https:\"==window.location.protocol},kEXPI:\"17259,4000116,4007661,4007830,4008142,4009033,4009641,4010806,4010858,4010899,4011228,4011258,4011679,4011960,4012373,4012504,4013414,4013591,4013723,4013758,4013787,4013823,4013941,4013967,4013979,4014016,4014093,4014237,4014431,4014515,4014636,4014671,4014789,4014810,4014813,4014991,4015155,4015234,4015260,4015299,4015301,4015519,4015550,4015587,4015638,4015639,4015772,4016013,4016127,4016309,4016363,4016367,4016373,4016446,4016487,4016703,4016730,4016800,4016824,4016855,4016933,4016976,4017261,4017278,4017280,4017285,4017596,4017612,4017639,4017666,4017681,4017694,4017710,4017712,4017742,4017788,4017801,4017818,4017856,4017862,4017913,4017922,4017960,4017962,4018019,8300012,8300015,8500223,8500240,8500252,8500264,8500272,8500306,8500314,8500332,8500365,8500378,8500389,8500394,10200002,10200012,10200014,10200029,10200038,10200040,10200048,10200053,10200066,10200084,10200120,10200134,10200136,10200155,10200157,10200159,10200164,10200176,10200184,10200211,10200221,10200226,10200242,10200246,10200250,10200254,10200261,10200263,10200271,10200293,10200295,10200299\",kCSI:{e:\"17259,4000116,4007661,4007830,4008142,4009033,4009641,4010806,4010858,4010899,4011228,4011258,4011679,4011960,4012373,4012504,4013414,4013591,4013723,401375"
|
14
|
+
],
|
15
|
+
[
|
16
|
+
"read",
|
17
|
+
"8,4013787,4013823,4013941,4013967,4013979,4014016,4014093,4014237,4014431,4014515,4014636,4014671,4014789,4014810,4014813,4014991,4015155,4015234,4015260,4015299,4015301,4015519,4015550,4015587,4015638,4015639,4015772,4016013,4016127,4016309,4016363,4016367,4016373,4016446,4016487,4016703,4016730,4016800,4016824,4016855,4016933,4016976,4017261,4017278,4017280,4017285,4017596,4017612,4017639,4017666,4017681,4017694,4017710,4017712,4017742,4017788,4017801,4017818,4017856,4017862,4017913,4017922,4017960,4017962,4018019,8300012,8300015,8500223,8500240,8500252,8500264,8500272,8500306,8500314,8500332,8500365,8500378,8500389,8500394,10200002,10200012,10200014,10200029,10200038,10200040,10200048,10200053,10200066,10200084,10200120,10200134,10200136,10200155,10200157,10200159,10200164,10200176,10200184,10200211,10200221,10200226,10200242,10200246,10200250,10200254,10200261,10200263,10200271,10200293,10200295,10200299\",ei:\"Y9d0U_rDLo-jqAaU0oDoDA\"},authuser:0,ml:function(){},kHL:\"en\",time:function(){return(new Date).getTime()},log:function(a,b,c,h,k){var d=\nnew Image,f=google.lc,e=google.li,g=\"\";d.onerror=d.onload=d.onabort=function(){delete f[e]};f[e]=d;c||-1!=b.search(\"&ei=\")||(g=\"&ei=\"+google.getEI(h));c=c||\"/\"+(k||\"gen_204\")+\"?atyp=i&ct=\"+a+\"&cad=\"+b+g+\"&zx=\"+google.time();a=/^http:/i;a.test(c)&&google.https()?(google.ml(Err"
|
18
|
+
],
|
19
|
+
[
|
20
|
+
"read",
|
21
|
+
"or(\"GLMM\"),!1,{src:c}),delete f[e]):(d.src=c,google.li=e+1)},lc:[],li:0,y:{},x:function(a,b){google.y[a.id]=[a,b];return!1},load:function(a,b,c){google.x({id:a+l++},function(){google.load(a,b,c)})}};var l=0;})();\n(function(){google.sn=\"webhp\";google.timers={};google.startTick=function(a,b){var f=google.time();google.timers[a]={t:{start:f},bfr:!!b};};google.tick=function(a,b,f){google.timers[a]||google.startTick(a);google.timers[a].t[b]=f||google.time()};google.startTick(\"load\",!0);\ntry{}catch(d){}})();\nvar _gjwl=location;function _gjuc(){var a=_gjwl.href.indexOf(\"#\");if(0<=a&&(a=_gjwl.href.substring(a),0<a.indexOf(\"&q=\")||0<=a.indexOf(\"#q=\"))&&(a=a.substring(1),-1==a.indexOf(\"#\"))){for(var d=0;d<a.length;){var b=d;\"&\"==a.charAt(b)&&++b;var c=a.indexOf(\"&\",b);-1==c&&(c=a.length);b=a.substring(b,c);if(0==b.indexOf(\"fp=\"))a=a.substring(0,d)+a.substring(c,a.length),c=d;else if(\"cad=h\"==b)return 0;d=c}_gjwl.href=\"/search?\"+a+\"&cad=h\";return 1}return 0}\nfunction _gjh(){!_gjuc()&&window.google&&google.x&&google.x({id:\"GJH\"},function(){google.nav&&google.nav.gjh&&google.nav.gjh()})};\nwindow._gjh&&_gjh();</script><style>#gbar,#guser{font-size:13px;padding-top:1px !important;}#gbar{height:22px}#guser{padding-bottom:7px !important;text-align:right}.gbh,.gbd{border-top:1px solid #c9d7f1;font-size:1px}.gbh{height:0;position:absolu"
|
22
|
+
],
|
23
|
+
[
|
24
|
+
"read",
|
25
|
+
"te;top:24px;width:100%}@media all{.gb1{height:22px;margin-right:.5em;vertical-align:top}#gbar{float:left}}a.gb1,a.gb4{text-decoration:underline !important}a.gb1,a.gb4{color:#00c !important}.gbi .gb4{color:#dd8e27 !important}.gbf .gb4{color:#900 !important}</style><style>body,td,a,p,.h{font-family:arial,sans-serif}body{margin:0;overflow-y:scroll}#gog{padding:3px 8px 0}td{line-height:.8em}.gac_m td{line-height:17px}form{margin-bottom:20px}.h{color:#36c}.q{color:#00c}.ts td{padding:0}.ts{border-collapse:collapse}em{font-weight:bold;font-style:normal}.lst{height:25px;width:496px}.gsfi,.lst{font:18px arial,sans-serif}.gsfs{font:17px arial,sans-serif}.ds{display:inline-box;display:inline-block;margin:3px 0 4px;margin-left:4px}input{font-family:inherit}a.gb1,a.gb2,a.gb3,a.gb4{color:#11c !important}body{background:#fff;color:black}a{color:#11c;text-decoration:none}a:hover,a:active{text-decoration:underline}.fl a{color:#36c}a:visited{color:#551a8b}a.gb1,a.gb4{text-decoration:underline}a.gb3:hover{text-decoration:none}#ghead a.gb2:hover{color:#fff !important}.sblc{padding-top:5px}.sblc a{display:block;margin:2px 0;margin-left:13px;font-size:11px}.lsbb{background:#eee;border:solid 1px;border-color:#ccc #999 #999 #ccc;height:30px}.lsbb{display:block}.ftl,#fll a{display:inline-block;margin:0 12px}.lsb{background:url(/images/srpr/n"
|
26
|
+
],
|
27
|
+
[
|
28
|
+
"read",
|
29
|
+
"av_logo80.png) 0 -258px repeat-x;border:none;color:#000;cursor:pointer;height:30px;margin:0;outline:0;font:15px arial,sans-serif;vertical-align:top}.lsb:active{background:#ccc}.lst:focus{outline:none}#addlang a{padding:0 3px}</style><script></script></head><body bgcolor=\"#fff\"><script>(function(){var src='/images/nav_logo176.png';var iesg=false;document.body.onload = function(){window.n && window.n();if (document.images){new Image().src=src;}\nif (!iesg){document.f&&document.f.q.focus();document.gbqf&&document.gbqf.q.focus();}\n}\n})();</script><textarea id=\"csi\" style=\"display:none\"></textarea><div id=\"mngb\"> <div id=gbar><nobr><b class=gb1>Search</b> <a class=gb1 href=\"https://www.google.com/imghp?hl=en&tab=wi\">Images</a> <a class=gb1 href=\"https://maps.google.com/maps?hl=en&tab=wl\">Maps</a> <a class=gb1 href=\"https://play.google.com/?hl=en&tab=w8\">Play</a> <a class=gb1 href=\"https://www.youtube.com/?tab=w1\">YouTube</a> <a class=gb1 href=\"https://news.google.com/nwshp?hl=en&tab=wn\">News</a> <a class=gb1 href=\"https://mail.google.com/mail/?tab=wm\">Gmail</a> <a class=gb1 href=\"https://drive.google.com/?tab=wo\">Drive</a> <a class=gb1 style=\"text-decoration:none\" href=\"http://www.google.com/intl/en/options/\"><u>More</u> »</a></nobr></div><div id=guser width=100%><nobr><span id=gbn class=gbi></span><span id=gbf class="
|
30
|
+
],
|
31
|
+
[
|
32
|
+
"read",
|
33
|
+
"gbf></span><span id=gbe></span><a href=\"http://www.google.com/history/optout?hl=en\" class=gb4>Web History</a> | <a href=\"/preferences?hl=en\" class=gb4>Settings</a> | <a target=_top id=gb_70 href=\"https://accounts.google.com/ServiceLogin?hl=en&continue=https://www.google.com/\" class=gb4>Sign in</a></nobr></div><div class=gbh style=left:0></div><div class=gbh style=right:0></div> </div><center><br clear=\"all\" id=\"lgpd\"><div id=\"lga\"><img alt=\"Google\" height=\"95\" src=\"/images/srpr/logo9w.png\" style=\"padding:28px 0 14px\" width=\"269\" id=\"hplogo\" onload=\"window.lol&&lol()\"><br><br></div><form action=\"/search\" name=\"f\"><table cellpadding=\"0\" cellspacing=\"0\"><tr valign=\"top\"><td width=\"25%\"> </td><td align=\"center\" nowrap=\"\"><input name=\"ie\" value=\"ISO-8859-1\" type=\"hidden\"><input value=\"en\" name=\"hl\" type=\"hidden\"><input name=\"source\" type=\"hidden\" value=\"hp\"><div class=\"ds\" style=\"height:32px;margin:4px 0\"><input style=\"color:#000;margin:0;padding:5px 8px 0 6px;vertical-align:top\" autocomplete=\"off\" class=\"lst\" value=\"\" title=\"Google Search\" maxlength=\"2048\" name=\"q\" size=\"57\"></div><br style=\"line-height:0\"><span class=\"ds\"><span class=\"lsbb\"><input class=\"lsb\" value=\"Google Search\" name=\"btnG\" type=\"submit\"></span></span><span class=\"ds\"><span class=\"lsbb\"><input class=\"lsb\" value=\"I'm Feeling Lucky\" name=\"btnI\" on"
|
34
|
+
],
|
35
|
+
[
|
36
|
+
"read",
|
37
|
+
"click=\"if(this.form.q.value)this.checked=1; else top.location='/doodles/'\" type=\"submit\"></span></span></td><td class=\"fl sblc\" align=\"left\" nowrap=\"\" width=\"25%\"><a href=\"/advanced_search?hl=en&authuser=0\">Advanced search</a><a href=\"/language_tools?hl=en&authuser=0\">Language tools</a></td></tr></table><input id=\"gbv\" name=\"gbv\" type=\"hidden\" value=\"1\"></form><div id=\"gac_scont\"></div><div style=\"font-size:83%;min-height:3.5em\"><br></div><span id=\"footer\"><div style=\"font-size:10pt\"><div style=\"margin:19px auto;text-align:center\" id=\"fll\"><a href=\"/intl/en/ads/\">Advertising Programs</a><a href=\"/services/\">Business Solutions</a><a href=\"https://plus.google.com/116899029375914044550\" rel=\"publisher\">+Google</a><a href=\"/intl/en/about.html\">About Google</a></div></div><p style=\"color:#767676;font-size:8pt\">© 2013 - <a href=\"/intl/en/policies/\">Privacy & Terms</a></p></span></center><div id=xjsd></div><div id=xjsi data-jiis=\"bp\"><script>if(google.y)google.y.first=[];(function(){function b(a){window.setTimeout(function(){var c=document.createElement(\"script\");c.src=a;document.getElementById(\"xjsd\").appendChild(c)},0)}google.dljp=function(a){google.xjsu=a;b(a)};google.dlj=b;})();\nif(!google.xjs){window._=window._||{};window._._DumpException=function(e){throw e};if(google.timers&&google.timers.load.t){goo"
|
38
|
+
],
|
39
|
+
[
|
40
|
+
"read",
|
41
|
+
"gle.timers.load.t.xjsls=new Date().getTime();}google.dljp('/xjs/_/js/k\\x3dxjs.hp.en_US.Gh3uPFri1JA.O/m\\x3dsb_he,pcc/rt\\x3dj/d\\x3d1/sv\\x3d1/rs\\x3dAItRSTNl_2-4gdX7sEzEk9xHPbjumUrcCA');google.xjs=1;}google.pmc={\"sb_he\":{\"agen\":true,\"cgen\":true,\"client\":\"heirloom-hp\",\"dh\":true,\"ds\":\"\",\"eqch\":true,\"fl\":true,\"host\":\"google.com\",\"jam\":0,\"jsonp\":true,\"msgs\":{\"cibl\":\"Clear Search\",\"dym\":\"Did you mean:\",\"lcky\":\"I\\u0026#39;m Feeling Lucky\",\"lml\":\"Learn more\",\"oskt\":\"Input tools\",\"psrc\":\"This search was removed from your \\u003Ca href=\\\"/history\\\"\\u003EWeb History\\u003C/a\\u003E\",\"psrl\":\"Remove\",\"sbit\":\"Search by image\",\"srch\":\"Google Search\"},\"ovr\":{},\"pq\":\"\",\"qcpw\":false,\"scd\":10,\"sce\":5,\"stok\":\"PoibB8u7e-sS13448HMOoIqGSu4\"},\"pcc\":{}};google.y.first.push(function(){if(google.med){google.med('init');google.initHistory();google.med('history');}});if(google.j&&google.j.en&&google.j.xi){window.setTimeout(google.j.xi,0);}</script></div><script>(function(){if(google.timers&&google.timers.load.t){var b,c,d,e,g=function(a,f){a.removeEventListener?(a.removeEventListener(\"load\",f,!1),a.removeEventListener(\"error\",f,!1)):(a.detachEvent(\"onload\",f),a.detachEvent(\"onerror\",f))},h=function(a){e=(new Date).getTime();++c;a=a||window.event;a=a.target||a.srcElement;g(a,h)},k=document.getElementsByTagName(\"img\");b=k.length;for(var l=c=0,m;l<b;++l)m"
|
42
|
+
],
|
43
|
+
[
|
44
|
+
"read",
|
45
|
+
"=k[l],m.complete||\"string\"!=typeof m.src||!m.src?++c:m.addEventListener?(m.addEventListener(\"load\",h,!1),m.addEventListener(\"error\",\nh,!1)):(m.attachEvent(\"onload\",h),m.attachEvent(\"onerror\",h));d=b-c;var n=function(){if(google.timers.load.t){google.timers.load.t.ol=(new Date).getTime();google.timers.load.t.iml=e;google.kCSI.imc=c;google.kCSI.imn=b;google.kCSI.imp=d;void 0!==google.stt&&(google.kCSI.stt=google.stt);google.csiReport&&google.csiReport()}};window.addEventListener?window.addEventListener(\"load\",n,!1):window.attachEvent&&\nwindow.attachEvent(\"onload\",n);google.timers.load.t.prt=e=(new Date).getTime()};})();\n</script></body></html>"
|
46
|
+
]
|
47
|
+
]
|
48
|
+
]
|
@@ -0,0 +1,92 @@
|
|
1
|
+
[
|
2
|
+
[
|
3
|
+
[
|
4
|
+
"read",
|
5
|
+
"* OK Gimap ready for requests from 72.16.218.22 n58mb218005052yhi\r\n"
|
6
|
+
],
|
7
|
+
[
|
8
|
+
"write",
|
9
|
+
"RUBY0001 LOGIN"
|
10
|
+
],
|
11
|
+
[
|
12
|
+
"write",
|
13
|
+
" "
|
14
|
+
],
|
15
|
+
[
|
16
|
+
"write",
|
17
|
+
"ben.olive@example.net"
|
18
|
+
],
|
19
|
+
[
|
20
|
+
"write",
|
21
|
+
" "
|
22
|
+
],
|
23
|
+
[
|
24
|
+
"write",
|
25
|
+
"password"
|
26
|
+
],
|
27
|
+
[
|
28
|
+
"write",
|
29
|
+
"\r\n"
|
30
|
+
],
|
31
|
+
[
|
32
|
+
"read",
|
33
|
+
"* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH\r\n"
|
34
|
+
],
|
35
|
+
[
|
36
|
+
"read",
|
37
|
+
"RUBY0001 OK ben.olive@example.net Ben Olive authenticated (Success)\r\n"
|
38
|
+
],
|
39
|
+
[
|
40
|
+
"write",
|
41
|
+
"RUBY0002 EXAMINE"
|
42
|
+
],
|
43
|
+
[
|
44
|
+
"write",
|
45
|
+
" "
|
46
|
+
],
|
47
|
+
[
|
48
|
+
"write",
|
49
|
+
"INBOX"
|
50
|
+
],
|
51
|
+
[
|
52
|
+
"write",
|
53
|
+
"\r\n"
|
54
|
+
],
|
55
|
+
[
|
56
|
+
"read",
|
57
|
+
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen $Phishing $NotPhishing)\r\n"
|
58
|
+
],
|
59
|
+
[
|
60
|
+
"read",
|
61
|
+
"* OK [PERMANENTFLAGS ()] Flags permitted.\r\n"
|
62
|
+
],
|
63
|
+
[
|
64
|
+
"read",
|
65
|
+
"* OK [UIDVALIDITY 1] UIDs valid.\r\n"
|
66
|
+
],
|
67
|
+
[
|
68
|
+
"read",
|
69
|
+
"* 206 EXISTS\r\n"
|
70
|
+
],
|
71
|
+
[
|
72
|
+
"read",
|
73
|
+
"* 0 RECENT\r\n"
|
74
|
+
],
|
75
|
+
[
|
76
|
+
"read",
|
77
|
+
"* OK [UIDNEXT 5504] Predicted next UID.\r\n"
|
78
|
+
],
|
79
|
+
[
|
80
|
+
"read",
|
81
|
+
"* OK [HIGHESTMODSEQ 609503]\r\n"
|
82
|
+
],
|
83
|
+
[
|
84
|
+
"read",
|
85
|
+
"RUBY0002 OK [READ-ONLY] INBOX selected. (Success)\r\n"
|
86
|
+
],
|
87
|
+
[
|
88
|
+
"read",
|
89
|
+
null
|
90
|
+
]
|
91
|
+
]
|
92
|
+
]
|
data/spec/spec_helper.rb
CHANGED
@@ -3,6 +3,20 @@
|
|
3
3
|
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
4
|
# loaded once.
|
5
5
|
#
|
6
|
+
module SpawnTestServers
|
7
|
+
# Spawn a new server that will send the specified payload when connected to.
|
8
|
+
# The listening port number will be returned
|
9
|
+
def spawn_server(payload)
|
10
|
+
server = TCPServer.new("127.0.0.1", 0)
|
11
|
+
fork do
|
12
|
+
client = server.accept
|
13
|
+
client.write(payload)
|
14
|
+
client.close
|
15
|
+
end
|
16
|
+
server.addr[1]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
6
20
|
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
21
|
RSpec.configure do |config|
|
8
22
|
config.treat_symbols_as_metadata_keys_with_true_values = true
|
@@ -14,4 +28,6 @@ RSpec.configure do |config|
|
|
14
28
|
# the seed, which is printed after each run.
|
15
29
|
# --seed 1234
|
16
30
|
config.order = 'random'
|
31
|
+
|
32
|
+
config.include(SpawnTestServers)
|
17
33
|
end
|
data/spec/tcr_spec.rb
CHANGED
@@ -1,13 +1,23 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
require "tcr"
|
3
3
|
require "net/protocol"
|
4
|
+
require "net/http"
|
5
|
+
require "net/imap"
|
4
6
|
require "net/smtp"
|
7
|
+
require 'thread'
|
8
|
+
|
5
9
|
|
6
10
|
describe TCR do
|
7
11
|
before(:each) do
|
8
12
|
TCR.configuration.reset_defaults!
|
9
13
|
end
|
10
14
|
|
15
|
+
around(:each) do |example|
|
16
|
+
File.unlink("test.json") if File.exists?("test.json")
|
17
|
+
example.run
|
18
|
+
File.unlink("test.json") if File.exists?("test.json")
|
19
|
+
end
|
20
|
+
|
11
21
|
describe ".configuration" do
|
12
22
|
it "has a default cassette location configured" do
|
13
23
|
TCR.configuration.cassette_library_dir.should == "fixtures/tcr_cassettes"
|
@@ -16,6 +26,10 @@ describe TCR do
|
|
16
26
|
it "has an empty list of hook ports by default" do
|
17
27
|
TCR.configuration.hook_tcp_ports.should == []
|
18
28
|
end
|
29
|
+
|
30
|
+
it "defaults to erroring on read/write mismatch access" do
|
31
|
+
TCR.configuration.block_for_reads.should be_false
|
32
|
+
end
|
19
33
|
end
|
20
34
|
|
21
35
|
describe ".configure" do
|
@@ -30,6 +44,12 @@ describe TCR do
|
|
30
44
|
TCR.configure { |c| c.hook_tcp_ports = [25] }
|
31
45
|
}.to change{ TCR.configuration.hook_tcp_ports }.from([]).to([25])
|
32
46
|
end
|
47
|
+
|
48
|
+
it "configures allowing a blocking read mode" do
|
49
|
+
expect {
|
50
|
+
TCR.configure { |c| c.block_for_reads = true }
|
51
|
+
}.to change{ TCR.configuration.block_for_reads }.from(false).to(true)
|
52
|
+
end
|
33
53
|
end
|
34
54
|
|
35
55
|
it "raises an error if you connect to a hooked port without using a cassette" do
|
@@ -62,16 +82,50 @@ describe TCR do
|
|
62
82
|
end
|
63
83
|
end
|
64
84
|
|
85
|
+
describe "block_for_reads" do
|
86
|
+
before(:each) {
|
87
|
+
TCR.configure { |c|
|
88
|
+
c.hook_tcp_ports = [9999]
|
89
|
+
c.cassette_library_dir = '.'
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
it "blocks read thread until data is available instead of raising mismatch error" do
|
94
|
+
TCR.configure { |c| c.block_for_reads = true }
|
95
|
+
reads = Queue.new
|
96
|
+
|
97
|
+
TCR.use_cassette("spec/fixtures/block_for_reads") do
|
98
|
+
sock = TCPSocket.open("google.com", 9999)
|
99
|
+
|
100
|
+
t = Thread.new do
|
101
|
+
reads << sock.gets
|
102
|
+
end
|
103
|
+
|
104
|
+
expect(reads.size).to eq(0)
|
105
|
+
sock.print("hello\n")
|
106
|
+
t.value
|
107
|
+
expect(reads.size).to eq(1)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context "when disabled" do
|
112
|
+
it "raises mismatch error" do
|
113
|
+
TCR.use_cassette("spec/fixtures/block_for_reads") do
|
114
|
+
sock = TCPSocket.open("google.com", 9999)
|
115
|
+
expect {
|
116
|
+
Timeout::timeout(1) { sock.gets }
|
117
|
+
}.to raise_error(TCR::DirectionMismatchError)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
65
123
|
describe ".use_cassette" do
|
66
124
|
before(:each) {
|
67
125
|
TCR.configure { |c|
|
68
126
|
c.hook_tcp_ports = [25]
|
69
127
|
c.cassette_library_dir = "."
|
70
128
|
}
|
71
|
-
File.unlink("test.json") if File.exists?("test.json")
|
72
|
-
}
|
73
|
-
after(:each) {
|
74
|
-
File.unlink("test.json") if File.exists?("test.json")
|
75
129
|
}
|
76
130
|
|
77
131
|
it "requires a block to call" do
|
@@ -90,7 +144,6 @@ describe TCR do
|
|
90
144
|
expect {
|
91
145
|
TCR.use_cassette("test") do
|
92
146
|
tcp_socket = TCPSocket.open("aspmx.l.google.com", 25)
|
93
|
-
tcp_socket.close
|
94
147
|
end
|
95
148
|
}.to change{ File.exists?("./test.json") }.from(false).to(true)
|
96
149
|
end
|
@@ -126,6 +179,42 @@ describe TCR do
|
|
126
179
|
}.to raise_error(TCR::DirectionMismatchError)
|
127
180
|
end
|
128
181
|
|
182
|
+
it "stubs out Socket#gets" do
|
183
|
+
TCR.configure { |c|
|
184
|
+
c.hook_tcp_ports = [993]
|
185
|
+
c.block_for_reads = true
|
186
|
+
}
|
187
|
+
expect {
|
188
|
+
TCR.use_cassette("spec/fixtures/google_imap") do
|
189
|
+
conn = Net::IMAP.new("imap.gmail.com", 993, true)
|
190
|
+
conn.login("ben.olive@example.net", "password")
|
191
|
+
conn.examine(Net::IMAP.encode_utf7("INBOX"))
|
192
|
+
conn.disconnect
|
193
|
+
end
|
194
|
+
}.not_to raise_error
|
195
|
+
end
|
196
|
+
|
197
|
+
it "stubs out Socket#read" do
|
198
|
+
TCR.configure { |c|
|
199
|
+
c.hook_tcp_ports = [23]
|
200
|
+
}
|
201
|
+
TCR.use_cassette("spec/fixtures/starwars_telnet") do
|
202
|
+
sock = TCPSocket.open("towel.blinkenlights.nl", 23)
|
203
|
+
expect(sock.read(50).length).to eq(50)
|
204
|
+
sock.close
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
it "supports ssl sockets" do
|
209
|
+
TCR.configure { |c| c.hook_tcp_ports = [443] }
|
210
|
+
http = Net::HTTP.new("www.google.com", 443)
|
211
|
+
http.use_ssl = true
|
212
|
+
expect {
|
213
|
+
TCR.use_cassette("spec/fixtures/google_https") do
|
214
|
+
http.request(Net::HTTP::Get.new("/"))
|
215
|
+
end
|
216
|
+
}.not_to raise_error
|
217
|
+
end
|
129
218
|
|
130
219
|
context "multiple connections" do
|
131
220
|
it "records multiple sessions per cassette" do
|
@@ -152,6 +241,35 @@ describe TCR do
|
|
152
241
|
end
|
153
242
|
end
|
154
243
|
|
244
|
+
context "when a cassette is recorded with connections opens concurrently" do
|
245
|
+
let(:ports) { ["Apple", "Banana"].map { |payload| spawn_server(payload) } }
|
246
|
+
before(:each) do
|
247
|
+
TCR.configure { |c|
|
248
|
+
c.hook_tcp_ports = ports
|
249
|
+
c.cassette_library_dir = "."
|
250
|
+
}
|
251
|
+
end
|
252
|
+
before(:each) do
|
253
|
+
TCR.use_cassette("test") do
|
254
|
+
apple = TCPSocket.open("127.0.0.1", ports.first)
|
255
|
+
banana = TCPSocket.open("127.0.0.1", ports.last)
|
256
|
+
|
257
|
+
banana.gets
|
258
|
+
apple.gets
|
259
|
+
|
260
|
+
banana.close
|
261
|
+
apple.close
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
it "replays the sessions in the order they were created" do
|
266
|
+
TCR.use_cassette("test") do
|
267
|
+
apple = TCPSocket.open("127.0.0.1", ports.first)
|
268
|
+
expect(apple.gets).to eq("Apple")
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
155
273
|
it "raises an error if you try to playback more sessions than you previously recorded" do
|
156
274
|
expect {
|
157
275
|
TCR.use_cassette("spec/fixtures/multitest-smtp") do
|
@@ -163,4 +281,4 @@ describe TCR do
|
|
163
281
|
end
|
164
282
|
end
|
165
283
|
end
|
166
|
-
end
|
284
|
+
end
|
metadata
CHANGED
@@ -1,46 +1,41 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tcr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.0.5
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Rob Forman
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date:
|
11
|
+
date: 2015-01-20 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rspec
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - ">="
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '0'
|
22
20
|
type: :development
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - ">="
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: geminabox
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - ">="
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '0'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - ">="
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '0'
|
46
41
|
description: TCR is a lightweight VCR for TCP sockets.
|
@@ -50,8 +45,9 @@ executables: []
|
|
50
45
|
extensions: []
|
51
46
|
extra_rdoc_files: []
|
52
47
|
files:
|
53
|
-
- .gitignore
|
54
|
-
- .rvmrc
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rvmrc"
|
50
|
+
- ".travis.yml"
|
55
51
|
- Gemfile
|
56
52
|
- LICENSE.txt
|
57
53
|
- README.md
|
@@ -62,39 +58,46 @@ files:
|
|
62
58
|
- lib/tcr/errors.rb
|
63
59
|
- lib/tcr/recordable_tcp_socket.rb
|
64
60
|
- lib/tcr/version.rb
|
61
|
+
- spec/fixtures/block_for_reads.json
|
62
|
+
- spec/fixtures/google_https.json
|
63
|
+
- spec/fixtures/google_imap.json
|
65
64
|
- spec/fixtures/google_smtp.json
|
66
65
|
- spec/fixtures/multitest-smtp.json
|
67
66
|
- spec/fixtures/multitest.json
|
67
|
+
- spec/fixtures/starwars_telnet.json
|
68
68
|
- spec/spec_helper.rb
|
69
69
|
- spec/tcr_spec.rb
|
70
70
|
- tcr.gemspec
|
71
71
|
homepage: ''
|
72
72
|
licenses: []
|
73
|
+
metadata: {}
|
73
74
|
post_install_message:
|
74
75
|
rdoc_options: []
|
75
76
|
require_paths:
|
76
77
|
- lib
|
77
78
|
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
-
none: false
|
79
79
|
requirements:
|
80
|
-
- -
|
80
|
+
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
|
-
none: false
|
85
84
|
requirements:
|
86
|
-
- -
|
85
|
+
- - ">="
|
87
86
|
- !ruby/object:Gem::Version
|
88
87
|
version: '0'
|
89
88
|
requirements: []
|
90
89
|
rubyforge_project:
|
91
|
-
rubygems_version:
|
90
|
+
rubygems_version: 2.2.2
|
92
91
|
signing_key:
|
93
|
-
specification_version:
|
92
|
+
specification_version: 4
|
94
93
|
summary: TCR is a lightweight VCR for TCP sockets.
|
95
94
|
test_files:
|
95
|
+
- spec/fixtures/block_for_reads.json
|
96
|
+
- spec/fixtures/google_https.json
|
97
|
+
- spec/fixtures/google_imap.json
|
96
98
|
- spec/fixtures/google_smtp.json
|
97
99
|
- spec/fixtures/multitest-smtp.json
|
98
100
|
- spec/fixtures/multitest.json
|
101
|
+
- spec/fixtures/starwars_telnet.json
|
99
102
|
- spec/spec_helper.rb
|
100
103
|
- spec/tcr_spec.rb
|