tobacco 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/tobacco.rb ADDED
@@ -0,0 +1,46 @@
1
+ # A convenience module to gain access to
2
+ # configuration options
3
+ #
4
+ require 'open-uri'
5
+
6
+ module Tobacco
7
+
8
+ # Default options in the event no configuration
9
+ # file is created
10
+ #
11
+ @base_path = '/tmp/published_content'
12
+ @published_host = 'http://localhost:3000'
13
+ @content_method = :content
14
+ @content_url_method = :content_url
15
+ @output_filepath_method = :output_filepath
16
+
17
+ class << self
18
+ attr_accessor :base_path,
19
+ :published_host,
20
+ :content_method,
21
+ :content_url_method,
22
+ :output_filepath_method
23
+ end
24
+
25
+ def self.configure
26
+ yield self
27
+ end
28
+
29
+ def self.log(msg)
30
+ msg = "LOGGING: #{msg}"
31
+
32
+ if defined? Rails
33
+ Rails.logger.info(msg)
34
+ else
35
+ puts msg
36
+ end
37
+ end
38
+ end
39
+
40
+ require 'tobacco/burnout.rb'
41
+ require 'tobacco/smoker'
42
+ require 'tobacco/roller'
43
+ require 'tobacco/inhaler'
44
+ require 'tobacco/exhaler'
45
+ require 'tobacco/error'
46
+
@@ -0,0 +1,18 @@
1
+ require 'tobacco'
2
+ require 'pry'
3
+ require 'vcr'
4
+
5
+ RSpec.configure do |config|
6
+ config.treat_symbols_as_metadata_keys_with_true_values = true
7
+ config.run_all_when_everything_filtered = true
8
+ config.filter_run :focus
9
+ config.order = 'random'
10
+
11
+ VCR.configure do |c|
12
+ c.cassette_library_dir = 'spec/support/vcr_cassettes'
13
+ c.hook_into :fakeweb
14
+ c.allow_http_connections_when_no_cassette = true
15
+ end
16
+
17
+ config.extend VCR::RSpec::Macros
18
+ end
@@ -0,0 +1,72 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://www.iana.org/domains/example/
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ accept:
11
+ - ! '*/*'
12
+ user-agent:
13
+ - Ruby
14
+ response:
15
+ status:
16
+ code: 200
17
+ message: OK
18
+ headers:
19
+ date:
20
+ - Sun, 30 Sep 2012 02:45:14 GMT
21
+ server:
22
+ - Apache/2.2.3 (CentOS)
23
+ last-modified:
24
+ - Wed, 09 Feb 2011 17:13:15 GMT
25
+ vary:
26
+ - Accept-Encoding
27
+ connection:
28
+ - close
29
+ transfer-encoding:
30
+ - chunked
31
+ content-type:
32
+ - text/html; charset=UTF-8
33
+ body:
34
+ encoding: US-ASCII
35
+ string: ! "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"
36
+ \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n\t<title>IANA
37
+ &mdash; Example domains</title>\n\t<!-- start common-head -->\n\t<meta http-equiv=\"Content-type\"
38
+ content=\"text/html; charset=utf-8\" />\n\t<link rel=\"stylesheet\" type=\"text/css\"
39
+ href=\"/_css/2008.1/reset-fonts-grids.css\" />\n\t<link rel=\"stylesheet\"
40
+ type=\"text/css\" media=\"screen\" href=\"/_css/2008.1/screen.css\" />\n\t<link
41
+ rel=\"stylesheet\" type=\"text/css\" media=\"print\" href=\"/_css/2008.1/print.css\"
42
+ />\n\t<link rel=\"shortcut icon\" type=\"image/ico\" href=\"/favicon.ico\"
43
+ />\n\t<script type=\"text/javascript\" src=\"/_js/prototype.js\"></script>\n\t<script
44
+ type=\"text/javascript\" src=\"/_js/corners.js\"></script>\n\t<script type=\"text/javascript\"
45
+ src=\"/_js/common.js\"></script>\n\t<!-- end common-head -->\n\n</head>\n<body>\n\t<!--
46
+ start common-bodyhead -->\n\t<div id=\"header-frame\">\n\t<div id=\"header\">\n\t<div
47
+ id=\"header-logo\"><a href=\"/\"><img src=\"/_img/iana-logo-pageheader.png\"
48
+ alt=\"Homepage\"/></a></div>\n\t<div id=\"header-nav\">\n\t<ul>\n\t<li><a
49
+ href=\"/domains/\">Domains</a></li>\n\t<li><a href=\"/numbers/\">Numbers</a></li>\n\t<li><a
50
+ href=\"/protocols/\">Protocols</a></li>\n\t<li><a href=\"/about/\">About IANA</a></li>\n\t</ul>\n\t</div>\n\t</div>\n\t</div>\n\n\t<div
51
+ id=\"body-container\">\n\t<div id=\"body\">\n\t<!-- end common-bodyhead -->\n\n\n\t<h1>Example
52
+ Domains</h1>\n\n\t<p>As described in <a href=\"/go/rfc2606\">RFC 2606</a>,\n\twe
53
+ maintain a number of domains such as EXAMPLE.COM and EXAMPLE.ORG\n\tfor documentation
54
+ purposes. These domains may be used as illustrative\n\texamples in documents
55
+ without prior coordination with us. They are \n\tnot available for registration.</p>\n\n\t
56
+ <!-- start common-bodytail -->\n\t</div>\n\t</div>\n\n\t<div id=\"footer-frame\">\n\t<div
57
+ id=\"footer\">\n\n\n\t<table width=100%>\n\t<tr>\n\t\t<td id=\"iana-footer-first\"><b><a
58
+ href=\"/about/\">About</a></b><br/>\n <a href=\"/about/presentations/\">Presentations</a><br/>\n
59
+ \ <a href=\"/about/performance/\">Performance</a><br/>\n\t\t<a
60
+ href=\"/reports/\">Reports</a><br/>\n </td>\n\n\t\t<td><b><a
61
+ href=\"/domains/\">Domains</a></b><br/>\n\t\t<a href=\"/domains/root/\">Root
62
+ Zone</a><br/>\n\t\t<a href=\"/domains/int/\">.INT</a><br/>\n\t\t<a href=\"/domains/arpa/\">.ARPA</a><br/>\n\t\t<a
63
+ href=\"/domains/idn-tables/\">IDN Repository</a></td>\n\n\t\t<td><b><a href=\"/protocols/\">Protocols</a></b><br/>\n\t\t<br/>\n\t\t<b><a
64
+ href=\"/numbers/\">Number Resources</a></b><br/>\n\t\t<a href=\"/abuse/\">Abuse
65
+ Information</a></td>\n\n\t\t<td id=\"iana-footer-icann\"><img src=\"/_img/icann-logo-micro.png\"><br/>IANA
66
+ is operated by the<br/><a href=\"http://www.icann.org/\">Internet Corporation
67
+ for Assigned Names and Numbers</a></td>\n\t</tr>\n\t</table>\n\n<div id=\"footer-beta-feedback\">\n
68
+ \ <p>Please direct general feedback regarding IANA to <a href=\"mailto:iana@iana.org?subject=General%20website%20feedback\">iana@iana.org</a>.</p>\n
69
+ \ </div>\n\n\t</div>\n\t</div>\n\t<!-- end common-bodytail -->\n\n\n</body>\n</html>\n"
70
+ http_version: '1.1'
71
+ recorded_at: Sun, 30 Sep 2012 02:45:14 GMT
72
+ recorded_with: VCR 2.2.5
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tobacco::Burnout do
4
+ subject { Tobacco::Burnout }
5
+
6
+ let(:call_count) { 0 }
7
+ let(:max_timeouts) { 0 }
8
+
9
+ let(:untimely_block) {
10
+ ->(call_count, max_timeouts) {
11
+ if call_count < max_timeouts
12
+ call_count += 1
13
+
14
+ raise Timeout::Error
15
+ else
16
+ return "an important value"
17
+ end
18
+ }
19
+ }
20
+
21
+ context "when the attempted block times out exactly X times" do
22
+ before do
23
+ max_timeouts = 5
24
+ end
25
+
26
+ it "raises nothing" do
27
+ expect {
28
+ subject.try(5) { untimely_block.call(call_count, max_timeouts) }
29
+ }.to_not raise_error
30
+ end
31
+
32
+ it "returns whatever the block returned" do
33
+ subject.try(5) { untimely_block.call(call_count, max_timeouts) }.should == "an important value"
34
+ end
35
+ end
36
+
37
+ context "when the attempted block times out more than X times" do
38
+ before do
39
+ max_timeouts = 3
40
+ end
41
+
42
+ it "raises a MaximumAttemptsExceeded exception" do
43
+ expect {
44
+ subject.try(2) do
45
+ untimely_block.call(0, 4)
46
+ end
47
+ }.to raise_error(Tobacco::Burnout::MaximumAttemptsExceeded, /more than 2 time/)
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+ require 'tobacco'
3
+
4
+ shared_examples_for "attributes set" do
5
+ it 'msg is set' do
6
+ error.msg.should == msg
7
+ end
8
+
9
+ it 'filepath is set' do
10
+ error.filepath.should == filepath
11
+ end
12
+
13
+ it 'content is set' do
14
+ error.content.should == content
15
+ end
16
+
17
+ it 'object is an error object' do
18
+ error.object.message.should == error_object.message
19
+ end
20
+ end
21
+
22
+ describe Tobacco::Error do
23
+ subject { Tobacco::Error }
24
+
25
+ describe '#new' do
26
+ context 'sets attributes during initialize' do
27
+ let(:msg) { 'Error Writing' }
28
+ let(:filepath) { '/users/somepath/index.html' }
29
+ let(:content) { '<h1>Page Title</h1>'}
30
+ let(:error_object) { RuntimeError.new("Can't do that!") }
31
+
32
+ let(:error) do
33
+ subject.new(
34
+ msg: msg,
35
+ filepath: filepath,
36
+ content: content,
37
+ object: error_object
38
+ )
39
+ end
40
+
41
+ it_behaves_like "attributes set"
42
+ end
43
+
44
+ context 'allows setting attributes directly' do
45
+ let(:msg) { 'Error Writing' }
46
+ let(:filepath) { '/users/no_permissions_here/index.html' }
47
+ let(:content) { '<h1>Overdone</h1>'}
48
+ let(:error_object) { Exception.new("I know this is crazy. Not sure what just happened. Try it again and the error may not happen... maybe") }
49
+ let(:error) { subject.new }
50
+
51
+ before do
52
+ error.msg = msg
53
+ error.filepath = filepath
54
+ error.content = content
55
+ error.object = error_object
56
+ end
57
+
58
+ it_behaves_like "attributes set"
59
+ end
60
+ end
61
+
62
+ describe '#to_a' do
63
+ context 'when destructuring using *splat' do
64
+ let(:msg) { 'Error Writing' }
65
+ let(:filepath) { '/users/somepath/index.html' }
66
+ let(:content) { '<h1>Page Title</h1>'}
67
+ let(:error_object) { RuntimeError.new("Can't do that!") }
68
+
69
+ let(:expected_array) { [ msg, filepath, content, error_object ] }
70
+
71
+ let(:error) do
72
+ subject.new(
73
+ msg: msg,
74
+ filepath: filepath,
75
+ content: content,
76
+ object: error_object
77
+ )
78
+ end
79
+
80
+ it 'sets the array of variables' do
81
+ splatted = *error
82
+ expected_array.should == splatted
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'tobacco'
3
+
4
+ describe Tobacco::Exhaler do
5
+
6
+ subject { Tobacco::Exhaler }
7
+
8
+ let(:exhaler) { subject.new(content, filepath) }
9
+
10
+ describe '#new' do
11
+ context 'when passing arguments' do
12
+ let(:content) { 'File content' }
13
+ let(:filepath) { '/users/someuser/work/index.html' }
14
+
15
+ it 'sets the content' do
16
+ exhaler.filepath.should == filepath
17
+ end
18
+
19
+ it 'sets the filepath' do
20
+ exhaler.content.should == content
21
+ end
22
+ end
23
+
24
+ context 'when setting attributes after creation' do
25
+ let(:exhaler) { Tobacco::Exhaler.new }
26
+
27
+ it 'sets the content' do
28
+ exhaler.content = 'new content'
29
+
30
+ exhaler.content.should == 'new content'
31
+ end
32
+
33
+ it 'sets the filepath' do
34
+ exhaler.filepath = '/var/other'
35
+
36
+ exhaler.filepath.should == '/var/other'
37
+ end
38
+ end
39
+ end
40
+
41
+ describe 'file operations' do
42
+ let(:filepath) { '/tmp/base_dir/index.html' }
43
+ let(:dir) { '/tmp/base_dir/' }
44
+ let(:content) { '<h1>Page Title</h1>' }
45
+
46
+ after { FileUtils.rm_rf(dir) }
47
+
48
+ describe '#create_directory' do
49
+ it 'creates a directory based on a filepath' do
50
+ exhaler.create_directory
51
+
52
+ File.exists?(dir).should be_true
53
+ end
54
+ end
55
+
56
+ describe '#write!' do
57
+ it 'writes content to a file' do
58
+ exhaler.write!
59
+ File.read(filepath).should == content
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'tobacco'
3
+
4
+ describe Tobacco::Inhaler do
5
+
6
+ subject { Tobacco::Inhaler.new(url) }
7
+
8
+ describe '#new' do
9
+ let!(:url) { 'http://google.com/videos' }
10
+
11
+ it 'sets the url' do
12
+ subject.url.should == url
13
+ end
14
+ end
15
+
16
+ describe '#read' do
17
+
18
+ context 'when reading a url succeeds' do
19
+ let!(:url) { 'http://www.iana.org/domains/example/' }
20
+
21
+ before { VCR.insert_cassette('url_content') }
22
+ after { VCR.eject_cassette() }
23
+
24
+ it 'reads content from a url' do
25
+ subject.read
26
+ end
27
+ end
28
+
29
+ context 'when reading a url fails' do
30
+ let!(:url) { 'http://somehost' }
31
+
32
+ it 'errors on reading' do
33
+ expect { subject.read }.to raise_error(Tobacco::Burnout::MaximumAttemptsExceeded)
34
+ end
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+ require 'tobacco'
3
+
4
+ describe Tobacco::Roller do
5
+
6
+ context 'paths' do
7
+ before do
8
+ Tobacco.configure do |config|
9
+ config.published_host = 'http://localhost:3000'
10
+ config.base_path = 'published_content'
11
+ config.content_method = :content
12
+ config.content_url_method = :content_url
13
+ config.output_filepath_method = :output_filepath
14
+ end
15
+ end
16
+
17
+ subject { Tobacco::Roller.new(smoker) }
18
+
19
+ context 'when using default settings' do
20
+ let(:smoker) { double('smoker') }
21
+ let(:expected_output_filepath) { 'published_content/1/video/245' }
22
+
23
+ describe '#output_filepath' do
24
+ it 'builds a path when given array of options' do
25
+ smoker.stub(:output_filepath) { [ '1', 'video', '245' ] }
26
+
27
+ subject.output_filepath.should == expected_output_filepath
28
+ end
29
+
30
+ it 'builds a path when given a string' do
31
+ smoker.stub(:output_filepath) { '1/video/245' }
32
+
33
+ subject.output_filepath.should == expected_output_filepath
34
+ end
35
+ end
36
+
37
+ describe '#content_url' do
38
+ context 'when prepended with a forward slash' do
39
+ let(:expected_content_url) { 'http://localhost:3000/publisher/video/245' }
40
+
41
+ it 'builds a content url' do
42
+ smoker.stub(:content_url) { '/publisher/video/245' }
43
+
44
+ subject.content_url.should == expected_content_url
45
+ end
46
+ end
47
+
48
+ context 'when not prepended with a forward slash' do
49
+ let(:expected_content_url) { 'http://localhost:3000/publisher/video/245' }
50
+
51
+ it 'builds a content url' do
52
+ smoker.stub(:content_url) { 'publisher/video/245' }
53
+
54
+ subject.content_url.should == expected_content_url
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+
61
+ context 'when overriding default settings' do
62
+ let(:smoker) { double('smoker') }
63
+
64
+ before do
65
+ Tobacco.configure do |config|
66
+ config.published_host = 'http://localhost:5000'
67
+ config.base_path = 'pub_content'
68
+ config.content_method = :data
69
+ config.content_url_method = :url
70
+ config.output_filepath_method = :fpath
71
+ end
72
+ end
73
+
74
+ let(:smoker) { double('smoker') }
75
+ let(:expected_output_filepath) { 'pub_content/1/video/245' }
76
+
77
+ describe '#output_filepath' do
78
+ it 'builds an ouput filepath' do
79
+ smoker.stub(:fpath) { [ '1', 'video', '245' ] }
80
+
81
+ subject.output_filepath.should == expected_output_filepath
82
+ end
83
+ end
84
+
85
+ describe '#content_url' do
86
+ context 'when prepended with a forward slash' do
87
+ let(:expected_content_url) { 'http://localhost:5000/publisher/video/245' }
88
+
89
+ it 'builds a content url' do
90
+ smoker.stub(:url) { '/publisher/video/245' }
91
+
92
+ subject.content_url.should == expected_content_url
93
+ end
94
+ end
95
+
96
+ context 'when not prepended with a forward slash' do
97
+ let(:expected_content_url) { 'http://localhost:5000/publisher/video/245' }
98
+
99
+ it 'builds a content url' do
100
+ smoker.stub(:url) { 'publisher/video/245' }
101
+
102
+ subject.content_url.should == expected_content_url
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,204 @@
1
+ require 'spec_helper'
2
+ require 'tobacco'
3
+
4
+
5
+ describe Tobacco::Smoker do
6
+ before do
7
+ Tobacco.configure do |config|
8
+ config.published_host = 'http://localhost:3000'
9
+ config.base_path = '/tmp/published_content'
10
+ config.content_method = :content
11
+ config.content_url_method = :content_url
12
+ config.output_filepath_method = :output_filepath
13
+ end
14
+ end
15
+
16
+ let(:smoker) { mock('smoker') }
17
+
18
+ subject { Tobacco::Smoker.new(smoker) }
19
+
20
+ context '#write!' do
21
+
22
+ context 'when content is empty' do
23
+ let(:inhaler) { mock('inhaler', read: '') }
24
+ let(:filepath) { mock('roller', content_url: '/video', filepath: '/user') }
25
+
26
+ before do
27
+ Tobacco::Inhaler.should_receive(:new).and_return(inhaler)
28
+ subject.file_path_generator = filepath
29
+ end
30
+
31
+ it 'does not attempt a write' do
32
+ Tobacco::Exhaler.should_not_receive(:new)
33
+
34
+ subject.read
35
+ subject.write!
36
+ end
37
+ end
38
+
39
+ context 'content' do
40
+ let(:content) { 'Directly set content' }
41
+ let(:exhaler) { mock('exhaler', write!: true) }
42
+ let(:filepath) { mock('roller', content_url: '/video', filepath: '/user', output_filepath: '/desktop') }
43
+
44
+ before do
45
+ Tobacco::Exhaler.should_receive(:new).and_return(exhaler)
46
+ subject.file_path_generator = filepath
47
+ end
48
+
49
+ context 'when providing content directly' do
50
+
51
+ it 'allows setting content directly' do
52
+ exhaler.should_receive(:write!)
53
+ subject.content = content
54
+
55
+ subject.write!
56
+ end
57
+
58
+ it 'callback :on_success is called' do
59
+ smoker.should_receive(:before_write).with(content).and_return(content)
60
+ smoker.should_receive(:on_success).with(content)
61
+ subject.content = content
62
+
63
+ subject.write!
64
+ end
65
+ end
66
+
67
+ context 'when an error occurs during writing' do
68
+ let(:error) { raise RuntimeError.new('File Permission Error') }
69
+
70
+ before do
71
+ subject.file_path_generator = filepath
72
+ smoker.should_receive(:before_write).with(content).and_return(content)
73
+ exhaler.should_receive(:write!).and_return { error }
74
+ end
75
+
76
+ it 'calls the callback :on_write_error' do
77
+ smoker.should_receive(:on_write_error)
78
+ subject.content = content
79
+
80
+ subject.write!
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#modify_content_before_writing' do
87
+ let(:content) { '<h1>Summer Gear</h1>' }
88
+ let(:modified_content) { '<h1>Winter Gear</h1>' }
89
+
90
+ before do
91
+ smoker.stub(:before_write).and_return(modified_content)
92
+ end
93
+
94
+ it 'allows the smoker to modify content before writing' do
95
+ subject.content = content
96
+
97
+ subject.modify_content_before_writing.should == modified_content
98
+ end
99
+ end
100
+
101
+ describe '#generate_file_paths' do
102
+ it 'sets the file_path_generator' do
103
+ Tobacco::Roller.should_receive(:new).with(smoker)
104
+
105
+ subject.generate_file_paths
106
+ end
107
+ end
108
+
109
+ describe '#choose_reader' do
110
+ context 'when smoker provides the content' do
111
+ before { smoker.stub(:content) }
112
+
113
+ it 'uses the smoker for the content' do
114
+ subject.choose_reader
115
+ subject.reader.should == smoker
116
+ end
117
+ end
118
+
119
+ context 'when smoker does not provide content' do
120
+ let(:inhaler) { mock('inhaler', read: 'reader method') }
121
+ let(:filepath) { mock('roller', content_url: '/video') }
122
+
123
+ before do
124
+ Tobacco::Inhaler.should_receive(:new).and_return(inhaler)
125
+ subject.file_path_generator = filepath
126
+ end
127
+
128
+ it 'uses the smoker for the content' do
129
+ subject.choose_reader
130
+ subject.reader.should == inhaler
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '#read_content' do
136
+ context 'when content is read from the smoker' do
137
+ let(:content) { 'Provided content' }
138
+
139
+ before { smoker.stub(:content).and_return(content) }
140
+
141
+ it 'should have the correct content' do
142
+ subject.choose_reader
143
+ subject.read_content
144
+
145
+ subject.content.should == content
146
+ end
147
+ end
148
+
149
+ context 'when content is read from url' do
150
+ let(:content) { 'Content from reading url' }
151
+ let(:inhaler) { mock('inhaler', read: content) }
152
+ let(:filepath) { mock('roller', content_url: '/video') }
153
+
154
+ before do
155
+ Tobacco::Inhaler.should_receive(:new).and_return(inhaler)
156
+ subject.file_path_generator = filepath
157
+ end
158
+
159
+ it 'should have the correct content' do
160
+ subject.choose_reader
161
+ subject.read_content
162
+
163
+ subject.content.should == content
164
+ end
165
+ end
166
+ end
167
+
168
+ describe '#read' do
169
+ context 'when content is empty' do
170
+ class Writer
171
+ attr_accessor :error
172
+ def on_read_error(error)
173
+ self.error = error
174
+ end
175
+ end
176
+
177
+ describe '#on_read_error' do
178
+ let(:inhaler) { mock('inhaler', read: '') }
179
+ let(:filepath) { mock('roller', content_url: '/video', filepath: '/file/path') }
180
+ let(:smoker) { Writer.new }
181
+
182
+ before do
183
+ Tobacco::Inhaler.should_receive(:new).and_return(inhaler)
184
+ subject.file_path_generator = filepath
185
+ end
186
+
187
+ it 'the callback is called on the smoker' do
188
+ subject.smoker.should_receive(:on_read_error)
189
+
190
+ subject.read
191
+ end
192
+
193
+ it 'has the correct message on the error object' do
194
+ subject.read
195
+
196
+ error = subject.smoker.error
197
+ message = error.object.message
198
+
199
+ message.should match(/No error encountered/)
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end