richard_iii 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +11 -0
- data/README.md +37 -26
- data/Rakefile +15 -0
- data/lib/richard_iii/curl_reply.rb +72 -0
- data/lib/richard_iii/internal/curl.rb +68 -0
- data/lib/richard_iii/internal/text_line.rb +29 -0
- data/lib/richard_iii/internal/xml_format.rb +17 -0
- data/lib/richard_iii/request_and_response.rb +33 -0
- data/lib/richard_iii/version.rb +1 -1
- data/lib/richard_iii.rb +46 -3
- data/richard_iii.gemspec +3 -1
- data/test/acceptance.tests/an_example.rb +34 -0
- data/test/helper.rb +9 -0
- data/test/integration.tests/adapters/internet.rb +19 -0
- data/test/support/extensions.rb +13 -0
- data/test/support/spy_internet.rb +18 -0
- data/test/unit.tests/can_conduct_conversations.rb +46 -0
- data/test/unit.tests/can_find_out_why_a_match_failed.rb +69 -0
- data/test/unit.tests/can_match_pretty_formatted_xml_bodies.rb +36 -0
- data/test/unit.tests/can_partial_match_on_replies.rb +75 -0
- data/test/unit.tests/can_pattern_match_replies.rb +37 -0
- data/test/unit.tests/earls_may_be_relative_or_absolute.rb +15 -0
- data/test/unit.tests/the_basics.rb +59 -0
- metadata +62 -6
- data/test/the_basics.rb +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 434e46a127df3a3f2b582f0f756fa532af1dd7ec
|
4
|
+
data.tar.gz: a001d83f06e97f59587e9d2bb9986f288c53cde1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 17c790c383ba196b630152af5e31cd9e1a242f72225352715f9f98b3ad98395e2f3dea20f0576c551ef17f4d879f3c5bca9a816d94a0e636255d20024c3b8775
|
7
|
+
data.tar.gz: cd5010064c3b9b4e3e26c3bf23c44a3c253aa29a5ab10d702ce8787ebaa5bd02f1b3699f703c1d9268cf2c6076f60487dc98ea61fc8fd9701d8cf331eb14b95b
|
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,31 +1,42 @@
|
|
1
|
-
#
|
1
|
+
# Richard III
|
2
2
|
|
3
|
-
|
3
|
+
[](http://travis-ci.org/ben-biddington/richard_iii)
|
4
4
|
|
5
|
-
|
5
|
+
Based on [HTTP/1.1](http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html)
|
6
6
|
|
7
|
-
|
7
|
+
Here's what we'd like to be able to write about any restful microservice:
|
8
8
|
|
9
|
-
```ruby
|
10
|
-
gem 'richard_iii'
|
11
9
|
```
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
10
|
+
describe "An end-to-end example" do
|
11
|
+
it "lets me see raw text" do
|
12
|
+
when_I_request <<-TEXT
|
13
|
+
GET /1.1/statuses
|
14
|
+
Host: api.twitter.com
|
15
|
+
Accept: application/json
|
16
|
+
TEXT
|
17
|
+
|
18
|
+
then_I_get <<-REPLY
|
19
|
+
HTTP/1.1 400 Bad Request
|
20
|
+
content-length: 24
|
21
|
+
content-type: text/plain;charset=utf-8
|
22
|
+
date: Sat, 30 Aug 2014 01:30:43 UTC
|
23
|
+
server: tsa_a
|
24
|
+
set-cookie: guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC
|
25
|
+
strict-transport-security: max-age=631138519
|
26
|
+
x-connection-hash: 7710cb2762e0a47702d21bb7e10d3056
|
27
|
+
|
28
|
+
Bad Authentication data
|
29
|
+
REPLY
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def when_I_request(text)
|
35
|
+
fail "Not implemented"
|
36
|
+
end
|
37
|
+
|
38
|
+
def then_I_get(what)
|
39
|
+
fail "Not implemented"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
data/Rakefile
CHANGED
@@ -1,2 +1,17 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
namespace :test do
|
6
|
+
%w{unit integration acceptance}.each do |name|
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.name = name
|
9
|
+
t.test_files = FileList["test/#{name}.tests/**/*.rb"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run all tests"
|
14
|
+
task :all => ["test:unit", "test:integration", "test:acceptance"]
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => "test:unit"
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Richard
|
2
|
+
class CurlReply
|
3
|
+
attr_reader :missing, :surplus
|
4
|
+
|
5
|
+
def initialize(text)
|
6
|
+
@text = text
|
7
|
+
@missing = @surplus = []
|
8
|
+
@pretty = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def use_pretty_xml
|
12
|
+
@pretty = true;
|
13
|
+
end
|
14
|
+
|
15
|
+
def matches?(expected)
|
16
|
+
actual_lines = strip(@text)
|
17
|
+
|
18
|
+
expected_lines = parse(expected)
|
19
|
+
|
20
|
+
matches = actual_lines.select do |line|
|
21
|
+
expected_lines.any?{|it| it.matches?(line)}
|
22
|
+
end
|
23
|
+
|
24
|
+
expectations_that_did_not_match_anything = expected_lines.select do |expected|
|
25
|
+
actual_lines.none?{|line| expected.matches?(line)}
|
26
|
+
end
|
27
|
+
|
28
|
+
@missing = expectations_that_did_not_match_anything.map(&:text)
|
29
|
+
@surplus = actual_lines - matches
|
30
|
+
|
31
|
+
return matches.size.eql? expected_lines.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def eql?(text)
|
35
|
+
strip(@text).eql? strip(text)
|
36
|
+
end
|
37
|
+
|
38
|
+
def equals?(text); self.eql? text; end
|
39
|
+
def ==(text); self.eql? text; end
|
40
|
+
|
41
|
+
def to_s; @text; end
|
42
|
+
def inspect; to_s; end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def parse(text)
|
47
|
+
convert_all strip(text)
|
48
|
+
end
|
49
|
+
|
50
|
+
def strip(text)
|
51
|
+
format(text).map(&:chomp).map(&:strip)
|
52
|
+
end
|
53
|
+
|
54
|
+
def format(text)
|
55
|
+
return text.lines unless @pretty
|
56
|
+
|
57
|
+
lines = text.lines
|
58
|
+
|
59
|
+
body = lines.delete_at(lines.size-1)
|
60
|
+
|
61
|
+
lines += Richard::Internal::XmlFormat.pretty(body)
|
62
|
+
|
63
|
+
lines
|
64
|
+
end
|
65
|
+
|
66
|
+
def convert_all(lines=[])
|
67
|
+
lines.map do |text|
|
68
|
+
text.start_with?("/") ? Richard::Internal::PatternLine.new(text) : Richard::Internal::TextLine.new(text)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Richard
|
2
|
+
module Internal
|
3
|
+
class BasicRequestLineParser
|
4
|
+
class << self
|
5
|
+
def from(text, headers)
|
6
|
+
lines = text.lines.map(&:chomp).map(&:strip)
|
7
|
+
|
8
|
+
verb = lines.first.match(/^(\w+)/)[1]
|
9
|
+
path = lines.first.match(/(\S+)$/)[1]
|
10
|
+
host = headers["Host"]
|
11
|
+
|
12
|
+
is_absolute = path.include?('://')
|
13
|
+
|
14
|
+
fail "Missing host header. When you supply a relative earl you have to supply host header too." if !is_absolute && host.nil?
|
15
|
+
|
16
|
+
earl = is_absolute ? path : "https://#{host}#{path}"
|
17
|
+
|
18
|
+
RequestLine.new(verb, earl)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class BasicHeaderParser
|
24
|
+
class << self
|
25
|
+
def from(text)
|
26
|
+
lines = text.lines.map(&:chomp).map(&:strip)
|
27
|
+
lines.drop(1).take_while{|line| !line.strip.empty?}.map do |line|
|
28
|
+
name,value = line.split(':')
|
29
|
+
RequestHeader.new name.strip, value.strip
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class BasicBodyParser
|
36
|
+
class << self
|
37
|
+
def from(text)
|
38
|
+
lines = text.lines.map(&:chomp).map(&:strip)
|
39
|
+
lines.drop_while{|line| !line.strip.empty?}.drop(1).first
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class CurlFormat
|
45
|
+
class << self
|
46
|
+
def as_string(reply)
|
47
|
+
return '' if reply.nil?
|
48
|
+
CurlReply.new((
|
49
|
+
["HTTP/1.1 #{reply.status}"] +
|
50
|
+
headers_from(reply) +
|
51
|
+
["\n#{reply.body.strip}\n"]
|
52
|
+
).join("\n"))
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def headers_from(reply)
|
58
|
+
reply.headers.map do |name, value|
|
59
|
+
"#{name}: #{value}"
|
60
|
+
end.to_a
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
RequestLine = Struct.new 'RequestLine' , :verb, :uri
|
66
|
+
RequestHeader = Struct.new 'RequestHeader', :name, :value
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Richard
|
2
|
+
module Internal
|
3
|
+
class TextLine
|
4
|
+
attr_reader :text
|
5
|
+
|
6
|
+
def initialize(text)
|
7
|
+
@text = text || fail("You need to supply some text even if it's empty")
|
8
|
+
end
|
9
|
+
|
10
|
+
def matches?(text)
|
11
|
+
@text.eql?(text)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class PatternLine
|
16
|
+
attr_reader :text
|
17
|
+
|
18
|
+
def initialize(text)
|
19
|
+
@text = text || fail("You need to supply some text even if it's empty")
|
20
|
+
end
|
21
|
+
|
22
|
+
# [!] expects lines to start and end with /
|
23
|
+
def matches?(text)
|
24
|
+
regex = Regexp.new(@text.slice(1,(@text.size-2)))
|
25
|
+
false == regex.match(text).nil?
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Richard
|
2
|
+
module Internal
|
3
|
+
class XmlFormat
|
4
|
+
class << self
|
5
|
+
def pretty(text)
|
6
|
+
require 'nokogiri'
|
7
|
+
|
8
|
+
doc = Nokogiri.XML(text) do |config|
|
9
|
+
config.default_xml.noblanks
|
10
|
+
end
|
11
|
+
|
12
|
+
doc.to_xml(:indent => 2).lines.drop(1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Richard
|
2
|
+
class Request
|
3
|
+
attr_reader :verb, :uri, :headers, :body
|
4
|
+
|
5
|
+
def initialize(opts={})
|
6
|
+
@verb,@uri,@headers,@body = opts[:verb],opts[:uri],opts[:headers],opts[:body]
|
7
|
+
end
|
8
|
+
|
9
|
+
def eql?(other)
|
10
|
+
self.verb.eql?(other.verb) && self.uri.eql?(other.uri) && self.headers == other.headers && self.body.eql?(other.body)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Response
|
15
|
+
attr_reader :status, :headers, :body
|
16
|
+
|
17
|
+
def initialize(opts={})
|
18
|
+
@status,@headers,@body = opts[:status],opts[:headers],opts[:body]
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.status.eql?(other.status) && self.headers == other.headers && self.body.eql?(other.body)
|
23
|
+
end
|
24
|
+
|
25
|
+
def each_header(&block)
|
26
|
+
self.headers.each_pair{|k,v| block.call(k,v)}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Status = Struct.new 'Status', :code, :desc do
|
31
|
+
def to_s; "#{code} #{desc}"; end
|
32
|
+
end
|
33
|
+
end
|
data/lib/richard_iii/version.rb
CHANGED
data/lib/richard_iii.rb
CHANGED
@@ -1,5 +1,48 @@
|
|
1
|
-
|
1
|
+
module Richard; end
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
dir = File.join(File.dirname(__FILE__))
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift File.join(dir, 'richard_iii')
|
6
|
+
|
7
|
+
Dir.glob(File.join(dir, "richard_iii", "**", "*.rb")).each {|f| require f}
|
8
|
+
|
9
|
+
module Richard
|
10
|
+
class III
|
11
|
+
def initialize(opts={})
|
12
|
+
@internet = opts[:internet] || fail("You need to supply :internet")
|
13
|
+
end
|
14
|
+
|
15
|
+
def exec(text)
|
16
|
+
request_line = request_line_from text
|
17
|
+
|
18
|
+
reply = @internet.execute(
|
19
|
+
Richard::Request.new(
|
20
|
+
:verb => request_line.verb,
|
21
|
+
:uri => request_line.uri,
|
22
|
+
:headers => headers_from(text),
|
23
|
+
:body => body_from(text)
|
24
|
+
)
|
25
|
+
)
|
26
|
+
|
27
|
+
Internal::CurlFormat.as_string reply
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def request_line_from(text)
|
33
|
+
Richard::Internal::BasicRequestLineParser.from text, headers_from(text)
|
34
|
+
end
|
35
|
+
|
36
|
+
def headers_from(text)
|
37
|
+
result = {}
|
38
|
+
|
39
|
+
Internal::BasicHeaderParser.from(text).each{|h| result[h.name] = h.value }
|
40
|
+
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def body_from(text)
|
45
|
+
Internal::BasicBodyParser.from(text)
|
46
|
+
end
|
47
|
+
end
|
5
48
|
end
|
data/richard_iii.gemspec
CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
-
spec.
|
21
|
+
spec.add_dependency "nokogiri"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
23
|
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "tinternet"
|
23
25
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "An end-to-end example" do
|
4
|
+
it "lets me see raw text" do
|
5
|
+
when_I_request <<-TEXT
|
6
|
+
GET /1.1/statuses
|
7
|
+
Host: api.twitter.com
|
8
|
+
Accept: application/json
|
9
|
+
TEXT
|
10
|
+
|
11
|
+
then_I_get <<-REPLY
|
12
|
+
HTTP/1.1 400 Bad Request
|
13
|
+
content-length: 24
|
14
|
+
content-type: text/plain;charset=utf-8
|
15
|
+
date: Sat, 30 Aug 2014 01:30:43 UTC
|
16
|
+
server: tsa_a
|
17
|
+
set-cookie: guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC
|
18
|
+
strict-transport-security: max-age=631138519
|
19
|
+
x-connection-hash: 7710cb2762e0a47702d21bb7e10d3056
|
20
|
+
|
21
|
+
Bad Authentication data
|
22
|
+
REPLY
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def when_I_request(text)
|
28
|
+
fail "Not implemented"
|
29
|
+
end
|
30
|
+
|
31
|
+
def then_I_get(what)
|
32
|
+
fail "Not implemented"
|
33
|
+
end
|
34
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
class SystemInternet
|
4
|
+
class << self
|
5
|
+
def execute(request)
|
6
|
+
require 't'
|
7
|
+
reply = T::Internet.new.execute(T::Request.new(:verb => :get, :uri => "http://api.twitter.com/1.1/statuses"))
|
8
|
+
|
9
|
+
Richard::Response.new :status => reply.code
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "the internet adapter" do
|
15
|
+
it "can be asked to execute a basic get" do
|
16
|
+
reply = SystemInternet.execute Request.new(:verb => "GET", :uri => "http://api.twitter.com/1.1/statuses")
|
17
|
+
reply.status.must_equal 400
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'minitest/unit'
|
2
|
+
|
3
|
+
Richard::CurlReply.class_eval do
|
4
|
+
include MiniTest::Assertions
|
5
|
+
|
6
|
+
def must_match(expected)
|
7
|
+
assert(self.matches?(expected), "Expected <#{self}> to match <#{expected}>")
|
8
|
+
end
|
9
|
+
|
10
|
+
def must_not_match(expected)
|
11
|
+
assert(false === self.matches?(expected), "Expected <#{self}> not to match <#{expected}>")
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class SpyInternet
|
2
|
+
def initialize
|
3
|
+
@requests = []
|
4
|
+
end
|
5
|
+
|
6
|
+
def execute(request)
|
7
|
+
@requests << request
|
8
|
+
@response
|
9
|
+
end
|
10
|
+
|
11
|
+
def must_have_been_asked_to_execute(what)
|
12
|
+
@requests.any?{|it| it.eql? what}.must_be :==, true, "Unable to locate match for:\n\n#{what.inspect}\n\nin:\n\n#{@requests.inspect}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def always_return(response)
|
16
|
+
@response = response
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Full conversations" do
|
4
|
+
before do
|
5
|
+
@internet = SpyInternet.new
|
6
|
+
|
7
|
+
@internet.always_return(
|
8
|
+
Response.new(
|
9
|
+
:status => Status.new(400, 'Bad Request'),
|
10
|
+
:headers => {
|
11
|
+
'content-length' => 24,
|
12
|
+
'content-type' => 'text/plain;charset=utf-8',
|
13
|
+
'date' => 'Sat, 30 Aug 2014 01:30:43 UTC',
|
14
|
+
'server' => 'tsa_a',
|
15
|
+
'set-cookie' => 'guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC',
|
16
|
+
'strict-transport-security' => 'max-age=631138519',
|
17
|
+
'x-connection-hash' => '7710cb2762e0a47702d21bb7e10d3056'
|
18
|
+
},
|
19
|
+
:body => 'Bad Authentication data'
|
20
|
+
)
|
21
|
+
)
|
22
|
+
|
23
|
+
richard_iii = Richard::III.new :internet => @internet
|
24
|
+
|
25
|
+
@reply = richard_iii.exec <<-TEXT
|
26
|
+
GET /1.1/statuses
|
27
|
+
Host: api.twitter.com
|
28
|
+
Accept: application/json
|
29
|
+
TEXT
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can be used to conduct a conversation" do
|
33
|
+
assert_equal @reply, <<-REPLY
|
34
|
+
HTTP/1.1 400 Bad Request
|
35
|
+
content-length: 24
|
36
|
+
content-type: text/plain;charset=utf-8
|
37
|
+
date: Sat, 30 Aug 2014 01:30:43 UTC
|
38
|
+
server: tsa_a
|
39
|
+
set-cookie: guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC
|
40
|
+
strict-transport-security: max-age=631138519
|
41
|
+
x-connection-hash: 7710cb2762e0a47702d21bb7e10d3056
|
42
|
+
|
43
|
+
Bad Authentication data
|
44
|
+
REPLY
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Finding out why a match failed" do
|
4
|
+
before do
|
5
|
+
@internet = SpyInternet.new
|
6
|
+
|
7
|
+
@internet.always_return(
|
8
|
+
Response.new(
|
9
|
+
:status => Status.new(400, 'Bad Request'),
|
10
|
+
:headers => {
|
11
|
+
'content-length' => 24,
|
12
|
+
'content-type' => 'text/plain;charset=utf-8',
|
13
|
+
'date' => 'Sat, 30 Aug 2014 01:30:43 UTC',
|
14
|
+
'server' => 'tsa_a',
|
15
|
+
'set-cookie' => 'guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC',
|
16
|
+
'strict-transport-security' => 'max-age=631138519',
|
17
|
+
'x-connection-hash' => '7710cb2762e0a47702d21bb7e10d3056'
|
18
|
+
},
|
19
|
+
:body => 'Bad Authentication data'
|
20
|
+
)
|
21
|
+
)
|
22
|
+
|
23
|
+
richard_iii = Richard::III.new :internet => @internet
|
24
|
+
|
25
|
+
@reply = richard_iii.exec <<-TEXT
|
26
|
+
GET /1.1/statuses
|
27
|
+
Host: api.twitter.com
|
28
|
+
Accept: application/json
|
29
|
+
TEXT
|
30
|
+
end
|
31
|
+
|
32
|
+
it "tells you which expected lines are missing" do
|
33
|
+
expected = <<-REPLY
|
34
|
+
HTTP/1.1 400 Bad Request
|
35
|
+
content-type: text/plain;charset=utf-8
|
36
|
+
|
37
|
+
xxx_body_differs_xxx
|
38
|
+
REPLY
|
39
|
+
|
40
|
+
@reply.must_not_match expected
|
41
|
+
|
42
|
+
@reply.missing.must_equal ['xxx_body_differs_xxx']
|
43
|
+
end
|
44
|
+
|
45
|
+
it "tells you which lines are present that were not expected, i.e., are surplus" do
|
46
|
+
expected = <<-REPLY
|
47
|
+
HTTP/1.1 400 Bad Request
|
48
|
+
REPLY
|
49
|
+
|
50
|
+
@reply.matches? expected
|
51
|
+
|
52
|
+
@reply.surplus.must_equal [
|
53
|
+
"content-length: 24",
|
54
|
+
"content-type: text/plain;charset=utf-8",
|
55
|
+
"date: Sat, 30 Aug 2014 01:30:43 UTC",
|
56
|
+
"server: tsa_a",
|
57
|
+
"set-cookie: guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC",
|
58
|
+
"strict-transport-security: max-age=631138519",
|
59
|
+
"x-connection-hash: 7710cb2762e0a47702d21bb7e10d3056",
|
60
|
+
"",
|
61
|
+
"Bad Authentication data"
|
62
|
+
]
|
63
|
+
end
|
64
|
+
|
65
|
+
it "both surplus and missing are empty before matching" do
|
66
|
+
@reply.missing.must_equal []
|
67
|
+
@reply.surplus.must_equal []
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Matching pretty-formatted bodies" do
|
4
|
+
before do
|
5
|
+
@curl_reply = CurlReply.new <<-TEXT
|
6
|
+
HTTP/1.1 200 OK
|
7
|
+
Content-Type: text/xml; charset=utf-8
|
8
|
+
|
9
|
+
<?xml version="1.0" encoding="utf-8"?><current><city id="6058560" name="London" /><humidity value="76" unit="%"/><pressure value="1018" unit="hPa"/></current>
|
10
|
+
TEXT
|
11
|
+
end
|
12
|
+
|
13
|
+
it "matches when you have neither pretty formatted" do
|
14
|
+
@curl_reply.must_match <<-TEXT
|
15
|
+
HTTP/1.1 200 OK
|
16
|
+
Content-Type: text/xml; charset=utf-8
|
17
|
+
|
18
|
+
<?xml version="1.0" encoding="utf-8"?><current><city id="6058560" name="London" /><humidity value="76" unit="%"/><pressure value="1018" unit="hPa"/></current>
|
19
|
+
TEXT
|
20
|
+
end
|
21
|
+
|
22
|
+
it "matches when you say yes to pretty bodies" do
|
23
|
+
@curl_reply.use_pretty_xml
|
24
|
+
@curl_reply.must_match <<-TEXT
|
25
|
+
HTTP/1.1 200 OK
|
26
|
+
Content-Type: text/xml; charset=utf-8
|
27
|
+
|
28
|
+
<?xml version="1.0" encoding="utf-8"?>
|
29
|
+
<current>
|
30
|
+
<city id="6058560" name="London"/>
|
31
|
+
<humidity value="76" unit="%"/>
|
32
|
+
<pressure value="1018" unit="hPa"/>
|
33
|
+
</current>
|
34
|
+
TEXT
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Partial matching replies" do
|
4
|
+
before do
|
5
|
+
@internet = SpyInternet.new
|
6
|
+
|
7
|
+
@internet.always_return(
|
8
|
+
Response.new(
|
9
|
+
:status => Status.new(400, 'Bad Request'),
|
10
|
+
:headers => {
|
11
|
+
'content-length' => 24,
|
12
|
+
'content-type' => 'text/plain;charset=utf-8',
|
13
|
+
'date' => 'Sat, 30 Aug 2014 01:30:43 UTC',
|
14
|
+
'server' => 'tsa_a',
|
15
|
+
'set-cookie' => 'guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC',
|
16
|
+
'strict-transport-security' => 'max-age=631138519',
|
17
|
+
'x-connection-hash' => '7710cb2762e0a47702d21bb7e10d3056'
|
18
|
+
},
|
19
|
+
:body => 'Bad Authentication data'
|
20
|
+
)
|
21
|
+
)
|
22
|
+
|
23
|
+
richard_iii = Richard::III.new :internet => @internet
|
24
|
+
|
25
|
+
@reply = richard_iii.exec <<-TEXT
|
26
|
+
GET /1.1/statuses
|
27
|
+
Host: api.twitter.com
|
28
|
+
Accept: application/json
|
29
|
+
TEXT
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can match partially, so that you can ignore headers like date" do
|
33
|
+
expected = <<-REPLY
|
34
|
+
HTTP/1.1 400 Bad Request
|
35
|
+
content-type: text/plain;charset=utf-8
|
36
|
+
|
37
|
+
Bad Authentication data
|
38
|
+
REPLY
|
39
|
+
|
40
|
+
@reply.must_match expected
|
41
|
+
end
|
42
|
+
|
43
|
+
it "fails when the response line does not match" do
|
44
|
+
expected = <<-REPLY
|
45
|
+
HTTP/1.1 XXX_BAD_RESPONSE_LINE
|
46
|
+
content-type: text/plain;charset=utf-8
|
47
|
+
|
48
|
+
Bad Authentication data
|
49
|
+
REPLY
|
50
|
+
|
51
|
+
@reply.must_not_match expected
|
52
|
+
end
|
53
|
+
|
54
|
+
it "fails, for example, when you expected a header that is not present" do
|
55
|
+
expected = <<-REPLY
|
56
|
+
HTTP/1.1 400 Bad Request
|
57
|
+
xxx: this_one_is_not_present
|
58
|
+
|
59
|
+
Bad Authentication data
|
60
|
+
REPLY
|
61
|
+
|
62
|
+
@reply.must_not_match expected
|
63
|
+
end
|
64
|
+
|
65
|
+
it "fails when the body does not match" do
|
66
|
+
expected = <<-REPLY
|
67
|
+
HTTP/1.1 400 Bad Request
|
68
|
+
content-type: text/plain;charset=utf-8
|
69
|
+
|
70
|
+
Who says famine has to be depressing
|
71
|
+
REPLY
|
72
|
+
|
73
|
+
@reply.must_not_match expected
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Pattern matching on reply lines" do
|
4
|
+
before do
|
5
|
+
@curl_reply = CurlReply.new <<-TEXT
|
6
|
+
HTTP/1.1 400 Bad Request
|
7
|
+
|
8
|
+
content-type: text/plain;charset=utf-8
|
9
|
+
date: Sat, 30 Aug 2014 01:30:43 UTC
|
10
|
+
server: tsa_a
|
11
|
+
set-cookie: guest_id=v1%3A140936224322485447; Domain=.twitter.com; Path=/; Expires=Mon, 29-Aug-2016 01:30:43 UTC
|
12
|
+
strict-transport-security: max-age=631138519
|
13
|
+
x-connection-hash: 7710cb2762e0a47702d21bb7e10d3056
|
14
|
+
|
15
|
+
Bad Authentication data
|
16
|
+
TEXT
|
17
|
+
end
|
18
|
+
|
19
|
+
it "allows you to match part of a dynamic header, for example" do
|
20
|
+
@curl_reply.must_match <<-REPLY
|
21
|
+
HTTP/1.1 400 Bad Request
|
22
|
+
content-type: text/plain;charset=utf-8
|
23
|
+
/set-cookie: .+Domain=.twitter.com; Path=\/;/
|
24
|
+
REPLY
|
25
|
+
end
|
26
|
+
|
27
|
+
it "another example might be /content-length: \d+/" do
|
28
|
+
skip("WIP: for some reason the backslash is being dropped")
|
29
|
+
@curl_reply.must_match <<-REPLY
|
30
|
+
HTTP/1.1 400 Bad Request
|
31
|
+
content-type: text/plain;charset=utf-8
|
32
|
+
/content-length: \d+/
|
33
|
+
REPLY
|
34
|
+
end
|
35
|
+
|
36
|
+
it "how are we going to fail in a way that tells you a pattern match failed?"
|
37
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe "Request earls may be absolute or relative" do
|
4
|
+
it "must have host header when relative" do
|
5
|
+
spy_internet = SpyInternet.new
|
6
|
+
|
7
|
+
richard_iii = Richard::III.new :internet => spy_internet
|
8
|
+
|
9
|
+
err = lambda { richard_iii.exec 'GET /1.1/statuses' }.must_raise RuntimeError
|
10
|
+
|
11
|
+
err.message.must_match /missing host header/i
|
12
|
+
end
|
13
|
+
|
14
|
+
it "ignores host header when earl is absolute -- that is it does not send it"
|
15
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.join '.', 'test', 'helper'
|
2
|
+
|
3
|
+
describe 'The basics of Richard III' do
|
4
|
+
it "can issue a very simple GET request" do
|
5
|
+
spy_internet = SpyInternet.new
|
6
|
+
|
7
|
+
richard_iii = Richard::III.new :internet => spy_internet
|
8
|
+
|
9
|
+
richard_iii.exec <<-TEXT
|
10
|
+
GET /1.1/statuses
|
11
|
+
Host: api.twitter.com
|
12
|
+
Accept: application/json
|
13
|
+
TEXT
|
14
|
+
|
15
|
+
spy_internet.must_have_been_asked_to_execute(
|
16
|
+
Request.new(
|
17
|
+
:verb => 'GET',
|
18
|
+
:uri => 'https://api.twitter.com/1.1/statuses',
|
19
|
+
:headers => {
|
20
|
+
'Host' => 'api.twitter.com',
|
21
|
+
'Accept' => 'application/json'
|
22
|
+
}
|
23
|
+
)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "can issue a very simple POST request" do
|
28
|
+
spy_internet = SpyInternet.new
|
29
|
+
|
30
|
+
richard_iii = Richard::III.new :internet => spy_internet
|
31
|
+
|
32
|
+
richard_iii.exec <<-TEXT
|
33
|
+
POST /1.1/statuses/update
|
34
|
+
Host: api.twitter.com
|
35
|
+
Accept: application/json
|
36
|
+
Content-type: application/x-www-form-urlencoded
|
37
|
+
|
38
|
+
status=Who%20says%20famine%20has%20to%20be%20depressing?
|
39
|
+
TEXT
|
40
|
+
|
41
|
+
spy_internet.must_have_been_asked_to_execute(
|
42
|
+
Request.new(
|
43
|
+
:verb => 'POST',
|
44
|
+
:uri => 'https://api.twitter.com/1.1/statuses/update',
|
45
|
+
:headers => {
|
46
|
+
'Host' => 'api.twitter.com',
|
47
|
+
'Accept' => 'application/json',
|
48
|
+
'Content-type' => 'application/x-www-form-urlencoded'
|
49
|
+
},
|
50
|
+
:body => 'status=Who%20says%20famine%20has%20to%20be%20depressing?'
|
51
|
+
)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# TEST: quotes are treated literally
|
56
|
+
# TEST: whitespace does not matter
|
57
|
+
# TEST: where does it read the protocol part (HTTP or HTTPS)
|
58
|
+
# TEST: looks like you can either supply absolute uri, or relative AND Host header
|
59
|
+
end
|
metadata
CHANGED
@@ -1,29 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: richard_iii
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben Biddington
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - "~>"
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
33
|
+
version: '1.6'
|
20
34
|
type: :development
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
38
|
- - "~>"
|
25
39
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
40
|
+
version: '1.6'
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: rake
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +52,20 @@ dependencies:
|
|
38
52
|
- - "~>"
|
39
53
|
- !ruby/object:Gem::Version
|
40
54
|
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tinternet
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
41
69
|
description: ''
|
42
70
|
email:
|
43
71
|
- ben.biddington@gmail.com
|
@@ -46,15 +74,32 @@ extensions: []
|
|
46
74
|
extra_rdoc_files: []
|
47
75
|
files:
|
48
76
|
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
49
78
|
- Gemfile
|
50
79
|
- LICENSE
|
51
80
|
- LICENSE.txt
|
52
81
|
- README.md
|
53
82
|
- Rakefile
|
54
83
|
- lib/richard_iii.rb
|
84
|
+
- lib/richard_iii/curl_reply.rb
|
85
|
+
- lib/richard_iii/internal/curl.rb
|
86
|
+
- lib/richard_iii/internal/text_line.rb
|
87
|
+
- lib/richard_iii/internal/xml_format.rb
|
88
|
+
- lib/richard_iii/request_and_response.rb
|
55
89
|
- lib/richard_iii/version.rb
|
56
90
|
- richard_iii.gemspec
|
57
|
-
- test/
|
91
|
+
- test/acceptance.tests/an_example.rb
|
92
|
+
- test/helper.rb
|
93
|
+
- test/integration.tests/adapters/internet.rb
|
94
|
+
- test/support/extensions.rb
|
95
|
+
- test/support/spy_internet.rb
|
96
|
+
- test/unit.tests/can_conduct_conversations.rb
|
97
|
+
- test/unit.tests/can_find_out_why_a_match_failed.rb
|
98
|
+
- test/unit.tests/can_match_pretty_formatted_xml_bodies.rb
|
99
|
+
- test/unit.tests/can_partial_match_on_replies.rb
|
100
|
+
- test/unit.tests/can_pattern_match_replies.rb
|
101
|
+
- test/unit.tests/earls_may_be_relative_or_absolute.rb
|
102
|
+
- test/unit.tests/the_basics.rb
|
58
103
|
homepage: ''
|
59
104
|
licenses:
|
60
105
|
- MIT
|
@@ -80,4 +125,15 @@ signing_key:
|
|
80
125
|
specification_version: 4
|
81
126
|
summary: ''
|
82
127
|
test_files:
|
83
|
-
- test/
|
128
|
+
- test/acceptance.tests/an_example.rb
|
129
|
+
- test/helper.rb
|
130
|
+
- test/integration.tests/adapters/internet.rb
|
131
|
+
- test/support/extensions.rb
|
132
|
+
- test/support/spy_internet.rb
|
133
|
+
- test/unit.tests/can_conduct_conversations.rb
|
134
|
+
- test/unit.tests/can_find_out_why_a_match_failed.rb
|
135
|
+
- test/unit.tests/can_match_pretty_formatted_xml_bodies.rb
|
136
|
+
- test/unit.tests/can_partial_match_on_replies.rb
|
137
|
+
- test/unit.tests/can_pattern_match_replies.rb
|
138
|
+
- test/unit.tests/earls_may_be_relative_or_absolute.rb
|
139
|
+
- test/unit.tests/the_basics.rb
|
data/test/the_basics.rb
DELETED
@@ -1,74 +0,0 @@
|
|
1
|
-
require 'minitest/autorun'
|
2
|
-
require 'minitest/pride'
|
3
|
-
|
4
|
-
module Richard
|
5
|
-
module Internal
|
6
|
-
class RequestLineParser
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
class RequestLine
|
11
|
-
attr_reader :method, :uri
|
12
|
-
|
13
|
-
def initialize(opts = {})
|
14
|
-
@method,@uri = opts[:method],opts[:uri]
|
15
|
-
end
|
16
|
-
|
17
|
-
def eql?(other)
|
18
|
-
return self.method == other.method && self.uri == other.uri
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
module Richard
|
25
|
-
class III
|
26
|
-
def initialize(opts={})
|
27
|
-
@internet = opts[:internet] || fail("You need to supply :internet")
|
28
|
-
end
|
29
|
-
|
30
|
-
def exec(text)
|
31
|
-
lines = text.lines.map(&:chomp)
|
32
|
-
|
33
|
-
verb = lines.first.match(/^(\w+)/)[1]
|
34
|
-
path = lines.first.match(/(\S+)$/)[1]
|
35
|
-
host = lines[1].match(/Host: (.+)$/)[1]
|
36
|
-
|
37
|
-
@internet.execute Request.new(:verb => verb, :uri => "https://#{host}#{path}")
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
class Request
|
43
|
-
attr_reader :verb, :uri
|
44
|
-
|
45
|
-
def initialize(opts={})
|
46
|
-
@verb,@uri = opts[:verb],opts[:uri]
|
47
|
-
end
|
48
|
-
|
49
|
-
def eql?(other)
|
50
|
-
self.verb.eql?(other.verb) && self.uri.eql?(other.uri)
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
describe 'The basics of Richard III' do
|
55
|
-
it "can issue a very simple request" do
|
56
|
-
spy_internet = MiniTest::Mock.new
|
57
|
-
|
58
|
-
spy_internet.expect :execute, nil do |actual|
|
59
|
-
actual.first.eql? Request.new(:verb => 'GET', :uri => 'https://api.twitter.com/1.1/statuses')
|
60
|
-
end
|
61
|
-
|
62
|
-
richard_iii = Richard::III.new :internet => spy_internet
|
63
|
-
|
64
|
-
richard_iii.exec <<-TEXT
|
65
|
-
GET /1.1/statuses
|
66
|
-
Host: api.twitter.com
|
67
|
-
Accept: application/json
|
68
|
-
TEXT
|
69
|
-
|
70
|
-
spy_internet.verify
|
71
|
-
end
|
72
|
-
|
73
|
-
# TEST: where does it read the protocol part (HTTP of HTTPS)
|
74
|
-
end
|