richard_iii 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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