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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2778cafda0e669461d11038fb18214895b8718f7
4
- data.tar.gz: 1011b06506a60f98edaf3d980e37b3d43d143cc6
3
+ metadata.gz: 434e46a127df3a3f2b582f0f756fa532af1dd7ec
4
+ data.tar.gz: a001d83f06e97f59587e9d2bb9986f288c53cde1
5
5
  SHA512:
6
- metadata.gz: 547792d0cf8db701e128bd41ea1de9ad9e7b05caa3751d6811a322556cfbe50efa7aa2ee443a6841ae666030f52e7be8e5ad02d494c0206e9a1be9ac77aca85d
7
- data.tar.gz: aa0bd33e8d43f0116cc838c83f2549c119e3dce4548ac288adf1af9ea07ed42276318d6218495b892e88e0260b6e00626439c524372543ca4932d31f92e74c3a
6
+ metadata.gz: 17c790c383ba196b630152af5e31cd9e1a242f72225352715f9f98b3ad98395e2f3dea20f0576c551ef17f4d879f3c5bca9a816d94a0e636255d20024c3b8775
7
+ data.tar.gz: cd5010064c3b9b4e3e26c3bf23c44a3c253aa29a5ab10d702ce8787ebaa5bd02f1b3699f703c1d9268cf2c6076f60487dc98ea61fc8fd9701d8cf331eb14b95b
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ branches:
6
+ only:
7
+ - master
8
+ gemfile: Gemfile
9
+ notifications:
10
+ recipients:
11
+ - ben.biddington@gmail.com
data/README.md CHANGED
@@ -1,31 +1,42 @@
1
- # RichardIii
1
+ # Richard III
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://secure.travis-ci.org/ben-biddington/richard_iii.png)](http://travis-ci.org/ben-biddington/richard_iii)
4
4
 
5
- ## Installation
5
+ Based on [HTTP/1.1](http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html)
6
6
 
7
- Add this line to your application's Gemfile:
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
- And then execute:
14
-
15
- $ bundle
16
-
17
- Or install it yourself as:
18
-
19
- $ gem install richard_iii
20
-
21
- ## Usage
22
-
23
- TODO: Write usage instructions here
24
-
25
- ## Contributing
26
-
27
- 1. Fork it ( https://github.com/[my-github-username]/richard_iii/fork )
28
- 2. Create your feature branch (`git checkout -b my-new-feature`)
29
- 3. Commit your changes (`git commit -am 'Add some feature'`)
30
- 4. Push to the branch (`git push origin my-new-feature`)
31
- 5. Create a new Pull Request
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
@@ -1,3 +1,3 @@
1
1
  module RichardIii
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/richard_iii.rb CHANGED
@@ -1,5 +1,48 @@
1
- require "richard_iii/version"
1
+ module Richard; end
2
2
 
3
- module RichardIii
4
- # Your code goes here...
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.add_development_dependency "bundler", "~> 1.7"
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,9 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/pride'
3
+ require 'richard_iii'
4
+
5
+ include Richard
6
+
7
+ dir = File.join(File.dirname(__FILE__))
8
+
9
+ Dir.glob(File.join(dir, "support", "**", "*.rb")).each {|f| require f}
@@ -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.2
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-08-31 00:00:00.000000000 Z
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.7'
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.7'
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/the_basics.rb
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/the_basics.rb
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