blobby 1.0.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.
@@ -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