blobby 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,206 @@
1
+ require "ostruct"
2
+ require "blobby/http_store"
3
+ require "blobby/in_memory_store"
4
+ require "blobby/store_behaviour"
5
+ require "sinatra/base"
6
+ require "sham_rack"
7
+
8
+ describe Blobby::HttpStore do
9
+
10
+ let(:backing_store) do
11
+ Hash.new
12
+ end
13
+
14
+ class FakeStorageService < Sinatra::Base
15
+
16
+ set :show_exceptions, false
17
+
18
+ put %r{/(.+)} do
19
+ halt 413 if request.content_type == "application/x-www-form-urlencoded"
20
+ status 201 unless store.key?(key)
21
+ store[key] = OpenStruct.new(:content_type => request.content_type, :body => request.body.read)
22
+ nil
23
+ end
24
+
25
+ get %r{/(.+)} do
26
+ halt 404 unless store.key?(key)
27
+ halt 200 if request.head?
28
+ content_type "application/octet-stream"
29
+ store[key].body
30
+ end
31
+
32
+ delete %r{/(.+)} do
33
+ halt 404 unless store.key?(key)
34
+ store.delete(key)
35
+ nil
36
+ end
37
+
38
+ def store
39
+ settings.backing_store
40
+ end
41
+
42
+ def key
43
+ params[:captures].first.tap do |key|
44
+ if key =~ /FAIL/ # simulate failure
45
+ fail "hell"
46
+ end
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ let(:fake_storage_service) do
53
+ subclass = Class.new(FakeStorageService)
54
+ subclass.set :backing_store, backing_store
55
+ subclass
56
+ end
57
+
58
+ let(:http_storage_host) { "storeit.com" }
59
+
60
+ before do
61
+ ShamRack.mount(fake_storage_service, http_storage_host)
62
+ end
63
+
64
+ after do
65
+ ShamRack.unmount_all
66
+ end
67
+
68
+ subject do
69
+ described_class.new("http://#{http_storage_host}/object-prefix/")
70
+ end
71
+
72
+ before do
73
+ allow(subject).to receive(:retry_intervals).and_return([0.01, 0.02])
74
+ end
75
+
76
+ it_behaves_like Blobby::Store
77
+
78
+ describe "#write" do
79
+
80
+ let(:content) { "CONTENT" }
81
+
82
+ before do
83
+ subject["foobar"].write(content)
84
+ end
85
+
86
+ it "PUTs stuff in the remote store" do
87
+ expect(backing_store["object-prefix/foobar"].body).to eq(content)
88
+ end
89
+
90
+ it "presents it as binary data" do
91
+ expect(backing_store["object-prefix/foobar"].content_type).to eq("application/octet-stream")
92
+ end
93
+
94
+ end
95
+
96
+ context "when a server error occurs" do
97
+
98
+ let(:key) { "SERVER/FAIL" }
99
+
100
+ describe "#read" do
101
+
102
+ it "raises an exception" do
103
+ expect do
104
+ subject[key].read
105
+ end.to raise_error
106
+ end
107
+
108
+ end
109
+
110
+ describe "#write" do
111
+
112
+ it "raises an exception" do
113
+ expect do
114
+ subject[key].write("something")
115
+ end.to raise_error
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ [EOFError, Errno::ECONNRESET].each do |retryable_exception|
123
+
124
+ context "when a transient #{retryable_exception} occurs" do
125
+
126
+ before do
127
+ allow(Net::HTTP).to receive(:start) do
128
+ allow(Net::HTTP).to receive(:start).and_call_original
129
+ fail retryable_exception, "interruptus connecti"
130
+ end
131
+ end
132
+
133
+ it "retries and recovers" do
134
+ subject["foo"].write("bar")
135
+ expect(subject["foo"].read).to eq("bar")
136
+ end
137
+
138
+ end
139
+
140
+ context "when #{retryable_exception} exceptions keep happening" do
141
+
142
+ before do
143
+ allow(Net::HTTP).to receive(:start) do
144
+ fail retryable_exception, "interruptus connecti"
145
+ end
146
+ end
147
+
148
+ it "raises the final exception" do
149
+ expect do
150
+ subject["anything"].exists?
151
+ end.to raise_error(retryable_exception)
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
158
+ context "when the hostname can't be resolved" do
159
+
160
+ before do
161
+ allow(Net::HTTP).to receive(:start) do
162
+ fail SocketError, "getaddrinfo: nodename nor servname provided, or not known"
163
+ end
164
+ end
165
+
166
+ it { is_expected.not_to be_available }
167
+
168
+ end
169
+
170
+ context "when HTTP server cannot be contacted" do
171
+
172
+ before do
173
+ allow(Net::HTTP).to receive(:start) do
174
+ fail Errno::ECONNREFUSED, "Connection refused - connect(2)"
175
+ end
176
+ end
177
+
178
+ it { is_expected.not_to be_available }
179
+
180
+ end
181
+
182
+ context "when the base_url does not include a trailing slash" do
183
+
184
+ subject do
185
+ described_class.new("http://#{http_storage_host}/prefix")
186
+ end
187
+
188
+ it "appends a trailing slash" do
189
+ expect(subject.base_uri.to_s).to eq("http://#{http_storage_host}/prefix/")
190
+ end
191
+
192
+ end
193
+
194
+ context "when the base_url does include a trailing slash" do
195
+
196
+ subject do
197
+ described_class.new("http://#{http_storage_host}/prefix/")
198
+ end
199
+
200
+ it "does not append another" do
201
+ expect(subject.base_uri.to_s).to eq("http://#{http_storage_host}/prefix/")
202
+ end
203
+
204
+ end
205
+
206
+ end
@@ -0,0 +1,12 @@
1
+ require "blobby/in_memory_store"
2
+ require "blobby/store_behaviour"
3
+
4
+ describe Blobby::InMemoryStore do
5
+
6
+ subject do
7
+ described_class.new
8
+ end
9
+
10
+ it_behaves_like Blobby::Store
11
+
12
+ end
@@ -0,0 +1,102 @@
1
+ require "blobby/filesystem_store"
2
+ require "blobby/key_transforming_store"
3
+ require "blobby/store_behaviour"
4
+ require "tmpdir"
5
+
6
+ describe Blobby::KeyTransformingStore do
7
+
8
+ let(:memory) { Hash.new }
9
+
10
+ subject do
11
+ described_class.new(Blobby::InMemoryStore.new(memory)) { |key| key }
12
+ end
13
+
14
+ it_behaves_like Blobby::Store
15
+
16
+ let(:content) { "CONTENT" }
17
+
18
+ context "with a noop key transforming strategy" do
19
+
20
+ describe "#write" do
21
+
22
+ let(:key) { "NAMESPACED/KEY" }
23
+ let(:transformed_key) { key }
24
+
25
+ before do
26
+ subject[key].write(content)
27
+ end
28
+
29
+ it "uses the transformed key" do
30
+ expect(subject[key]).to exist
31
+ expect(memory.key?(transformed_key)).to be_truthy
32
+ end
33
+
34
+ it "has the correct contents" do
35
+ expect(subject[key].read).to eq(content)
36
+ expect(memory[transformed_key]).to eq(content)
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ context "with a sharding key transformation strategy" do
43
+
44
+ subject do
45
+ described_class.new(Blobby::InMemoryStore.new(memory)) do |key|
46
+ [key[0, 2], key[2, 2], key[4, 2], key].join "/"
47
+ end
48
+ end
49
+
50
+ describe "#write" do
51
+
52
+ let(:key) { "aabbccdd.png" }
53
+ let(:transformed_key) { "aa/bb/cc/aabbccdd.png" }
54
+
55
+ before do
56
+ subject[key].write(content)
57
+ end
58
+
59
+ it "uses the transformed key" do
60
+ expect(subject[key]).to exist
61
+ expect(memory.key?(transformed_key)).to be_truthy
62
+ end
63
+
64
+ it "has the correct contents" do
65
+ expect(subject[key].read).to eq(content)
66
+ expect(memory[transformed_key]).to eq(content)
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ context "with a shard ignoring key transformation strategy" do
73
+
74
+ subject do
75
+ described_class.new(Blobby::InMemoryStore.new(memory)) do |key|
76
+ key.split("/").last
77
+ end
78
+ end
79
+
80
+ describe "#write" do
81
+
82
+ let(:key) { "/aa/bb/cc/aabbccdd.png" }
83
+ let(:transformed_key) { "aabbccdd.png" }
84
+
85
+ before do
86
+ subject[key].write(content)
87
+ end
88
+
89
+ it "uses the transformed key" do
90
+ expect(subject[key]).to exist
91
+ expect(memory.key?(transformed_key)).to be_truthy
92
+ end
93
+
94
+ it "has the correct contents" do
95
+ expect(subject[key].read).to eq(content)
96
+ expect(memory[transformed_key]).to eq(content)
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ end
@@ -0,0 +1,45 @@
1
+ require "logger"
2
+ require "blobby/in_memory_store"
3
+ require "blobby/logging_store"
4
+ require "blobby/store_behaviour"
5
+ require "stringio"
6
+
7
+ describe Blobby::LoggingStore do
8
+
9
+ let(:backing_store) { Blobby::InMemoryStore.new }
10
+ let(:log_buffer) { StringIO.new }
11
+ let(:logger) { Logger.new(log_buffer) }
12
+ let(:log_output) { log_buffer.string }
13
+
14
+ subject do
15
+ described_class.new(backing_store, "THE STORE", logger)
16
+ end
17
+
18
+ it_behaves_like Blobby::Store
19
+
20
+ describe "#write" do
21
+
22
+ before do
23
+ subject["foo"].write("bar")
24
+ end
25
+
26
+ it "logs the write" do
27
+ expect(log_output).to include(%(wrote to "foo" in THE STORE))
28
+ end
29
+
30
+ end
31
+
32
+ describe "#delete" do
33
+
34
+ before do
35
+ subject["foo"].write("bar")
36
+ subject["foo"].delete
37
+ end
38
+
39
+ it "logs the delete" do
40
+ expect(log_output).to include(%(deleted "foo" from THE STORE))
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,174 @@
1
+ # encoding: UTF-8
2
+
3
+ require "rspec"
4
+
5
+ module Blobby
6
+ module Store
7
+ end
8
+ end
9
+
10
+ shared_examples_for Blobby::Store do
11
+
12
+ let(:key) { "KEY" }
13
+ let(:content) { "CONTENT" }
14
+
15
+ it "is available" do
16
+ expect(subject).to be_available
17
+ end
18
+
19
+ context "for a valid key" do
20
+
21
+ let(:stored_object) { subject[key] }
22
+
23
+ context "when nothing has been stored" do
24
+
25
+ describe "#exists?" do
26
+ it "is false" do
27
+ expect(stored_object).not_to exist
28
+ end
29
+ end
30
+
31
+ describe "#read" do
32
+ it "returns nil" do
33
+ expect(stored_object.read).to be_nil
34
+ end
35
+ end
36
+
37
+ describe "#delete" do
38
+ it "returns false" do
39
+ expect(stored_object.delete).to eq(false)
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ context "after content has been written" do
46
+
47
+ before do
48
+ stored_object.write(content)
49
+ end
50
+
51
+ describe "#exists?" do
52
+ it "is true" do
53
+ expect(stored_object).to exist
54
+ end
55
+ end
56
+
57
+ describe "#read" do
58
+
59
+ it "returns the content" do
60
+ expect(stored_object.read).to eq(content)
61
+ end
62
+
63
+ context "with a block" do
64
+
65
+ before do
66
+ @chunks = []
67
+ @rval = stored_object.read do |chunk|
68
+ @chunks << chunk
69
+ end
70
+ end
71
+
72
+ it "yields the content in chunks" do
73
+ expect(@chunks.join).to eq(content)
74
+ end
75
+
76
+ it "returns nil" do
77
+ expect(@rval).to be_nil
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+
84
+ describe "#delete" do
85
+ it "returns true" do
86
+ expect(stored_object.delete).to eq(true)
87
+ end
88
+
89
+ it "removes the object" do
90
+ stored_object.delete
91
+ expect(stored_object).not_to exist
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ if "Strings".respond_to?(:encoding)
98
+
99
+ context "for UTF-8 content" do
100
+
101
+ let(:content) { "SN☃WMAN" }
102
+
103
+ before do
104
+ stored_object.write(content)
105
+ end
106
+
107
+ describe "#read" do
108
+
109
+ it "returns binary data" do
110
+ expect(stored_object.read.encoding.name).to eq("ASCII-8BIT")
111
+ end
112
+
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+
119
+ describe "#write" do
120
+
121
+ it "returns nil" do
122
+ expect(stored_object.write(content)).to be_nil
123
+ end
124
+
125
+ context "with a stream" do
126
+
127
+ before do
128
+ stored_object.write(StringIO.new(content))
129
+ end
130
+
131
+ it "writes the content of the stream" do
132
+ expect(stored_object.read).to eq(content)
133
+ end
134
+
135
+ end
136
+
137
+ end
138
+
139
+ end
140
+
141
+ class << self
142
+
143
+ def it_allows_keys_containing(description, example_key)
144
+ it "allows keys containing #{description}" do
145
+ expect { subject[example_key] }.not_to raise_error
146
+ end
147
+ end
148
+
149
+ def it_disallows_keys_containing(description, example_key)
150
+ it "disallows keys containing #{description}" do
151
+ expect { subject[example_key] }.to raise_error(ArgumentError)
152
+ end
153
+ end
154
+
155
+ end
156
+
157
+ it_disallows_keys_containing "nil", nil
158
+ it_disallows_keys_containing "blank", ""
159
+
160
+ it_allows_keys_containing "slashes", "foo/bar/baz"
161
+ it_allows_keys_containing "selected metacharacters", "@$&*.,;()~"
162
+
163
+ it_disallows_keys_containing "spaces", "foo bar"
164
+ it_disallows_keys_containing "tabs", "foo\tbar"
165
+ it_disallows_keys_containing "newlines", "foo\nbar"
166
+
167
+ it_disallows_keys_containing "a question mark", "foo?"
168
+ it_disallows_keys_containing "a colon", "foo:blah"
169
+
170
+ it_disallows_keys_containing "a leading slash", "/foo"
171
+ it_disallows_keys_containing "a trailing slash", "foo/"
172
+ it_disallows_keys_containing "double slashes", "foo//bar"
173
+
174
+ end