webmachine 0.1.0 → 0.2.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.
- data/Gemfile +11 -3
- data/README.md +55 -27
- data/lib/webmachine/adapters/mongrel.rb +84 -0
- data/lib/webmachine/adapters/webrick.rb +12 -3
- data/lib/webmachine/adapters.rb +1 -7
- data/lib/webmachine/configuration.rb +30 -0
- data/lib/webmachine/decision/conneg.rb +7 -72
- data/lib/webmachine/decision/flow.rb +13 -11
- data/lib/webmachine/decision/fsm.rb +1 -9
- data/lib/webmachine/decision/helpers.rb +27 -7
- data/lib/webmachine/errors.rb +1 -0
- data/lib/webmachine/headers.rb +12 -3
- data/lib/webmachine/locale/en.yml +2 -2
- data/lib/webmachine/media_type.rb +117 -0
- data/lib/webmachine/resource/callbacks.rb +9 -0
- data/lib/webmachine/streaming.rb +3 -3
- data/lib/webmachine/version.rb +1 -1
- data/lib/webmachine.rb +3 -1
- data/pkg/webmachine-0.1.0/Gemfile +16 -0
- data/pkg/webmachine-0.1.0/Guardfile +11 -0
- data/pkg/webmachine-0.1.0/README.md +90 -0
- data/pkg/webmachine-0.1.0/Rakefile +31 -0
- data/pkg/webmachine-0.1.0/examples/webrick.rb +19 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/adapters/webrick.rb +74 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/adapters.rb +15 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/conneg.rb +304 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/flow.rb +502 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/fsm.rb +79 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision/helpers.rb +80 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/decision.rb +12 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher/route.rb +85 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/dispatcher.rb +40 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/errors.rb +37 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/headers.rb +16 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/locale/en.yml +28 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/request.rb +56 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource/callbacks.rb +362 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource/encodings.rb +36 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/resource.rb +48 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/response.rb +49 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/streaming.rb +27 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/translation.rb +11 -0
- data/pkg/webmachine-0.1.0/lib/webmachine/version.rb +4 -0
- data/pkg/webmachine-0.1.0/lib/webmachine.rb +19 -0
- data/pkg/webmachine-0.1.0/spec/spec_helper.rb +13 -0
- data/pkg/webmachine-0.1.0/spec/tests.org +57 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/decision/conneg_spec.rb +152 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/decision/flow_spec.rb +1030 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher/route_spec.rb +109 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/dispatcher_spec.rb +34 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/headers_spec.rb +19 -0
- data/pkg/webmachine-0.1.0/spec/webmachine/request_spec.rb +24 -0
- data/pkg/webmachine-0.1.0/webmachine.gemspec +44 -0
- data/pkg/webmachine-0.1.0.gem +0 -0
- data/spec/webmachine/configuration_spec.rb +27 -0
- data/spec/webmachine/decision/conneg_spec.rb +18 -11
- data/spec/webmachine/decision/flow_spec.rb +2 -0
- data/spec/webmachine/decision/helpers_spec.rb +105 -0
- data/spec/webmachine/errors_spec.rb +13 -0
- data/spec/webmachine/headers_spec.rb +2 -1
- data/spec/webmachine/media_type_spec.rb +78 -0
- data/webmachine.gemspec +4 -1
- metadata +69 -11
@@ -0,0 +1,27 @@
|
|
1
|
+
module Webmachine
|
2
|
+
class StreamingEncoder
|
3
|
+
def initialize(resource, encoder, charsetter, body)
|
4
|
+
@resource, @encoder, @charsetter, @body = resource, encoder, charsetter, body
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class EnumerableEncoder < StreamingEncoder
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def each
|
12
|
+
body.each do |block|
|
13
|
+
yield @resource.send(@encoder, resource.send(@charsetter, block))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class CallableEncoder < StreamingEncoder
|
19
|
+
def call
|
20
|
+
@resource.send(@encoder, @resource.send(@charsetter, body.call))
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_proc
|
24
|
+
method(:call).to_proc
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'webmachine/headers'
|
2
|
+
require 'webmachine/request'
|
3
|
+
require 'webmachine/response'
|
4
|
+
require 'webmachine/errors'
|
5
|
+
require 'webmachine/decision'
|
6
|
+
require 'webmachine/streaming'
|
7
|
+
require 'webmachine/adapters'
|
8
|
+
require 'webmachine/dispatcher'
|
9
|
+
require 'webmachine/resource'
|
10
|
+
require 'webmachine/version'
|
11
|
+
|
12
|
+
# Webmachine is a toolkit for making well-behaved HTTP applications.
|
13
|
+
# It is based on the Erlang library of the same name.
|
14
|
+
module Webmachine
|
15
|
+
# Starts Webmachine serving requests
|
16
|
+
def self.run
|
17
|
+
Adapters.const_get(adapter).run
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path("..", __FILE__)
|
2
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'webmachine'
|
6
|
+
require 'rspec'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.mock_with :rspec
|
10
|
+
config.filter_run :focus => true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
* 2,2 Basic Rules
|
2
|
+
- HTTP/1.1 header field values can be folded onto multiple lines if
|
3
|
+
the continuation line begins with a space or horizontal tab. All
|
4
|
+
linear white space, including folding, has the same semantics as
|
5
|
+
SP. A recipient MAY replace any linear white space with a single
|
6
|
+
SP before interpreting the field value or forwarding the message
|
7
|
+
downstream.
|
8
|
+
- Many HTTP/1.1 header field values consist of words separated by
|
9
|
+
LWS or special characters. These special characters MUST be in a
|
10
|
+
quoted string to be used within a parameter value (as defined in
|
11
|
+
section 3.6).
|
12
|
+
* 3.1 HTTP Version
|
13
|
+
- Note that the major and minor numbers MUST be treated as separate
|
14
|
+
integers and that each MAY be incremented higher than a single
|
15
|
+
digit.
|
16
|
+
- Leading zeros MUST be ignored by recipients and MUST NOT be sent.
|
17
|
+
- An application that sends a request or response message that
|
18
|
+
includes HTTP-Version of "HTTP/1.1" MUST be at least conditionally
|
19
|
+
compliant with this specification.
|
20
|
+
- Applications that are at least conditionally compliant with this
|
21
|
+
specification SHOULD use an HTTP-Version of "HTTP/1.1" in their
|
22
|
+
messages, and MUST do so for any message that is not compatible
|
23
|
+
with HTTP/1.0.
|
24
|
+
- Since the protocol version indicates the protocol capability of
|
25
|
+
the sender, a proxy/gateway MUST NOT send a message with a version
|
26
|
+
indicator which is greater than its actual version. If a higher
|
27
|
+
version request is received, the proxy/gateway MUST either
|
28
|
+
downgrade the request version, or respond with an error, or switch
|
29
|
+
to tunnel behavior.
|
30
|
+
- Due to interoperability problems with HTTP/1.0 proxies discovered
|
31
|
+
since the publication of RFC 2068 [33], caching proxies MUST,
|
32
|
+
gateways MAY, and tunnels MUST NOT upgrade the request to the
|
33
|
+
highest version they support. The proxy/gateway's response to that
|
34
|
+
request MUST be in the same major version as the request.
|
35
|
+
* 3.2 URIs
|
36
|
+
** General Syntax
|
37
|
+
- Servers MUST be able to handle the URI of any resource they serve.
|
38
|
+
- Servers SHOULD be able to handle URIs of unbounded length if they
|
39
|
+
provide GET-based forms that could generate such URIs.
|
40
|
+
- A server SHOULD return 414 (Request-URI Too Long) status if a URI
|
41
|
+
is longer than the server can handle
|
42
|
+
** http URL
|
43
|
+
- If the abs_path is not present in the URL, it MUST be given as
|
44
|
+
"/" when used as a Request-URI for a resource.
|
45
|
+
- If a proxy receives a fully qualified domain name, the proxy MUST
|
46
|
+
NOT change the host name.
|
47
|
+
** URI Comparison
|
48
|
+
- When comparing two URIs to decide if they match or not, a client
|
49
|
+
SHOULD use a case-sensitive octet-by-octet comparison of the
|
50
|
+
entire URIs, with these exceptions:
|
51
|
+
- A port that is empty or not given is equivalent to the default
|
52
|
+
port for that URI-reference
|
53
|
+
- Comparisons of host names MUST be case-insensitive
|
54
|
+
- Comparisons of scheme names MUST be case-insensitive
|
55
|
+
- An empty abs_path is equivalent to an abs_path of "/"
|
56
|
+
* 3.3 Date/Time Formats
|
57
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Decision::Conneg do
|
4
|
+
let(:request) { Webmachine::Request.new("GET", URI.parse("http://localhost:8080/"), Webmachine::Headers["accept" => "*/*"], "") }
|
5
|
+
let(:response) { Webmachine::Response.new }
|
6
|
+
let(:resource) do
|
7
|
+
Class.new(Webmachine::Resource) do
|
8
|
+
def to_html; "hello world!"; end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
subject do
|
12
|
+
Webmachine::Decision::FSM.new(resource, request, response)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "choosing a media type" do
|
16
|
+
it "should not choose a type when none are provided" do
|
17
|
+
subject.choose_media_type([], "*/*").should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not choose a type when none are acceptable" do
|
21
|
+
subject.choose_media_type(["text/html"], "application/json").should be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should choose the first acceptable type" do
|
25
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
26
|
+
"application/xml, text/html, */*").should == "application/xml"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should choose the type that matches closest when matching subparams" do
|
30
|
+
subject.choose_media_type(["text/html",
|
31
|
+
["text/html", {"charset" => "iso8859-1"}]],
|
32
|
+
"text/html;charset=iso8859-1, application/xml").
|
33
|
+
should == "text/html;charset=iso8859-1"
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should choose the preferred type over less-preferred types" do
|
38
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
39
|
+
"application/xml;q=0.7, text/html, */*").should == "text/html"
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should raise an exception when a media-type is improperly formatted" do
|
44
|
+
expect {
|
45
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
46
|
+
"bah;")
|
47
|
+
}.to raise_error(Webmachine::MalformedRequest)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "choosing an encoding" do
|
52
|
+
it "should not set the encoding when none are provided" do
|
53
|
+
subject.choose_encoding({}, "identity, gzip")
|
54
|
+
subject.metadata['Content-Encoding'].should be_nil
|
55
|
+
subject.response.headers['Content-Encoding'].should be_nil
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not set the Content-Encoding header when it is identity" do
|
59
|
+
subject.choose_encoding({"gzip"=> :encode_gzip, "identity" => :encode_identity}, "identity")
|
60
|
+
subject.metadata['Content-Encoding'].should == 'identity'
|
61
|
+
response.headers['Content-Encoding'].should be_nil
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should choose the first acceptable encoding" do
|
65
|
+
subject.choose_encoding({"gzip" => :encode_gzip}, "identity, gzip")
|
66
|
+
subject.metadata['Content-Encoding'].should == 'gzip'
|
67
|
+
response.headers['Content-Encoding'].should == 'gzip'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should choose the preferred encoding over less-preferred encodings" do
|
71
|
+
subject.choose_encoding({"gzip" => :encode_gzip, "identity" => :encode_identity}, "gzip, identity;q=0.7")
|
72
|
+
subject.metadata['Content-Encoding'].should == 'gzip'
|
73
|
+
response.headers['Content-Encoding'].should == 'gzip'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not set the encoding if none are acceptable" do
|
77
|
+
subject.choose_encoding({"gzip" => :encode_gzip}, "identity")
|
78
|
+
subject.metadata['Content-Encoding'].should be_nil
|
79
|
+
response.headers['Content-Encoding'].should be_nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "choosing a charset" do
|
84
|
+
it "should not set the charset when none are provided" do
|
85
|
+
subject.choose_charset([], "ISO-8859-1")
|
86
|
+
subject.metadata['Charset'].should be_nil
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should choose the first acceptable charset" do
|
90
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII, UTF-8")
|
91
|
+
subject.metadata['Charset'].should == "US-ASCII"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should choose the preferred charset over less-preferred charsets" do
|
95
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII;q=0.7, UTF-8")
|
96
|
+
subject.metadata['Charset'].should == "UTF-8"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not set the charset if none are acceptable" do
|
100
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "ISO-8859-1")
|
101
|
+
subject.metadata['Charset'].should be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should choose a charset case-insensitively" do
|
105
|
+
subject.choose_charset([["UtF-8", :to_utf8],["US-ASCII", :to_ascii]], "iso-8859-1, utf-8")
|
106
|
+
subject.metadata['Charset'].should == "utf-8"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "choosing a language" do
|
111
|
+
it "should not set the language when none are provided" do
|
112
|
+
subject.choose_language([], "en")
|
113
|
+
subject.metadata['Language'].should be_nil
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should choose the first acceptable language" do
|
117
|
+
subject.choose_language(['en', 'en-US', 'es'], "en-US, es")
|
118
|
+
subject.metadata['Language'].should == "en-US"
|
119
|
+
response.headers['Content-Language'].should == "en-US"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should choose the preferred language over less-preferred languages" do
|
123
|
+
subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es")
|
124
|
+
subject.metadata['Language'].should == "es"
|
125
|
+
response.headers['Content-Language'].should == "es"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should select the first language if all are acceptable" do
|
129
|
+
subject.choose_language(['en', 'fr', 'es'], "*")
|
130
|
+
subject.metadata['Language'].should == "en"
|
131
|
+
response.headers['Content-Language'].should == "en"
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should select the closest acceptable language when an exact match is not available" do
|
135
|
+
subject.choose_language(['en-US', 'es'], "en, fr")
|
136
|
+
subject.metadata['Language'].should == 'en-US'
|
137
|
+
response.headers['Content-Language'].should == 'en-US'
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not set the language if none are acceptable" do
|
141
|
+
subject.choose_language(['en'], 'es')
|
142
|
+
subject.metadata['Language'].should be_nil
|
143
|
+
response.headers.should_not include('Content-Language')
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should choose a language case-insensitively" do
|
147
|
+
subject.choose_language(['en-US', 'ZH'], 'zh-ch, EN')
|
148
|
+
subject.metadata['Language'].should == 'en-US'
|
149
|
+
response.headers['Content-Language'].should == 'en-US'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,1030 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Decision::Flow do
|
4
|
+
subject { Webmachine::Decision::FSM.new(resource, request, response) }
|
5
|
+
let(:method) { 'GET' }
|
6
|
+
let(:uri) { URI.parse("http://localhost/") }
|
7
|
+
let(:headers) { Webmachine::Headers.new }
|
8
|
+
let(:body) { "" }
|
9
|
+
let(:request) { Webmachine::Request.new(method, uri, headers, body) }
|
10
|
+
let(:response) { Webmachine::Response.new }
|
11
|
+
let(:default_resource) { resource_with }
|
12
|
+
let(:missing_resource) { missing_resource_with }
|
13
|
+
|
14
|
+
def resource_with(&block)
|
15
|
+
klass = Class.new(Webmachine::Resource) do
|
16
|
+
def to_html; "test resource"; end
|
17
|
+
end
|
18
|
+
klass.module_eval(&block) if block_given?
|
19
|
+
klass.new(request, response)
|
20
|
+
end
|
21
|
+
|
22
|
+
def missing_resource_with(&block)
|
23
|
+
resource_with do
|
24
|
+
def resource_exists?; false; end
|
25
|
+
self.module_eval(&block) if block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#b13 (Service Available?)" do
|
30
|
+
let(:resource) do
|
31
|
+
resource_with do
|
32
|
+
attr_accessor :available
|
33
|
+
def service_available?; @available; end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should respond with 503 when the service is unavailable" do
|
38
|
+
resource.available = false
|
39
|
+
subject.run
|
40
|
+
response.code.should == 503
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#b12 (Known method?)" do
|
45
|
+
let(:resource) do
|
46
|
+
resource_with do
|
47
|
+
def known_methods; ['HEAD']; end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should respond with 501 when the method is unknown" do
|
52
|
+
subject.run
|
53
|
+
response.code.should == 501
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "#b11 (URI too long?)" do
|
58
|
+
let(:resource) do
|
59
|
+
resource_with do
|
60
|
+
def uri_too_long?(uri); true; end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should respond with 414 when the URI is too long" do
|
65
|
+
subject.run
|
66
|
+
response.code.should == 414
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#b10 (Method allowed?)" do
|
71
|
+
let(:resource) do
|
72
|
+
resource_with do
|
73
|
+
def allowed_methods; ['POST']; end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should respond with 405 when the method is not allowed" do
|
78
|
+
subject.run
|
79
|
+
response.code.should == 405
|
80
|
+
response.headers['Allow'].should == "POST"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "#b9 (Malformed request?)" do
|
85
|
+
let(:resource) { resource_with { def malformed_request?; true; end } }
|
86
|
+
|
87
|
+
it "should respond with 400 when the request is malformed" do
|
88
|
+
subject.run
|
89
|
+
response.code.should == 400
|
90
|
+
end
|
91
|
+
|
92
|
+
context "when the Content-MD5 header is present" do
|
93
|
+
let(:resource) do
|
94
|
+
resource_with do
|
95
|
+
def allowed_methods; ['POST']; end;
|
96
|
+
def process_post; true; end;
|
97
|
+
attr_accessor :validation
|
98
|
+
def validate_content_checksum; @validation; end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
let(:method) { "POST" }
|
103
|
+
let(:body) { "This is the body." }
|
104
|
+
let(:headers) { Webmachine::Headers["Content-Type" => "text/plain"] }
|
105
|
+
|
106
|
+
it "should respond with 400 when the request body does not match the header" do
|
107
|
+
headers['Content-MD5'] = "thiswillnotmatchthehash"
|
108
|
+
subject.run
|
109
|
+
response.code.should == 400
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should respond with 400 when the resource invalidates the checksum" do
|
113
|
+
resource.validation = false
|
114
|
+
headers['Content-MD5'] = "thiswillnotmatchthehash"
|
115
|
+
subject.run
|
116
|
+
response.code.should == 400
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should not respond with 400 when the resource validates the checksum" do
|
120
|
+
resource.validation = true
|
121
|
+
headers['Content-MD5'] = "thiswillnotmatchthehash"
|
122
|
+
subject.run
|
123
|
+
response.code.should_not == 400
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should respond with the given code when the resource returns a code while validating" do
|
127
|
+
resource.validation = 500
|
128
|
+
headers['Content-MD5'] = "thiswillnotmatchthehash"
|
129
|
+
subject.run
|
130
|
+
response.code.should == 500
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "#b8 (Authorized?)" do
|
136
|
+
let(:resource) { resource_with { attr_accessor :auth; def is_authorized?(header); @auth; end } }
|
137
|
+
|
138
|
+
it "should reply with 401 when the client is unauthorized" do
|
139
|
+
resource.auth = false
|
140
|
+
subject.run
|
141
|
+
response.code.should == 401
|
142
|
+
end
|
143
|
+
|
144
|
+
it "should reply with 401 when the resource gives a challenge" do
|
145
|
+
resource.auth = "Basic realm=Webmachine"
|
146
|
+
subject.run
|
147
|
+
response.code.should == 401
|
148
|
+
response.headers['WWW-Authenticate'].should == "Basic realm=Webmachine"
|
149
|
+
end
|
150
|
+
|
151
|
+
it "should halt with the given code when the resource returns a status code" do
|
152
|
+
resource.auth = 400
|
153
|
+
subject.run
|
154
|
+
response.code.should == 400
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should not reply with 401 when the client is authorized" do
|
158
|
+
resource.auth = true
|
159
|
+
subject.run
|
160
|
+
response.code.should_not == 401
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#b7 (Forbidden?)" do
|
165
|
+
let(:resource) { resource_with { attr_accessor :forbid; def forbidden?; @forbid; end } }
|
166
|
+
|
167
|
+
it "should reply with 403 when the request is forbidden" do
|
168
|
+
resource.forbid = true
|
169
|
+
subject.run
|
170
|
+
response.code.should == 403
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should not reply with 403 when the request is permitted" do
|
174
|
+
resource.forbid = false
|
175
|
+
subject.run
|
176
|
+
response.code.should_not == 403
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should halt with the given code when the resource returns a status code" do
|
180
|
+
resource.forbid = 400
|
181
|
+
subject.run
|
182
|
+
response.code.should == 400
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "#b6 (Unsupported Content-* header?)" do
|
187
|
+
let(:resource) do
|
188
|
+
resource_with do
|
189
|
+
def valid_content_headers?(contents)
|
190
|
+
contents['Content-Fail'].nil?
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should reply with 501 when an invalid Content-* header is present" do
|
196
|
+
headers['Content-Fail'] = "yup"
|
197
|
+
subject.run
|
198
|
+
response.code.should == 501
|
199
|
+
end
|
200
|
+
|
201
|
+
it "should not reply with 501 when all Content-* headers are valid" do
|
202
|
+
subject.run
|
203
|
+
response.code.should_not == 501
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
describe "#b5 (Known Content-Type?)" do
|
208
|
+
let(:method) { "POST" }
|
209
|
+
let(:body) { "This is the body." }
|
210
|
+
let(:resource) do
|
211
|
+
resource_with do
|
212
|
+
def known_content_type?(type) type !~ /unknown/; end;
|
213
|
+
def process_post; true; end
|
214
|
+
def allowed_methods; %w{POST}; end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
before { headers['Content-Length'] = body.length.to_s }
|
219
|
+
|
220
|
+
it "should reply with 415 when the Content-Type is unknown" do
|
221
|
+
headers['Content-Type'] = "application/x-unknown-type"
|
222
|
+
subject.run
|
223
|
+
response.code.should == 415
|
224
|
+
end
|
225
|
+
|
226
|
+
it "should not reply with 415 when the Content-Type is known" do
|
227
|
+
headers['Content-Type'] = "text/plain"
|
228
|
+
subject.run
|
229
|
+
response.code.should_not == 415
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
describe "#b4 (Request entity too large?)" do
|
234
|
+
let(:resource) do
|
235
|
+
resource_with do
|
236
|
+
def allowed_methods; %w{POST}; end
|
237
|
+
def process_post; true; end
|
238
|
+
def valid_entity_length?(length); length.to_i < 100; end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
let(:method) { "POST" }
|
242
|
+
before { headers['Content-Type'] = "text/plain"; headers['Content-Length'] = body.size.to_s }
|
243
|
+
|
244
|
+
context "when the request body is too large" do
|
245
|
+
let(:body) { "Big" * 100 }
|
246
|
+
it "should reply with 413" do
|
247
|
+
subject.run
|
248
|
+
response.code.should == 413
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context "when the request body is not too large" do
|
253
|
+
let(:body) { "small" }
|
254
|
+
|
255
|
+
it "should not reply with 413" do
|
256
|
+
subject.run
|
257
|
+
response.code.should_not == 413
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
describe "#b3 (OPTIONS?)" do
|
263
|
+
let(:method){ "OPTIONS" }
|
264
|
+
let(:resource){ resource_with { def allowed_methods; %w[GET HEAD OPTIONS]; end } }
|
265
|
+
it "should reply with 200 when the request method is OPTIONS" do
|
266
|
+
subject.run
|
267
|
+
response.code.should == 200
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe "#c3, #c4 (Acceptable media types)" do
|
272
|
+
let(:resource) { default_resource }
|
273
|
+
context "when the Accept header exists" do
|
274
|
+
it "should reply with 406 when the type is unacceptable" do
|
275
|
+
headers['Accept'] = "text/plain"
|
276
|
+
subject.run
|
277
|
+
response.code.should == 406
|
278
|
+
end
|
279
|
+
|
280
|
+
it "should not reply with 406 when the type is acceptable" do
|
281
|
+
headers['Accept'] = "text/*"
|
282
|
+
subject.run
|
283
|
+
response.code.should_not == 406
|
284
|
+
response.headers['Content-Type'].should == "text/html"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
context "when the Accept header does not exist" do
|
289
|
+
it "should not negotiate a media type" do
|
290
|
+
headers['Accept'].should be_nil
|
291
|
+
subject.should_not_receive(:c4)
|
292
|
+
subject.run
|
293
|
+
response.headers['Content-Type'].should == 'text/html'
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
describe "#d4, #d5 (Acceptable languages)" do
|
299
|
+
let(:resource) { resource_with { def languages_provided; %w{en-US fr}; end } }
|
300
|
+
context "when the Accept-Language header exists" do
|
301
|
+
it "should reply with 406 when the language is unacceptable" do
|
302
|
+
headers['Accept-Language'] = "es, de"
|
303
|
+
subject.run
|
304
|
+
response.code.should == 406
|
305
|
+
end
|
306
|
+
|
307
|
+
it "should not reply with 406 when the language is acceptable" do
|
308
|
+
headers['Accept-Language'] = "en-GB, en;q=0.7"
|
309
|
+
subject.run
|
310
|
+
response.code.should_not == 406
|
311
|
+
response.headers['Content-Language'].should == "en-US"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context "when the Accept-Language header is absent" do
|
316
|
+
it "should not negotiate the language" do
|
317
|
+
headers['Accept-Language'].should be_nil
|
318
|
+
subject.should_not_receive(:d5)
|
319
|
+
subject.run
|
320
|
+
response.headers['Content-Language'].should == 'en-US'
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
describe "#e5, #e6 (Acceptable charsets)" do
|
326
|
+
let(:resource) do
|
327
|
+
resource_with do
|
328
|
+
def charsets_provided
|
329
|
+
[["iso8859-1", :to_iso],["utf-8", :to_utf]];
|
330
|
+
end
|
331
|
+
def to_iso(chunk); chunk; end
|
332
|
+
def to_utf(chunk); chunk; end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
context "when the Accept-Charset header exists" do
|
337
|
+
it "should reply with 406 when the charset is unacceptable" do
|
338
|
+
headers['Accept-Charset'] = "utf-16"
|
339
|
+
subject.run
|
340
|
+
response.code.should == 406
|
341
|
+
end
|
342
|
+
|
343
|
+
it "should not reply with 406 when the charset is acceptable" do
|
344
|
+
headers['Accept-Charset'] = "iso8859-1"
|
345
|
+
subject.run
|
346
|
+
response.code.should_not == 406
|
347
|
+
response.headers['Content-Type'].should == "text/html;charset=iso8859-1"
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
context "when the Accept-Charset header is absent" do
|
352
|
+
it "should not negotiate the language" do
|
353
|
+
headers['Accept-Charset'].should be_nil
|
354
|
+
subject.should_not_receive(:e6)
|
355
|
+
subject.run
|
356
|
+
response.headers['Content-Type'].should == 'text/html;charset=iso8859-1'
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
describe "#f6, #f7 (Acceptable encodings)" do
|
362
|
+
let(:resource) do
|
363
|
+
resource_with do
|
364
|
+
def encodings_provided
|
365
|
+
super.merge("gzip" => :encode_gzip)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
context "when the Accept-Encoding header is present" do
|
371
|
+
it "should reply with 406 if the encoding is unacceptable" do
|
372
|
+
headers['Accept-Encoding'] = 'deflate, identity;q=0.0'
|
373
|
+
subject.run
|
374
|
+
response.code.should == 406
|
375
|
+
end
|
376
|
+
|
377
|
+
it "should not reply with 406 if the encoding is acceptable" do
|
378
|
+
headers['Accept-Encoding'] = 'gzip, deflate'
|
379
|
+
subject.run
|
380
|
+
response.code.should_not == 406
|
381
|
+
response.headers['Content-Encoding'].should == 'gzip'
|
382
|
+
# It should be compressed
|
383
|
+
response.body.should_not == 'test resource'
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
context "when the Accept-Encoding header is not present" do
|
388
|
+
it "should not negotiate an encoding" do
|
389
|
+
headers['Accept-Encoding'].should be_nil
|
390
|
+
subject.should_not_receive(:f7)
|
391
|
+
subject.run
|
392
|
+
response.code.should_not == 406
|
393
|
+
# It should not be compressed
|
394
|
+
response.body.should == 'test resource'
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
describe "#g7 (Resource exists?)" do
|
400
|
+
let(:resource) { resource_with { attr_accessor :exist; def resource_exists?; @exist; end } }
|
401
|
+
|
402
|
+
it "should not enter conditional requests if missing (and eventually reply with 404)" do
|
403
|
+
resource.exist = false
|
404
|
+
subject.should_not_receive(:g8)
|
405
|
+
subject.run
|
406
|
+
response.code.should == 404
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should not reply with 404 if it does exist" do
|
410
|
+
resource.exist = true
|
411
|
+
subject.should_not_receive(:h7)
|
412
|
+
subject.run
|
413
|
+
response.code.should_not == 404
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
# Conditional requests/preconditions
|
418
|
+
describe "#g8, #g9, #g10 (ETag match)" do
|
419
|
+
let(:resource) { resource_with { def generate_etag; "etag"; end } }
|
420
|
+
it "should skip ETag matching when If-Match is missing" do
|
421
|
+
headers['If-Match'].should be_nil
|
422
|
+
subject.should_not_receive(:g9)
|
423
|
+
subject.should_not_receive(:g11)
|
424
|
+
subject.run
|
425
|
+
response.code.should_not == 412
|
426
|
+
end
|
427
|
+
it "should not reply with 304 when If-Match is *" do
|
428
|
+
headers['If-Match'] = "*"
|
429
|
+
subject.run
|
430
|
+
response.code.should_not == 412
|
431
|
+
end
|
432
|
+
it "should reply with 412 if the ETag is not in If-Match" do
|
433
|
+
headers['If-Match'] = '"notetag"'
|
434
|
+
subject.run
|
435
|
+
response.code.should == 412
|
436
|
+
end
|
437
|
+
it "should not reply with 412 if the ETag is in If-Match" do
|
438
|
+
headers['If-Match'] = '"etag"'
|
439
|
+
subject.run
|
440
|
+
response.code.should_not == 412
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
describe "#h10, #h11, #h12 (If-Unmodified-Since match [IUMS])" do
|
445
|
+
let(:resource) { resource_with { attr_accessor :now; def last_modified; @now; end } }
|
446
|
+
before { @now = resource.now = Time.now }
|
447
|
+
|
448
|
+
it "should skip LM matching if IUMS is missing" do
|
449
|
+
headers['If-Unmodified-Since'].should be_nil
|
450
|
+
subject.should_not_receive(:h11)
|
451
|
+
subject.should_not_receive(:h12)
|
452
|
+
subject.run
|
453
|
+
response.code.should_not == 412
|
454
|
+
end
|
455
|
+
|
456
|
+
it "should skip LM matching if IUMS is an invalid date" do
|
457
|
+
headers['If-Unmodified-Since'] = "garbage"
|
458
|
+
subject.should_not_receive(:h12)
|
459
|
+
subject.run
|
460
|
+
response.code.should_not == 412
|
461
|
+
end
|
462
|
+
|
463
|
+
it "should not reply with 412 if LM is <= IUMS" do
|
464
|
+
headers['If-Unmodified-Since'] = (@now + 100).httpdate
|
465
|
+
subject.run
|
466
|
+
response.code.should_not == 412
|
467
|
+
end
|
468
|
+
|
469
|
+
it "should reply with 412 if LM is > IUMS" do
|
470
|
+
headers['If-Unmodified-Since'] = (@now - 100).httpdate
|
471
|
+
subject.run
|
472
|
+
response.code.should == 412
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
describe "#i12, #i13, #k13, #j18 (If-None-Match match)" do
|
477
|
+
let(:resource) do
|
478
|
+
resource_with do
|
479
|
+
def generate_etag; "etag"; end;
|
480
|
+
def process_post; true; end
|
481
|
+
def allowed_methods; %w{GET HEAD POST}; end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
it "should skip ETag matching if If-None-Match is missing" do
|
486
|
+
headers['If-None-Match'].should be_nil
|
487
|
+
%w{i13 k13 j18}.each do |m|
|
488
|
+
subject.should_not_receive(m.to_sym)
|
489
|
+
end
|
490
|
+
subject.run
|
491
|
+
[304, 412].should_not include(response.code)
|
492
|
+
end
|
493
|
+
|
494
|
+
it "should not reply with 412 or 304 if the ETag is not in If-None-Match" do
|
495
|
+
headers['If-None-Match'] = '"notetag"'
|
496
|
+
subject.run
|
497
|
+
[304, 412].should_not include(response.code)
|
498
|
+
end
|
499
|
+
|
500
|
+
context "when the method is GET or HEAD" do
|
501
|
+
let(:method){ %w{GET HEAD}[rand(1)] }
|
502
|
+
it "should reply with 304 when If-None-Match is *" do
|
503
|
+
headers['If-None-Match'] = '*'
|
504
|
+
end
|
505
|
+
it "should reply with 304 when the ETag is in If-None-Match" do
|
506
|
+
headers['If-None-Match'] = '"etag", "foobar"'
|
507
|
+
end
|
508
|
+
after { subject.run; response.code.should == 304 }
|
509
|
+
end
|
510
|
+
|
511
|
+
context "when the method is not GET or HEAD" do
|
512
|
+
let(:method){ "POST" }
|
513
|
+
let(:body) { "This is the body." }
|
514
|
+
let(:headers){ Webmachine::Headers["Content-Type" => "text/plain"] }
|
515
|
+
|
516
|
+
it "should reply with 412 when If-None-Match is *" do
|
517
|
+
headers['If-None-Match'] = '*'
|
518
|
+
end
|
519
|
+
|
520
|
+
it "should reply with 412 when the ETag is in If-None-Match" do
|
521
|
+
headers['If-None-Match'] = '"etag"'
|
522
|
+
end
|
523
|
+
after { subject.run; response.code.should == 412 }
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
describe "#l13, #l14, #l15, #l17 (If-Modified-Since match)" do
|
528
|
+
let(:resource) { resource_with { attr_accessor :now; def last_modified; @now; end } }
|
529
|
+
before { @now = resource.now = Time.now }
|
530
|
+
it "should skip LM matching if IMS is missing" do
|
531
|
+
headers['If-Modified-Since'].should be_nil
|
532
|
+
%w{l14 l15 l17}.each do |m|
|
533
|
+
subject.should_not_receive(m.to_sym)
|
534
|
+
end
|
535
|
+
subject.run
|
536
|
+
response.code.should_not == 304
|
537
|
+
end
|
538
|
+
|
539
|
+
it "should skip LM matching if IMS is an invalid date" do
|
540
|
+
headers['If-Modified-Since'] = "garbage"
|
541
|
+
%w{l15 l17}.each do |m|
|
542
|
+
subject.should_not_receive(m.to_sym)
|
543
|
+
end
|
544
|
+
subject.run
|
545
|
+
response.code.should_not == 304
|
546
|
+
end
|
547
|
+
|
548
|
+
it "should skip LM matching if IMS is later than current time" do
|
549
|
+
headers['If-Modified-Since'] = (@now + 1000).httpdate
|
550
|
+
subject.should_not_receive(:l17)
|
551
|
+
subject.run
|
552
|
+
response.code.should_not == 304
|
553
|
+
end
|
554
|
+
|
555
|
+
it "should reply with 304 if LM is <= IMS" do
|
556
|
+
headers['If-Modified-Since'] = (@now - 1).httpdate
|
557
|
+
resource.now = @now - 1000
|
558
|
+
subject.run
|
559
|
+
response.code.should == 304
|
560
|
+
end
|
561
|
+
|
562
|
+
it "should not reply with 304 if LM is > IMS" do
|
563
|
+
headers['If-Modified-Since'] = (@now - 1000).httpdate
|
564
|
+
subject.run
|
565
|
+
response.code.should_not == 304
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
569
|
+
# Resource missing branch (upper right)
|
570
|
+
describe "#h7 (If-Match: * exists?)" do
|
571
|
+
let(:resource) { missing_resource }
|
572
|
+
it "should reply with 412 when the If-Match header is *" do
|
573
|
+
headers['If-Match'] = '"*"'
|
574
|
+
subject.run
|
575
|
+
response.code.should == 412
|
576
|
+
end
|
577
|
+
|
578
|
+
it "should not reply with 412 when the If-Match header is missing or not *" do
|
579
|
+
headers['If-Match'] = ['"etag"', nil][rand(1)]
|
580
|
+
subject.run
|
581
|
+
response.code.should_not == 412
|
582
|
+
end
|
583
|
+
end
|
584
|
+
|
585
|
+
describe "#i7 (PUT?)" do
|
586
|
+
let(:resource) do
|
587
|
+
missing_resource_with do
|
588
|
+
def allowed_methods; %w{GET HEAD PUT POST}; end
|
589
|
+
def process_post; true; end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
let(:body) { %W{GET HEAD DELETE}.include?(method) ? nil : "This is the body." }
|
593
|
+
before { headers['Content-Type'] = 'text/plain' }
|
594
|
+
|
595
|
+
context "when the method is PUT" do
|
596
|
+
let(:method){ "PUT" }
|
597
|
+
|
598
|
+
it "should not reach state k7" do
|
599
|
+
subject.should_not_receive(:k7)
|
600
|
+
subject.run
|
601
|
+
end
|
602
|
+
|
603
|
+
after { [404, 410, 303].should_not include(response.code) }
|
604
|
+
end
|
605
|
+
|
606
|
+
context "when the method is not PUT" do
|
607
|
+
let(:method){ %W{GET HEAD POST DELETE}[rand(4)] }
|
608
|
+
|
609
|
+
it "should not reach state i4" do
|
610
|
+
subject.should_not_receive(:i4)
|
611
|
+
subject.run
|
612
|
+
end
|
613
|
+
|
614
|
+
after { response.code.should_not == 409 }
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
describe "#i4 (Apply to a different URI?)" do
|
619
|
+
let(:resource) do
|
620
|
+
missing_resource_with do
|
621
|
+
attr_accessor :location
|
622
|
+
def moved_permanently?; @location; end
|
623
|
+
def allowed_methods; %w[PUT]; end
|
624
|
+
end
|
625
|
+
end
|
626
|
+
let(:method){ "PUT" }
|
627
|
+
let(:body){ "This is the body." }
|
628
|
+
let(:headers) { Webmachine::Headers["Content-Type" => "text/plain", "Content-Length" => body.size.to_s] }
|
629
|
+
|
630
|
+
it "should reply with 301 when the resource has moved" do
|
631
|
+
resource.location = URI.parse("http://localhost:8098/newuri")
|
632
|
+
subject.run
|
633
|
+
response.code.should == 301
|
634
|
+
response.headers['Location'].should == resource.location.to_s
|
635
|
+
end
|
636
|
+
|
637
|
+
it "should not reply with 301 when resource has not moved" do
|
638
|
+
resource.location = false
|
639
|
+
subject.run
|
640
|
+
response.code.should_not == 301
|
641
|
+
end
|
642
|
+
end
|
643
|
+
|
644
|
+
describe "Redirection (Resource previously existed)" do
|
645
|
+
let(:resource) do
|
646
|
+
missing_resource_with do
|
647
|
+
attr_writer :moved_perm, :moved_temp, :allow_missing
|
648
|
+
def previously_existed?; true; end
|
649
|
+
def moved_permanently?; @moved_perm; end
|
650
|
+
def moved_temporarily?; @moved_temp; end
|
651
|
+
def allow_missing_post?; @allow_missing; end
|
652
|
+
def allowed_methods; %W{GET POST}; end
|
653
|
+
def process_post; true; end
|
654
|
+
end
|
655
|
+
end
|
656
|
+
let(:method){ @method || "GET" }
|
657
|
+
|
658
|
+
describe "#k5 (Moved permanently?)" do
|
659
|
+
it "should reply with 301 when the resource has moved permanently" do
|
660
|
+
uri = resource.moved_perm = URI.parse("http://www.google.com/")
|
661
|
+
subject.run
|
662
|
+
response.code.should == 301
|
663
|
+
response.headers['Location'].should == uri.to_s
|
664
|
+
end
|
665
|
+
it "should not reply with 301 when the resource has not moved permanently" do
|
666
|
+
resource.moved_perm = false
|
667
|
+
subject.run
|
668
|
+
response.code.should_not == 301
|
669
|
+
end
|
670
|
+
end
|
671
|
+
|
672
|
+
describe "#l5 (Moved temporarily?)" do
|
673
|
+
before { resource.moved_perm = false }
|
674
|
+
it "should reply with 307 when the resource has moved temporarily" do
|
675
|
+
uri = resource.moved_temp = URI.parse("http://www.basho.com/")
|
676
|
+
subject.run
|
677
|
+
response.code.should == 307
|
678
|
+
response.headers['Location'].should == uri.to_s
|
679
|
+
end
|
680
|
+
it "should not reply with 307 when the resource has not moved temporarily" do
|
681
|
+
resource.moved_temp = false
|
682
|
+
subject.run
|
683
|
+
response.code.should_not == 307
|
684
|
+
end
|
685
|
+
end
|
686
|
+
|
687
|
+
describe "#m5 (POST?), #n5 (POST to missing resource?)" do
|
688
|
+
before { resource.moved_perm = resource.moved_temp = false }
|
689
|
+
it "should reply with 410 when the method is not POST" do
|
690
|
+
method.should_not == "POST"
|
691
|
+
subject.run
|
692
|
+
response.code.should == 410
|
693
|
+
end
|
694
|
+
it "should reply with 410 when the resource disallows missing POSTs" do
|
695
|
+
@method = "POST"
|
696
|
+
resource.allow_missing = false
|
697
|
+
subject.run
|
698
|
+
response.code.should == 410
|
699
|
+
end
|
700
|
+
it "should not reply with 410 when the resource allows missing POSTs" do
|
701
|
+
@method = "POST"
|
702
|
+
resource.allow_missing = true
|
703
|
+
subject.run
|
704
|
+
response.code.should == 410
|
705
|
+
end
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
describe "#l7 (POST?), #m7 (POST to missing resource?)" do
|
710
|
+
let(:resource) do
|
711
|
+
missing_resource_with do
|
712
|
+
attr_accessor :allow_missing
|
713
|
+
def allowed_methods; %W{GET POST}; end
|
714
|
+
def previously_existed?; false; end
|
715
|
+
def allow_missing_post?; @allow_missing; end
|
716
|
+
def process_post; true; end
|
717
|
+
end
|
718
|
+
end
|
719
|
+
let(:method){ @method || "GET" }
|
720
|
+
it "should reply with 404 when the method is not POST" do
|
721
|
+
method.should_not == "POST"
|
722
|
+
subject.run
|
723
|
+
response.code.should == 404
|
724
|
+
end
|
725
|
+
it "should reply with 404 when the resource disallows missing POSTs" do
|
726
|
+
@method = "POST"
|
727
|
+
resource.allow_missing = false
|
728
|
+
subject.run
|
729
|
+
response.code.should == 404
|
730
|
+
end
|
731
|
+
it "should not reply with 404 when the resource allows missing POSTs" do
|
732
|
+
@method = "POST"
|
733
|
+
resource.allow_missing = true
|
734
|
+
subject.run
|
735
|
+
response.code.should_not == 404
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
describe "#p3 (Conflict?)" do
|
740
|
+
let(:resource) do
|
741
|
+
missing_resource_with do
|
742
|
+
attr_writer :conflict
|
743
|
+
def allowed_methods; %W{PUT}; end
|
744
|
+
def is_conflict?; @conflict; end
|
745
|
+
end
|
746
|
+
end
|
747
|
+
let(:method){ "PUT" }
|
748
|
+
it "should reply with 409 if the resource is in conflict" do
|
749
|
+
resource.conflict = true
|
750
|
+
subject.run
|
751
|
+
response.code.should == 409
|
752
|
+
end
|
753
|
+
it "should not reply with 409 if the resource is in conflict" do
|
754
|
+
resource.conflict = false
|
755
|
+
subject.run
|
756
|
+
response.code.should_not == 409
|
757
|
+
end
|
758
|
+
end
|
759
|
+
|
760
|
+
# Bottom right
|
761
|
+
describe "#n11 (Redirect?)" do
|
762
|
+
let(:method) { "POST" }
|
763
|
+
let(:resource) do
|
764
|
+
resource_with do
|
765
|
+
attr_writer :new_loc, :exist
|
766
|
+
def allowed_methods; %w{POST}; end
|
767
|
+
def allow_missing_post?; true; end
|
768
|
+
def process_post
|
769
|
+
response.redirect_to(@new_loc) if @new_loc
|
770
|
+
true
|
771
|
+
end
|
772
|
+
end
|
773
|
+
end
|
774
|
+
[true, false].each do |e|
|
775
|
+
context "and the resource #{ e ? "does not exist" : 'exists'}" do
|
776
|
+
before { resource.exist = e }
|
777
|
+
|
778
|
+
it "should reply with 303 if the resource redirected" do
|
779
|
+
resource.new_loc = URI.parse("/foo/bar")
|
780
|
+
subject.run
|
781
|
+
response.code.should == 303
|
782
|
+
response.headers['Location'].should == "/foo/bar"
|
783
|
+
end
|
784
|
+
|
785
|
+
it "should not reply with 303 if the resource did not redirect" do
|
786
|
+
resource.new_loc = nil
|
787
|
+
subject.run
|
788
|
+
response.code.should_not == 303
|
789
|
+
end
|
790
|
+
end
|
791
|
+
end
|
792
|
+
end
|
793
|
+
|
794
|
+
describe "#p11 (New resource?)" do
|
795
|
+
let(:resource) do
|
796
|
+
resource_with do
|
797
|
+
attr_writer :exist, :new_loc, :create
|
798
|
+
|
799
|
+
def allowed_methods; %W{PUT POST}; end
|
800
|
+
def resource_exists?; @exist; end
|
801
|
+
def process_post; true; end
|
802
|
+
def allow_missing_post?; true; end
|
803
|
+
def post_is_create?; @create; end
|
804
|
+
def create_path; @new_loc; end
|
805
|
+
def content_types_accepted; [["text/plain", :accept_text]]; end
|
806
|
+
def accept_text
|
807
|
+
response.headers['Location'] = @new_loc.to_s if @new_loc
|
808
|
+
true
|
809
|
+
end
|
810
|
+
end
|
811
|
+
end
|
812
|
+
let(:body) { "new content" }
|
813
|
+
let(:headers){ Webmachine::Headers['content-type' => 'text/plain'] }
|
814
|
+
|
815
|
+
context "when the method is PUT" do
|
816
|
+
let(:method){ "PUT" }
|
817
|
+
[true, false].each do |e|
|
818
|
+
context "and the resource #{ e ? "does not exist" : 'exists'}" do
|
819
|
+
before { resource.exist = e }
|
820
|
+
|
821
|
+
it "should reply with 201 when the Location header has been set" do
|
822
|
+
resource.exist = e
|
823
|
+
resource.new_loc = "http://ruby-doc.org/"
|
824
|
+
subject.run
|
825
|
+
response.code.should == 201
|
826
|
+
end
|
827
|
+
it "should not reply with 201 when the Location header has been set" do
|
828
|
+
resource.exist = e
|
829
|
+
subject.run
|
830
|
+
response.headers['Location'].should be_nil
|
831
|
+
response.code.should_not == 201
|
832
|
+
end
|
833
|
+
end
|
834
|
+
end
|
835
|
+
end
|
836
|
+
|
837
|
+
context "when the method is POST" do
|
838
|
+
let(:method){ "POST" }
|
839
|
+
[true, false].each do |e|
|
840
|
+
context "and the resource #{ e ? 'exists' : "does not exist"}" do
|
841
|
+
before { resource.exist = e }
|
842
|
+
it "should reply with 201 when post_is_create is true and create_path returns a URI" do
|
843
|
+
resource.new_loc = created = "/foo/bar/baz"
|
844
|
+
resource.create = true
|
845
|
+
subject.run
|
846
|
+
response.code.should == 201
|
847
|
+
response.headers['Location'].should == created
|
848
|
+
end
|
849
|
+
it "should reply with 500 when post_is_create is true and create_path returns nil" do
|
850
|
+
resource.create = true
|
851
|
+
subject.run
|
852
|
+
response.code.should == 500
|
853
|
+
response.error.should_not be_nil
|
854
|
+
end
|
855
|
+
it "should not reply with 201 when post_is_create is false" do
|
856
|
+
resource.create = false
|
857
|
+
subject.run
|
858
|
+
response.code.should_not == 201
|
859
|
+
end
|
860
|
+
end
|
861
|
+
end
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
describe "#o14 (Conflict?)" do
|
866
|
+
let(:resource) do
|
867
|
+
resource_with do
|
868
|
+
attr_writer :conflict
|
869
|
+
def allowed_methods; %W{PUT}; end
|
870
|
+
def is_conflict?; @conflict; end
|
871
|
+
end
|
872
|
+
end
|
873
|
+
let(:method){ "PUT" }
|
874
|
+
it "should reply with 409 if the resource is in conflict" do
|
875
|
+
resource.conflict = true
|
876
|
+
subject.run
|
877
|
+
response.code.should == 409
|
878
|
+
end
|
879
|
+
it "should not reply with 409 if the resource is in conflict" do
|
880
|
+
resource.conflict = false
|
881
|
+
subject.run
|
882
|
+
response.code.should_not == 409
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
describe "#m16 (DELETE?), #m20 (Delete enacted?)" do
|
887
|
+
let(:method){ @method || "DELETE" }
|
888
|
+
let(:resource) do
|
889
|
+
resource_with do
|
890
|
+
attr_writer :deleted, :completed
|
891
|
+
def allowed_methods; %w{GET DELETE}; end
|
892
|
+
def delete_resource; @deleted; end
|
893
|
+
def delete_completed?; @completed; end
|
894
|
+
end
|
895
|
+
end
|
896
|
+
it "should not reply with 202 if the method is not DELETE" do
|
897
|
+
@method = "GET"
|
898
|
+
subject.run
|
899
|
+
response.code.should_not == 202
|
900
|
+
end
|
901
|
+
it "should reply with 500 if the DELETE fails" do
|
902
|
+
resource.deleted = false
|
903
|
+
subject.run
|
904
|
+
response.code.should == 500
|
905
|
+
end
|
906
|
+
it "should reply with 202 if the DELETE succeeds but is not complete" do
|
907
|
+
resource.deleted = true
|
908
|
+
resource.completed = false
|
909
|
+
subject.run
|
910
|
+
response.code.should == 202
|
911
|
+
end
|
912
|
+
it "should not reply with 202 if the DELETE succeeds and completes" do
|
913
|
+
resource.completed = resource.deleted = true
|
914
|
+
subject.run
|
915
|
+
response.code.should_not == 202
|
916
|
+
end
|
917
|
+
end
|
918
|
+
|
919
|
+
# These decisions are covered by dozens of other examples. Leaving
|
920
|
+
# commented for now.
|
921
|
+
# describe "#n16 (POST?)" do it; end
|
922
|
+
# describe "#o16 (PUT?)" do it; end
|
923
|
+
|
924
|
+
describe "#o18 (Multiple representations?)" do
|
925
|
+
let(:resource) do
|
926
|
+
resource_with do
|
927
|
+
attr_writer :exist, :multiple
|
928
|
+
def delete_resource
|
929
|
+
response.body = "Response content."
|
930
|
+
true
|
931
|
+
end
|
932
|
+
def delete_completed?; true; end
|
933
|
+
def allowed_methods; %{GET HEAD PUT POST DELETE}; end
|
934
|
+
def resource_exists?; @exist; end
|
935
|
+
def allow_missing_post?; true; end
|
936
|
+
def content_types_accepted; [[request.content_type, :accept_all]]; end
|
937
|
+
def multiple_choices?; @multiple; end
|
938
|
+
def process_post
|
939
|
+
response.body = "Response content."
|
940
|
+
true
|
941
|
+
end
|
942
|
+
def accept_all
|
943
|
+
response.body = "Response content."
|
944
|
+
true
|
945
|
+
end
|
946
|
+
end
|
947
|
+
end
|
948
|
+
|
949
|
+
[["GET", true],["HEAD", true],["PUT", true],["PUT", false],["POST",true],["POST",false],
|
950
|
+
["DELETE", true]].each do |m, e|
|
951
|
+
context "when the method is #{m} and the resource #{e ? 'exists' : 'does not exist' }" do
|
952
|
+
let(:method){ m }
|
953
|
+
let(:body) { %W{PUT POST}.include?(m) ? "request body" : "" }
|
954
|
+
let(:headers) { %W{PUT POST}.include?(m) ? Webmachine::Headers['content-type' => 'text/plain'] : Webmachine::Headers.new }
|
955
|
+
before { resource.exist = e }
|
956
|
+
it "should reply with 200 if there are not multiple representations" do
|
957
|
+
resource.multiple = false
|
958
|
+
subject.run
|
959
|
+
puts response.error if response.code == 500
|
960
|
+
response.code.should == 200
|
961
|
+
end
|
962
|
+
it "should reply with 300 if there are multiple representations" do
|
963
|
+
resource.multiple = true
|
964
|
+
subject.run
|
965
|
+
puts response.error if response.code == 500
|
966
|
+
response.code.should == 300
|
967
|
+
end
|
968
|
+
end
|
969
|
+
end
|
970
|
+
end
|
971
|
+
|
972
|
+
describe "#o20 (Response has entity?)" do
|
973
|
+
let(:resource) do
|
974
|
+
resource_with do
|
975
|
+
attr_writer :exist, :body
|
976
|
+
def delete_resource; true; end
|
977
|
+
def delete_completed?; true; end
|
978
|
+
def allowed_methods; %{GET PUT POST DELETE}; end
|
979
|
+
def resource_exists?; @exist; end
|
980
|
+
def allow_missing_post?; true; end
|
981
|
+
def content_types_accepted; [[request.content_type, :accept_all]]; end
|
982
|
+
def process_post
|
983
|
+
response.body = @body if @body
|
984
|
+
true
|
985
|
+
end
|
986
|
+
def accept_all
|
987
|
+
response.body = @body if @body
|
988
|
+
true
|
989
|
+
end
|
990
|
+
end
|
991
|
+
end
|
992
|
+
let(:method) { @method || "GET" }
|
993
|
+
let(:headers) { %{PUT POST}.include?(method) ? Webmachine::Headers["content-type" => "text/plain"] : Webmachine::Headers.new }
|
994
|
+
let(:body) { %{PUT POST}.include?(method) ? "This is the body." : nil }
|
995
|
+
context "when a response body is present" do
|
996
|
+
before { resource.body = "Hello, world!" }
|
997
|
+
[
|
998
|
+
["PUT", false],
|
999
|
+
["POST", false],
|
1000
|
+
["DELETE", true],
|
1001
|
+
["POST", true],
|
1002
|
+
["PUT", true]
|
1003
|
+
].each do |m, e|
|
1004
|
+
it "should not reply with 204 (via exists:#{e}, #{m})" do
|
1005
|
+
@method = m
|
1006
|
+
resource.exist = e
|
1007
|
+
subject.run
|
1008
|
+
response.code.should_not == 204
|
1009
|
+
end
|
1010
|
+
end
|
1011
|
+
end
|
1012
|
+
context "when a response body is not present" do
|
1013
|
+
[
|
1014
|
+
["PUT", false],
|
1015
|
+
["POST", false],
|
1016
|
+
["DELETE", true],
|
1017
|
+
["POST", true],
|
1018
|
+
["PUT", true]
|
1019
|
+
].each do |m, e|
|
1020
|
+
it "should reply with 204 (via exists:#{e}, #{m})" do
|
1021
|
+
@method = m
|
1022
|
+
resource.exist = e
|
1023
|
+
subject.run
|
1024
|
+
response.code.should == 204
|
1025
|
+
response.trace.last.should == :o20
|
1026
|
+
end
|
1027
|
+
end
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
end
|