thingfish 0.5.0.pre20160707192835
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.simplecov +7 -0
- data/History.rdoc +5 -0
- data/LICENSE +29 -0
- data/Manifest.txt +39 -0
- data/Procfile +4 -0
- data/README.rdoc +92 -0
- data/Rakefile +92 -0
- data/bin/tfprocessord +6 -0
- data/bin/thingfish +10 -0
- data/etc/thingfish.conf.example +26 -0
- data/lib/strelka/app/metadata.rb +38 -0
- data/lib/strelka/httprequest/metadata.rb +70 -0
- data/lib/thingfish.rb +43 -0
- data/lib/thingfish/behaviors.rb +263 -0
- data/lib/thingfish/datastore.rb +55 -0
- data/lib/thingfish/datastore/memory.rb +93 -0
- data/lib/thingfish/handler.rb +728 -0
- data/lib/thingfish/metastore.rb +55 -0
- data/lib/thingfish/metastore/memory.rb +201 -0
- data/lib/thingfish/mixins.rb +57 -0
- data/lib/thingfish/processor.rb +79 -0
- data/lib/thingfish/processor/mp3.rb +167 -0
- data/lib/thingfish/processordaemon.rb +16 -0
- data/lib/thingfish/spechelpers.rb +165 -0
- data/spec/data/APIC-1-image.mp3 +0 -0
- data/spec/data/APIC-2-images.mp3 +0 -0
- data/spec/data/PIC-1-image.mp3 +0 -0
- data/spec/data/PIC-2-images.mp3 +0 -0
- data/spec/helpers.rb +67 -0
- data/spec/spec.opts +4 -0
- data/spec/thingfish/datastore/memory_spec.rb +19 -0
- data/spec/thingfish/datastore_spec.rb +64 -0
- data/spec/thingfish/handler_spec.rb +838 -0
- data/spec/thingfish/metastore/memory_spec.rb +17 -0
- data/spec/thingfish/metastore_spec.rb +96 -0
- data/spec/thingfish/mixins_spec.rb +63 -0
- data/spec/thingfish/processor/mp3_spec.rb +50 -0
- data/spec/thingfish/processor_spec.rb +65 -0
- data/spec/thingfish_spec.rb +23 -0
- metadata +244 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
|
4
|
+
require 'zmq'
|
5
|
+
require 'configurability'
|
6
|
+
require 'loggability'
|
7
|
+
|
8
|
+
require 'thingfish' unless defined?( Thingfish )
|
9
|
+
|
10
|
+
|
11
|
+
# Currently just a placeholder for what will eventually be the runner for
|
12
|
+
# async processors.
|
13
|
+
class Thingfish::ProcessorDaemon
|
14
|
+
end # class Thingfish::ProcessorDaemon
|
15
|
+
|
16
|
+
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
#encoding: utf-8
|
3
|
+
# vim: set nosta noet ts=4 sw=4 ft=ruby:
|
4
|
+
|
5
|
+
require 'time'
|
6
|
+
require 'thingfish'
|
7
|
+
require 'rspec'
|
8
|
+
|
9
|
+
|
10
|
+
### RSpec helper functions.
|
11
|
+
module Thingfish::SpecHelpers
|
12
|
+
|
13
|
+
module Constants
|
14
|
+
TEST_APPID = 'thingfish-test'
|
15
|
+
TEST_SEND_SPEC = 'tcp://127.0.0.1:9999'
|
16
|
+
TEST_RECV_SPEC = 'tcp://127.0.0.1:9998'
|
17
|
+
|
18
|
+
UUID_PATTERN = /[[:xdigit:]]{8}(?:-[[:xdigit:]]{4}){3}-[[:xdigit:]]{12}/i
|
19
|
+
|
20
|
+
TEST_UUID = 'E5DFEEAB-3525-4F14-B4DB-2772D0B9987F'
|
21
|
+
|
22
|
+
TEST_TEXT_DATA = "Pork sausage. Pork! Sausage!".b
|
23
|
+
TEST_TEXT_DATA_IO = StringIO.new( TEST_TEXT_DATA )
|
24
|
+
TEST_PNG_DATA = ("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMA" +
|
25
|
+
"AQAABQABDQottAAAAABJRU5ErkJggg==").unpack('m').first
|
26
|
+
TEST_PNG_DATA_IO = StringIO.new( TEST_PNG_DATA )
|
27
|
+
|
28
|
+
TEST_METADATA = [
|
29
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
30
|
+
"extent" => 1072,
|
31
|
+
"uploadaddress" => "127.0.0.1",
|
32
|
+
"format" => "application/rtf",
|
33
|
+
"created" => Time.parse('2010-10-14 00:08:21 UTC'),
|
34
|
+
"title" => "How to use the Public folder.rtf"},
|
35
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
36
|
+
"extent" => 832604,
|
37
|
+
"uploadaddress" => "127.0.0.1",
|
38
|
+
"format" => "image/jpeg",
|
39
|
+
"created" => Time.parse('2011-09-06 20:10:54 UTC'),
|
40
|
+
"title" => "IMG_0316.JPG"},
|
41
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
42
|
+
"extent" => 2253642,
|
43
|
+
"uploadaddress" => "127.0.0.1",
|
44
|
+
"format" => "image/jpeg",
|
45
|
+
"created" => Time.parse('2011-09-06 20:10:49 UTC'),
|
46
|
+
"title" => "IMG_0544.JPG"},
|
47
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
48
|
+
"extent" => 694785,
|
49
|
+
"uploadaddress" => "127.0.0.1",
|
50
|
+
"format" => "image/jpeg",
|
51
|
+
"created" => Time.parse('2011-09-06 20:10:52 UTC'),
|
52
|
+
"title" => "IMG_0552.JPG"},
|
53
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
54
|
+
"extent" => 1579773,
|
55
|
+
"uploadaddress" => "127.0.0.1",
|
56
|
+
"format" => "image/jpeg",
|
57
|
+
"created" => Time.parse('2011-09-06 20:10:56 UTC'),
|
58
|
+
"title" => "IMG_0748.JPG"},
|
59
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
60
|
+
"extent" => 6464493,
|
61
|
+
"uploadaddress" => "127.0.0.1",
|
62
|
+
"format" => "image/jpeg",
|
63
|
+
"created" => Time.parse('2011-10-14 05:05:23 UTC'),
|
64
|
+
"title" => "IMG_1700.JPG"},
|
65
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
66
|
+
"extent" => 388727,
|
67
|
+
"uploadaddress" => "127.0.0.1",
|
68
|
+
"format" => "image/jpeg",
|
69
|
+
"created" => Time.parse('2011-12-28 01:23:27 UTC'),
|
70
|
+
"title" => "IMG_3553.jpg"},
|
71
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
72
|
+
"extent" => 1354,
|
73
|
+
"uploadaddress" => "127.0.0.1",
|
74
|
+
"format" => "text/plain",
|
75
|
+
"created" => Time.parse('2013-09-09 15:43:31 UTC'),
|
76
|
+
"title" => "agilemanifesto.txt"},
|
77
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
78
|
+
"extent" => 3059035,
|
79
|
+
"uploadaddress" => "127.0.0.1",
|
80
|
+
"format" => "image/jpeg",
|
81
|
+
"created" => Time.parse('2013-04-18 00:25:56 UTC'),
|
82
|
+
"title" => "bacon.jpg"},
|
83
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
84
|
+
"extent" => 71860,
|
85
|
+
"uploadaddress" => "127.0.0.1",
|
86
|
+
"format" => "image/jpeg",
|
87
|
+
"created" => Time.parse('2011-09-06 20:10:57 UTC'),
|
88
|
+
"title" => "boom.jpg"},
|
89
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
90
|
+
"extent" => 2115410,
|
91
|
+
"uploadaddress" => "127.0.0.1",
|
92
|
+
"format" => "audio/mp3",
|
93
|
+
"created" => Time.parse('2013-09-09 15:42:49 UTC'),
|
94
|
+
"title" => "craigslist_erotica.mp3"},
|
95
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
96
|
+
"extent" => 377445,
|
97
|
+
"uploadaddress" => "127.0.0.1",
|
98
|
+
"format" => "image/jpeg",
|
99
|
+
"created" => Time.parse('2012-02-09 17:06:44 UTC'),
|
100
|
+
"title" => "cubes.jpg"},
|
101
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
102
|
+
"extent" => 240960,
|
103
|
+
"uploadaddress" => "127.0.0.1",
|
104
|
+
"format" => "audio/mp3",
|
105
|
+
"created" => Time.parse('2013-09-09 15:42:58 UTC'),
|
106
|
+
"title" => "gay_clowns.mp3"},
|
107
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
108
|
+
"extent" => 561792,
|
109
|
+
"uploadaddress" => "127.0.0.1",
|
110
|
+
"format" => "image/jpeg",
|
111
|
+
"created" => Time.parse('2011-09-06 20:10:57 UTC'),
|
112
|
+
"title" => "aaaaaaaa"},
|
113
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
114
|
+
"extent" => 1104950,
|
115
|
+
"uploadaddress" => "127.0.0.1",
|
116
|
+
"format" => "image/jpeg",
|
117
|
+
"created" => Time.parse('2013-09-09 15:37:25 UTC'),
|
118
|
+
"title" => "joss.jpg"},
|
119
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
120
|
+
"extent" => 163,
|
121
|
+
"uploadaddress" => "127.0.0.1",
|
122
|
+
"format" => "text/plain",
|
123
|
+
"created" => Time.parse('2013-01-23 07:52:44 UTC'),
|
124
|
+
"title" => "macbook.txt"},
|
125
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
126
|
+
"extent" => 2130567,
|
127
|
+
"uploadaddress" => "127.0.0.1",
|
128
|
+
"format" => "image/png",
|
129
|
+
"created" => Time.parse('2012-03-15 05:15:07 UTC'),
|
130
|
+
"title" => "marbles.png"},
|
131
|
+
{"useragent" => "ChunkersTheClown v2.0",
|
132
|
+
"extent" => 8971,
|
133
|
+
"uploadaddress" => "127.0.0.1",
|
134
|
+
"format" => "image/gif",
|
135
|
+
"created" => Time.parse('2013-01-15 19:15:35 UTC'),
|
136
|
+
"title" => "trusttom.GIF"}
|
137
|
+
].freeze
|
138
|
+
TEST_METADATA.each {|hash| hash.freeze }
|
139
|
+
|
140
|
+
|
141
|
+
end # module Constants
|
142
|
+
|
143
|
+
include Constants
|
144
|
+
|
145
|
+
|
146
|
+
# Load fixture data from the ThingFish spec data directory
|
147
|
+
FIXTURE_DIR = Pathname( __FILE__ ).dirname.parent.parent + 'spec/data'
|
148
|
+
|
149
|
+
|
150
|
+
RSpec::Matchers.define :be_a_uuid do |expected|
|
151
|
+
match do |actual|
|
152
|
+
actual =~ UUID_PATTERN
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
### Load and return the data from the fixture with the specified +filename+.
|
158
|
+
def fixture_data( filename )
|
159
|
+
fixture = FIXTURE_DIR + filename
|
160
|
+
return fixture.open( 'r', encoding: 'binary' )
|
161
|
+
end
|
162
|
+
|
163
|
+
end # Thingfish::SpecHelpers
|
164
|
+
|
165
|
+
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# coding: utf-8
|
3
|
+
|
4
|
+
BEGIN {
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
basedir = Pathname.new( __FILE__ ).dirname.parent
|
8
|
+
strelkadir = basedir.parent + 'Strelka'
|
9
|
+
strelkalibdir = strelkadir + 'lib'
|
10
|
+
mongrel2dir = basedir.parent + 'Mongrel2'
|
11
|
+
mongrel2libdir = mongrel2dir + 'lib'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift( strelkalibdir.to_s ) unless $LOAD_PATH.include?( strelkalibdir.to_s )
|
14
|
+
$LOAD_PATH.unshift( mongrel2libdir.to_s ) unless $LOAD_PATH.include?( mongrel2libdir.to_s )
|
15
|
+
}
|
16
|
+
|
17
|
+
# SimpleCov test coverage reporting; enable this using the :coverage rake task
|
18
|
+
require 'simplecov' if ENV['COVERAGE']
|
19
|
+
|
20
|
+
require 'stringio'
|
21
|
+
require 'time'
|
22
|
+
|
23
|
+
|
24
|
+
require 'loggability'
|
25
|
+
require 'loggability/spechelpers'
|
26
|
+
require 'configurability'
|
27
|
+
require 'configurability/behavior'
|
28
|
+
|
29
|
+
require 'rspec'
|
30
|
+
require 'mongrel2'
|
31
|
+
require 'mongrel2/testing'
|
32
|
+
|
33
|
+
require 'strelka'
|
34
|
+
require 'strelka/testing'
|
35
|
+
require 'strelka/authprovider'
|
36
|
+
|
37
|
+
require 'thingfish'
|
38
|
+
require 'thingfish/spechelpers'
|
39
|
+
|
40
|
+
|
41
|
+
Loggability.format_with( :color ) if $stdout.tty?
|
42
|
+
|
43
|
+
|
44
|
+
### Mock with RSpec
|
45
|
+
RSpec.configure do |c|
|
46
|
+
include Strelka::Constants
|
47
|
+
include Thingfish::SpecHelpers
|
48
|
+
include Thingfish::SpecHelpers::Constants
|
49
|
+
|
50
|
+
c.run_all_when_everything_filtered = true
|
51
|
+
c.filter_run :focus
|
52
|
+
c.order = 'random'
|
53
|
+
c.mock_with( :rspec ) do |mock|
|
54
|
+
mock.syntax = :expect
|
55
|
+
end
|
56
|
+
|
57
|
+
c.include( Loggability::SpecHelpers )
|
58
|
+
c.include( Mongrel2::SpecHelpers )
|
59
|
+
c.include( Mongrel2::Constants )
|
60
|
+
c.include( Mongrel2::Config::DSL )
|
61
|
+
c.include( Strelka::Constants )
|
62
|
+
c.include( Strelka::Testing )
|
63
|
+
c.include( Thingfish::SpecHelpers )
|
64
|
+
end
|
65
|
+
|
66
|
+
# vim: set nosta noet ts=4 sw=4:
|
67
|
+
|
data/spec/spec.opts
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../../helpers'
|
4
|
+
|
5
|
+
require 'rspec'
|
6
|
+
require 'thingfish/datastore'
|
7
|
+
require 'thingfish/behaviors'
|
8
|
+
|
9
|
+
|
10
|
+
describe Thingfish::Datastore, "memory" do
|
11
|
+
|
12
|
+
let( :store ) { Thingfish::Datastore.create(:memory) }
|
13
|
+
|
14
|
+
|
15
|
+
it_behaves_like "a Thingfish datastore"
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
# vim: set nosta noet ts=4 sw=4 ft=rspec:
|
@@ -0,0 +1,64 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../helpers'
|
4
|
+
|
5
|
+
require 'rspec'
|
6
|
+
require 'thingfish/datastore'
|
7
|
+
|
8
|
+
class TestingDatastore < Thingfish::Datastore
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
describe Thingfish::Datastore do
|
13
|
+
|
14
|
+
it "is abstract" do
|
15
|
+
expect { described_class.new }.to raise_error( NoMethodError, /private/i )
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
it "acts as a factory for its concrete derivatives" do
|
20
|
+
expect( described_class.create('testing') ).to be_a( TestingDatastore )
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
describe "an instance of a concrete derivative" do
|
25
|
+
|
26
|
+
let( :store ) { described_class.create('testing') }
|
27
|
+
|
28
|
+
it "raises an error if it doesn't implement #save" do
|
29
|
+
expect { store.save(TEST_PNG_DATA) }.to raise_error( NotImplementedError, /save/ )
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises an error if it doesn't implement #replace" do
|
33
|
+
expect { store.replace(TEST_UUID) }.to raise_error( NotImplementedError, /replace/ )
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises an error if it doesn't implement #fetch" do
|
37
|
+
expect { store.fetch(TEST_UUID) }.to raise_error( NotImplementedError, /fetch/ )
|
38
|
+
end
|
39
|
+
|
40
|
+
it "raises an error if it doesn't implement #each" do
|
41
|
+
expect { store.each }.to raise_error( NotImplementedError, /each/ )
|
42
|
+
end
|
43
|
+
|
44
|
+
it "raises an error if it doesn't implement #include?" do
|
45
|
+
expect { store.include?(TEST_UUID) }.to raise_error( NotImplementedError, /include\?/ )
|
46
|
+
end
|
47
|
+
|
48
|
+
it "raises an error if it doesn't implement #each_oid" do
|
49
|
+
expect { store.each_oid }.to raise_error( NotImplementedError, /each_oid/ )
|
50
|
+
end
|
51
|
+
|
52
|
+
it "raises an error if it doesn't implement #remove" do
|
53
|
+
expect { store.remove(TEST_UUID) }.to raise_error( NotImplementedError, /remove/ )
|
54
|
+
end
|
55
|
+
|
56
|
+
it "provides a transactional block method" do
|
57
|
+
expect {|block| store.transaction(&block) }.to yield_with_no_args
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
# vim: set nosta noet ts=4 sw=4 ft=rspec:
|
@@ -0,0 +1,838 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../helpers'
|
4
|
+
|
5
|
+
require 'rspec'
|
6
|
+
require 'thingfish/handler'
|
7
|
+
require 'thingfish/processor'
|
8
|
+
|
9
|
+
|
10
|
+
describe Thingfish::Handler do
|
11
|
+
|
12
|
+
EVENT_SOCKET_URI = 'tcp://127.0.0.1:0'
|
13
|
+
|
14
|
+
before( :all ) do
|
15
|
+
Thingfish::Handler.configure( :event_socket_uri => EVENT_SOCKET_URI )
|
16
|
+
Thingfish::Handler.install_plugins
|
17
|
+
end
|
18
|
+
|
19
|
+
before( :each ) do
|
20
|
+
@png_io = StringIO.new( TEST_PNG_DATA.dup )
|
21
|
+
@text_io = StringIO.new( TEST_TEXT_DATA.dup )
|
22
|
+
@handler = described_class.new( TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC )
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
after( :each ) do
|
27
|
+
@handler.shutdown
|
28
|
+
end
|
29
|
+
|
30
|
+
# let( :handler ) { described_class.new(TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC) }
|
31
|
+
|
32
|
+
|
33
|
+
#
|
34
|
+
# Shared behaviors
|
35
|
+
#
|
36
|
+
|
37
|
+
it_should_behave_like "an object with Configurability"
|
38
|
+
|
39
|
+
|
40
|
+
#
|
41
|
+
# Examples
|
42
|
+
#
|
43
|
+
|
44
|
+
context "misc api" do
|
45
|
+
|
46
|
+
let( :factory ) do
|
47
|
+
Mongrel2::RequestFactory.new(
|
48
|
+
:route => '/',
|
49
|
+
:headers => {:accept => '*/*'})
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'returns interesting configuration info' do
|
53
|
+
req = factory.get( '/serverinfo', content_type: 'text/plain' )
|
54
|
+
res = @handler.handle( req )
|
55
|
+
|
56
|
+
expect( res.status_line ).to match( /200 ok/i )
|
57
|
+
expect( res.headers ).to include( 'x-thingfish' )
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
context "datastore api" do
|
64
|
+
|
65
|
+
let( :factory ) do
|
66
|
+
Mongrel2::RequestFactory.new(
|
67
|
+
:route => '/',
|
68
|
+
:headers => {:accept => '*/*'})
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
it 'accepts a POSTed upload' do
|
73
|
+
req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
|
74
|
+
res = @handler.handle( req )
|
75
|
+
|
76
|
+
expect( res.status_line ).to match( /201 created/i )
|
77
|
+
expect( res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
it "accepts an upload POSTED via Mongrel's async API'" do
|
82
|
+
# Need the config to look up the async upload path relative to the server's chroot
|
83
|
+
Mongrel2::Config.db = Mongrel2::Config.in_memory_db
|
84
|
+
Mongrel2::Config.init_database
|
85
|
+
server( 'thingfish' ) do
|
86
|
+
chroot ''
|
87
|
+
host 'localhost' do
|
88
|
+
route '/', handler( 'tcp://127.0.0.1:9900', 'thingfish' )
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
spool_path = '/var/spool/uploadfile.672'
|
93
|
+
upload_size = 645_000
|
94
|
+
fh = instance_double( "File", size: upload_size, rewind: 0, pos: 0, :pos= => nil,
|
95
|
+
read: TEST_TEXT_DATA )
|
96
|
+
expect( FileTest ).to receive( :exist? ).with( spool_path ).and_return( true )
|
97
|
+
expect( File ).to receive( :open ).
|
98
|
+
with( spool_path, 'r', encoding: Encoding::ASCII_8BIT ).
|
99
|
+
and_return( fh )
|
100
|
+
|
101
|
+
start_req = factory.post( '/', nil,
|
102
|
+
x_mongrel2_upload_start: spool_path,
|
103
|
+
content_length: upload_size,
|
104
|
+
content_type: 'text/plain' )
|
105
|
+
upload_req = factory.post( '/', nil,
|
106
|
+
x_mongrel2_upload_start: spool_path,
|
107
|
+
x_mongrel2_upload_done: spool_path,
|
108
|
+
content_length: upload_size,
|
109
|
+
content_type: 'text/plain' )
|
110
|
+
|
111
|
+
start_res = @handler.dispatch_request( start_req )
|
112
|
+
upload_res = @handler.dispatch_request( upload_req )
|
113
|
+
|
114
|
+
expect( start_res ).to be_nil
|
115
|
+
|
116
|
+
expect( upload_res.status_line ).to match( /201 created/i )
|
117
|
+
expect( upload_res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
it "accepts resources added to a POSTed resource by processors" do
|
122
|
+
imageio = StringIO.new( TEST_PNG_DATA )
|
123
|
+
|
124
|
+
subclass = Class.new( described_class )
|
125
|
+
subclass.filter( :request ) do |req|
|
126
|
+
req.add_related_resource( imageio,
|
127
|
+
relationship: 'thumbnail',
|
128
|
+
format: 'image/png',
|
129
|
+
extent: TEST_PNG_DATA.bytesize )
|
130
|
+
end
|
131
|
+
subclass.metastore = 'memory'
|
132
|
+
subclass.datastore = 'memory'
|
133
|
+
handler = subclass.new( TEST_APPID, TEST_SEND_SPEC, TEST_RECV_SPEC )
|
134
|
+
|
135
|
+
req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
|
136
|
+
res = handler.handle( req )
|
137
|
+
|
138
|
+
metastore = handler.metastore
|
139
|
+
oid = res.headers.x_thingfish_uuid
|
140
|
+
related_oid = metastore.fetch_related_oids( oid ).first
|
141
|
+
|
142
|
+
expect(
|
143
|
+
metastore.fetch_value(related_oid, 'uploadaddress')
|
144
|
+
).to eq( metastore.fetch_value(oid, 'uploadaddress') )
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
it "allows additional metadata to be attached to uploads via X-Thingfish-* headers" do
|
149
|
+
headers = {
|
150
|
+
content_type: 'text/plain',
|
151
|
+
x_thingfish_title: 'Muffin the Panda Goes To School',
|
152
|
+
x_thingfish_tags: 'rapper,ukraine,potap',
|
153
|
+
}
|
154
|
+
req = factory.post( '/', TEST_TEXT_DATA, headers )
|
155
|
+
res = @handler.handle( req )
|
156
|
+
|
157
|
+
expect( res.status_line ).to match( /201 created/i )
|
158
|
+
expect( res.headers.location.to_s ).to match( %r:/#{UUID_PATTERN}$: )
|
159
|
+
|
160
|
+
uuid = res.headers.x_thingfish_uuid
|
161
|
+
expect( @handler.metastore.fetch_value(uuid, 'title') ).
|
162
|
+
to eq( 'Muffin the Panda Goes To School' )
|
163
|
+
expect( @handler.metastore.fetch_value(uuid, 'tags') ).to eq( 'rapper,ukraine,potap' )
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
it 'replaces content via PUT' do
|
168
|
+
uuid = @handler.datastore.save( @text_io )
|
169
|
+
@handler.metastore.save( uuid, {'format' => 'text/plain'} )
|
170
|
+
|
171
|
+
req = factory.put( "/#{uuid}", @png_io, content_type: 'image/png' )
|
172
|
+
res = @handler.handle( req )
|
173
|
+
|
174
|
+
expect( res.status ).to eq( HTTP::NO_CONTENT )
|
175
|
+
expect( @handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
|
176
|
+
expect( @handler.metastore.fetch(uuid) ).to include( 'format' => 'image/png' )
|
177
|
+
end
|
178
|
+
|
179
|
+
|
180
|
+
it "doesn't care about the case of the UUID when replacing content via PUT" do
|
181
|
+
uuid = @handler.datastore.save( @text_io )
|
182
|
+
@handler.metastore.save( uuid, {'format' => 'text/plain'} )
|
183
|
+
|
184
|
+
req = factory.put( "/#{uuid.upcase}", @png_io, content_type: 'image/png' )
|
185
|
+
res = @handler.handle( req )
|
186
|
+
|
187
|
+
expect( res.status ).to eq( HTTP::NO_CONTENT )
|
188
|
+
expect( @handler.datastore.fetch(uuid).read ).to eq( TEST_PNG_DATA )
|
189
|
+
expect( @handler.metastore.fetch(uuid) ).to include( 'format' => 'image/png' )
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
it 'can fetch all uploaded data' do
|
194
|
+
text_uuid = @handler.datastore.save( @text_io )
|
195
|
+
@handler.metastore.save( text_uuid, {
|
196
|
+
'format' => 'text/plain',
|
197
|
+
'extent' => @text_io.string.bytesize
|
198
|
+
})
|
199
|
+
png_uuid = @handler.datastore.save( @png_io )
|
200
|
+
@handler.metastore.save( png_uuid, {
|
201
|
+
'format' => 'image/png',
|
202
|
+
'extent' => @png_io.string.bytesize
|
203
|
+
})
|
204
|
+
|
205
|
+
req = factory.get( '/' )
|
206
|
+
res = @handler.handle( req )
|
207
|
+
content = Yajl::Parser.parse( res.body.read )
|
208
|
+
|
209
|
+
expect( res.status_line ).to match( /200 ok/i )
|
210
|
+
expect( res.headers.content_type ).to eq( 'application/json' )
|
211
|
+
expect( content ).to be_a( Array )
|
212
|
+
expect( content[0] ).to be_a( Hash )
|
213
|
+
expect( content[0]['uri'] ).to eq( "#{req.base_uri}#{text_uuid}" )
|
214
|
+
expect( content[0]['format'] ).to eq( "text/plain" )
|
215
|
+
expect( content[0]['extent'] ).to eq( @text_io.string.bytesize )
|
216
|
+
expect( content[1] ).to be_a( Hash )
|
217
|
+
expect( content[1]['uri'] ).to eq( "#{req.base_uri}#{png_uuid}" )
|
218
|
+
expect( content[1]['format'] ).to eq( 'image/png' )
|
219
|
+
expect( content[1]['extent'] ).to eq( @png_io.string.bytesize )
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
it 'can fetch all related data for a single resource' do
|
224
|
+
main_uuid = @handler.datastore.save( @png_io )
|
225
|
+
@handler.metastore.save( main_uuid, {
|
226
|
+
'format' => 'image/png',
|
227
|
+
'extent' => @png_io.string.bytesize
|
228
|
+
})
|
229
|
+
related_uuid = @handler.datastore.save( @png_io )
|
230
|
+
@handler.metastore.save( related_uuid, {
|
231
|
+
'format' => 'image/png',
|
232
|
+
'extent' => @png_io.string.bytesize,
|
233
|
+
'relation' => main_uuid,
|
234
|
+
'relationship' => "twinsies"
|
235
|
+
})
|
236
|
+
|
237
|
+
req = factory.get( "/#{main_uuid}/related" )
|
238
|
+
res = @handler.handle( req )
|
239
|
+
content = Yajl::Parser.parse( res.body.read )
|
240
|
+
|
241
|
+
expect( res.status_line ).to match( /200 ok/i )
|
242
|
+
expect( res.headers.content_type ).to eq( 'application/json' )
|
243
|
+
expect( content ).to be_a( Array )
|
244
|
+
expect( content[0] ).to be_a( Hash )
|
245
|
+
expect( content[0]['uri'] ).to eq( "#{req.base_uri}#{related_uuid}" )
|
246
|
+
expect( content[0]['format'] ).to eq( "image/png" )
|
247
|
+
expect( content[0]['extent'] ).to eq( @png_io.string.bytesize )
|
248
|
+
expect( content[0]['uuid'] ).to eq( related_uuid )
|
249
|
+
expect( content[0]['relation'] ).to eq( main_uuid )
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
it 'can fetch a related resource by name' do
|
254
|
+
main_uuid = @handler.datastore.save( @png_io )
|
255
|
+
@handler.metastore.save( main_uuid, {
|
256
|
+
'format' => 'image/png',
|
257
|
+
'extent' => @png_io.string.bytesize
|
258
|
+
})
|
259
|
+
related_uuid = @handler.datastore.save( @png_io )
|
260
|
+
@handler.metastore.save( related_uuid, {
|
261
|
+
'format' => 'image/png',
|
262
|
+
'extent' => @png_io.string.bytesize,
|
263
|
+
'relation' => main_uuid,
|
264
|
+
'relationship' => "twinsies"
|
265
|
+
})
|
266
|
+
|
267
|
+
req = factory.get( "/#{main_uuid}/related/twinsies" )
|
268
|
+
res = @handler.handle( req )
|
269
|
+
|
270
|
+
expect( res.status_line ).to match( /200 ok/i )
|
271
|
+
expect( res.headers.content_type ).to eq( 'image/png' )
|
272
|
+
expect( res.body.read ).to eq( TEST_PNG_DATA )
|
273
|
+
end
|
274
|
+
|
275
|
+
|
276
|
+
it "404s when attempting to fetch a resource related to a non-existant resource" do
|
277
|
+
req = factory.get( "/#{TEST_UUID}/related/twinsies" )
|
278
|
+
res = @handler.handle( req )
|
279
|
+
|
280
|
+
expect( res.status_line ).to match( /404 not found/i )
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
it "404s when attempting to fetch a related resource that doesn't exist" do
|
285
|
+
uuid = @handler.datastore.save( @png_io )
|
286
|
+
@handler.metastore.save( uuid, {
|
287
|
+
'format' => 'image/png',
|
288
|
+
'extent' => @png_io.string.bytesize
|
289
|
+
})
|
290
|
+
|
291
|
+
req = factory.get( "/#{uuid}/related/twinsies" )
|
292
|
+
res = @handler.handle( req )
|
293
|
+
|
294
|
+
expect( res.status_line ).to match( /404 not found/i )
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
it "can fetch an uploaded chunk of data" do
|
299
|
+
uuid = @handler.datastore.save( @png_io )
|
300
|
+
@handler.metastore.save( uuid, {'format' => 'image/png'} )
|
301
|
+
|
302
|
+
req = factory.get( "/#{uuid}" )
|
303
|
+
result = @handler.handle( req )
|
304
|
+
|
305
|
+
expect( result.status_line ).to match( /200 ok/i )
|
306
|
+
expect( result.body.read ).to eq( @png_io.string )
|
307
|
+
expect( result.headers.content_type ).to eq( 'image/png' )
|
308
|
+
end
|
309
|
+
|
310
|
+
|
311
|
+
it "returns a 404 Not Found when asked to fetch an object that doesn't exist" do
|
312
|
+
req = factory.get( "/#{TEST_UUID}" )
|
313
|
+
result = @handler.handle( req )
|
314
|
+
|
315
|
+
expect( result.status_line ).to match( /404 not found/i )
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
it "returns a 404 Not Found when asked to fetch an object that doesn't exist in the metastore" do
|
320
|
+
uuid = @handler.datastore.save( @png_io )
|
321
|
+
|
322
|
+
req = factory.get( "/#{uuid}" )
|
323
|
+
result = @handler.handle( req )
|
324
|
+
|
325
|
+
expect( result.status_line ).to match( /404 not found/i )
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
it "doesn't care about the case of the UUID when fetching uploaded data" do
|
330
|
+
uuid = @handler.datastore.save( @png_io )
|
331
|
+
@handler.metastore.save( uuid, {'format' => 'image/png'} )
|
332
|
+
|
333
|
+
req = factory.get( "/#{uuid.upcase}" )
|
334
|
+
result = @handler.handle( req )
|
335
|
+
|
336
|
+
expect( result.status_line ).to match( /200 ok/i )
|
337
|
+
expect( result.body.read ).to eq( @png_io.string )
|
338
|
+
expect( result.headers.content_type ).to eq( 'image/png' )
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
it "can remove everything associated with an object id" do
|
343
|
+
uuid = @handler.datastore.save( @png_io )
|
344
|
+
@handler.metastore.save( uuid, {
|
345
|
+
'format' => 'image/png',
|
346
|
+
'extent' => 288,
|
347
|
+
})
|
348
|
+
|
349
|
+
req = factory.delete( "/#{uuid}" )
|
350
|
+
result = @handler.handle( req )
|
351
|
+
|
352
|
+
expect( result.status_line ).to match( /200 ok/i )
|
353
|
+
expect( @handler.metastore.include?(uuid) ).to be_falsey
|
354
|
+
expect( @handler.datastore.include?(uuid) ).to be_falsey
|
355
|
+
end
|
356
|
+
|
357
|
+
|
358
|
+
it "returns a 404 Not Found when asked to remove an object that doesn't exist" do
|
359
|
+
req = factory.delete( "/#{TEST_UUID}" )
|
360
|
+
result = @handler.handle( req )
|
361
|
+
|
362
|
+
expect( result.status_line ).to match( /404 not found/i )
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
end
|
367
|
+
|
368
|
+
|
369
|
+
context "metastore api" do
|
370
|
+
|
371
|
+
let( :factory ) do
|
372
|
+
Mongrel2::RequestFactory.new(
|
373
|
+
:route => '/',
|
374
|
+
:headers => {:accept => 'application/json'})
|
375
|
+
end
|
376
|
+
|
377
|
+
it "can fetch the metadata associated with uploaded data" do
|
378
|
+
uuid = @handler.datastore.save( @png_io )
|
379
|
+
@handler.metastore.save( uuid, {
|
380
|
+
'format' => 'image/png',
|
381
|
+
'extent' => 288,
|
382
|
+
'created' => Time.at(1378313840),
|
383
|
+
})
|
384
|
+
|
385
|
+
req = factory.get( "/#{uuid}/metadata" )
|
386
|
+
result = @handler.handle( req )
|
387
|
+
content = result.body.read
|
388
|
+
|
389
|
+
content_hash = Yajl::Parser.parse( content )
|
390
|
+
|
391
|
+
expect( result.status ).to eq( 200 )
|
392
|
+
expect( result.headers.content_type ).to eq( 'application/json' )
|
393
|
+
expect( content_hash ).to be_a( Hash )
|
394
|
+
expect( content_hash['extent'] ).to eq( 288 )
|
395
|
+
expect( content_hash['created'] ).to eq( Time.at(1378313840).to_s )
|
396
|
+
end
|
397
|
+
|
398
|
+
|
399
|
+
it "returns a 404 Not Found when fetching metadata for an object that doesn't exist" do
|
400
|
+
req = factory.get( "/#{TEST_UUID}/metadata" )
|
401
|
+
result = @handler.handle( req )
|
402
|
+
|
403
|
+
expect( result.status_line ).to match( /404 not found/i )
|
404
|
+
end
|
405
|
+
|
406
|
+
|
407
|
+
it "can fetch a value for a single metadata key" do
|
408
|
+
uuid = @handler.datastore.save( @png_io )
|
409
|
+
@handler.metastore.save( uuid, {
|
410
|
+
'format' => 'image/png',
|
411
|
+
'extent' => 288,
|
412
|
+
})
|
413
|
+
|
414
|
+
req = factory.get( "/#{uuid}/metadata/extent" )
|
415
|
+
result = @handler.handle( req )
|
416
|
+
result.body.rewind
|
417
|
+
content = result.body.read
|
418
|
+
|
419
|
+
expect( result.status ).to eq( 200 )
|
420
|
+
expect( result.headers.content_type ).to eq( 'application/json' )
|
421
|
+
expect( content ).to eq( "288" )
|
422
|
+
end
|
423
|
+
|
424
|
+
|
425
|
+
it "returns a 404 Not Found when fetching a single metadata value for a uuid that doesn't exist" do
|
426
|
+
req = factory.get( "/#{TEST_UUID}/metadata/extent" )
|
427
|
+
result = @handler.handle( req )
|
428
|
+
|
429
|
+
expect( result.status_line ).to match( /404 not found/i )
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
it "doesn't error when fetching a non-existent metadata value" do
|
434
|
+
uuid = @handler.datastore.save( @png_io )
|
435
|
+
@handler.metastore.save( uuid, {
|
436
|
+
'format' => 'image/png',
|
437
|
+
'extent' => 288,
|
438
|
+
})
|
439
|
+
|
440
|
+
req = factory.get( "/#{uuid}/metadata/hururrgghh" )
|
441
|
+
result = @handler.handle( req )
|
442
|
+
|
443
|
+
content = Yajl::Parser.parse( result.body.read )
|
444
|
+
|
445
|
+
expect( result.status ).to eq( 200 )
|
446
|
+
expect( result.headers.content_type ).to eq( 'application/json' )
|
447
|
+
|
448
|
+
expect( content ).to be_nil
|
449
|
+
end
|
450
|
+
|
451
|
+
|
452
|
+
it "can merge in new metadata for an existing resource with a POST" do
|
453
|
+
uuid = @handler.datastore.save( @png_io )
|
454
|
+
@handler.metastore.save( uuid, {
|
455
|
+
'format' => 'image/png',
|
456
|
+
'extent' => 288,
|
457
|
+
})
|
458
|
+
|
459
|
+
body_json = Yajl.dump({ 'comment' => 'Ignore me!' })
|
460
|
+
req = factory.post( "/#{uuid}/metadata", body_json, 'Content-type' => 'application/json' )
|
461
|
+
result = @handler.handle( req )
|
462
|
+
|
463
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
464
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Ignore me!' )
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
it "returns FORBIDDEN when attempting to merge metadata with operational keys" do
|
469
|
+
uuid = @handler.datastore.save( @png_io )
|
470
|
+
@handler.metastore.save( uuid, {
|
471
|
+
'format' => 'image/png',
|
472
|
+
'extent' => 288,
|
473
|
+
})
|
474
|
+
|
475
|
+
body_json = Yajl.dump({ 'format' => 'text/plain', 'comment' => 'Ignore me!' })
|
476
|
+
req = factory.post( "/#{uuid}/metadata", body_json, 'Content-type' => 'application/json' )
|
477
|
+
result = @handler.handle( req )
|
478
|
+
|
479
|
+
expect( result.status ).to eq( HTTP::FORBIDDEN )
|
480
|
+
expect( result.body.string ).to match( /unable to alter protected metadata/i )
|
481
|
+
expect( result.body.string ).to match( /format/i )
|
482
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
|
483
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
484
|
+
end
|
485
|
+
|
486
|
+
|
487
|
+
it "can create single metadata values with a POST" do
|
488
|
+
uuid = @handler.datastore.save( @png_io )
|
489
|
+
@handler.metastore.save( uuid, {
|
490
|
+
'format' => 'image/png',
|
491
|
+
'extent' => 288,
|
492
|
+
})
|
493
|
+
|
494
|
+
req = factory.post( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
|
495
|
+
result = @handler.handle( req )
|
496
|
+
|
497
|
+
expect( result.status ).to eq( HTTP::CREATED )
|
498
|
+
expect( result.headers.location ).to match( %r|#{uuid}/metadata/comment$| )
|
499
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
it "returns NOT_FOUND when attempting to create metadata for a non-existent object" do
|
504
|
+
req = factory.post( "/#{TEST_UUID}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
|
505
|
+
result = @handler.handle( req )
|
506
|
+
|
507
|
+
expect( result.status ).to eq( HTTP::NOT_FOUND )
|
508
|
+
expect( result.body.string ).to match( /no such object/i )
|
509
|
+
end
|
510
|
+
|
511
|
+
|
512
|
+
it "returns CONFLICT when attempting to create a single metadata value if it already exists" do
|
513
|
+
uuid = @handler.datastore.save( @png_io )
|
514
|
+
@handler.metastore.save( uuid, {
|
515
|
+
'format' => 'image/png',
|
516
|
+
'extent' => 288,
|
517
|
+
'comment' => 'nill bill'
|
518
|
+
})
|
519
|
+
|
520
|
+
req = factory.post( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
|
521
|
+
result = @handler.handle( req )
|
522
|
+
|
523
|
+
expect( result.status ).to eq( HTTP::CONFLICT )
|
524
|
+
expect( result.body.string ).to match( /already exists/i )
|
525
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'nill bill' )
|
526
|
+
end
|
527
|
+
|
528
|
+
|
529
|
+
it "can create single metadata values with a PUT" do
|
530
|
+
uuid = @handler.datastore.save( @png_io )
|
531
|
+
@handler.metastore.save( uuid, {
|
532
|
+
'format' => 'image/png',
|
533
|
+
'extent' => 288,
|
534
|
+
})
|
535
|
+
|
536
|
+
req = factory.put( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
|
537
|
+
result = @handler.handle( req )
|
538
|
+
|
539
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
540
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
|
541
|
+
end
|
542
|
+
|
543
|
+
|
544
|
+
it "can replace a single metadata value with a PUT" do
|
545
|
+
uuid = @handler.datastore.save( @png_io )
|
546
|
+
@handler.metastore.save( uuid, {
|
547
|
+
'format' => 'image/png',
|
548
|
+
'extent' => 288,
|
549
|
+
'comment' => 'nill bill'
|
550
|
+
})
|
551
|
+
|
552
|
+
req = factory.put( "/#{uuid}/metadata/comment", "urrrg", 'Content-type' => 'text/plain' )
|
553
|
+
result = @handler.handle( req )
|
554
|
+
|
555
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
556
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'urrrg' )
|
557
|
+
end
|
558
|
+
|
559
|
+
|
560
|
+
it "returns FORBIDDEN when attempting to replace a operational metadata value with a PUT" do
|
561
|
+
uuid = @handler.datastore.save( @png_io )
|
562
|
+
@handler.metastore.save( uuid, {
|
563
|
+
'format' => 'image/png',
|
564
|
+
'extent' => 288,
|
565
|
+
'comment' => 'nill bill'
|
566
|
+
})
|
567
|
+
|
568
|
+
req = factory.put( "/#{uuid}/metadata/format", "image/gif", 'Content-type' => 'text/plain' )
|
569
|
+
result = @handler.handle( req )
|
570
|
+
|
571
|
+
expect( result.status ).to eq( HTTP::FORBIDDEN )
|
572
|
+
expect( result.body.string ).to match( /protected metadata/i )
|
573
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
it "can replace all metadata with a PUT" do
|
578
|
+
uuid = @handler.datastore.save( @png_io )
|
579
|
+
@handler.metastore.save( uuid, {
|
580
|
+
'format' => 'image/png',
|
581
|
+
'extent' => 288,
|
582
|
+
'comment' => 'nill bill',
|
583
|
+
'ephemeral' => 'butterflies',
|
584
|
+
})
|
585
|
+
|
586
|
+
req = factory.put( "/#{uuid}/metadata", %[{"comment":"Yeah."}],
|
587
|
+
'Content-type' => 'application/json' )
|
588
|
+
result = @handler.handle( req )
|
589
|
+
|
590
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
591
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Yeah.' )
|
592
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
593
|
+
expect( @handler.metastore ).to_not include( 'ephemeral' )
|
594
|
+
end
|
595
|
+
|
596
|
+
|
597
|
+
it "can remove all non-default metadata with a DELETE" do
|
598
|
+
timestamp = Time.now.getgm
|
599
|
+
uuid = @handler.datastore.save( @png_io )
|
600
|
+
@handler.metastore.save( uuid, {
|
601
|
+
'format' => 'image/png',
|
602
|
+
'extent' => 288,
|
603
|
+
'comment' => 'nill bill',
|
604
|
+
'useragent' => 'Inky/2.0',
|
605
|
+
'uploadaddress' => '127.0.0.1',
|
606
|
+
'created' => timestamp,
|
607
|
+
})
|
608
|
+
|
609
|
+
req = factory.delete( "/#{uuid}/metadata" )
|
610
|
+
result = @handler.handle( req )
|
611
|
+
|
612
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
613
|
+
expect( result.body.string ).to be_empty
|
614
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
615
|
+
expect( @handler.metastore.fetch_value(uuid, 'extent') ).to eq( 288 )
|
616
|
+
expect( @handler.metastore.fetch_value(uuid, 'uploadaddress') ).to eq( '127.0.0.1' )
|
617
|
+
expect( @handler.metastore.fetch_value(uuid, 'created') ).to eq( timestamp )
|
618
|
+
|
619
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
|
620
|
+
expect( @handler.metastore.fetch_value(uuid, 'useragent') ).to be_nil
|
621
|
+
end
|
622
|
+
|
623
|
+
|
624
|
+
it "can remove a single metadata value with DELETE" do
|
625
|
+
uuid = @handler.datastore.save( @png_io )
|
626
|
+
@handler.metastore.save( uuid, {
|
627
|
+
'format' => 'image/png',
|
628
|
+
'comment' => 'nill bill'
|
629
|
+
})
|
630
|
+
|
631
|
+
req = factory.delete( "/#{uuid}/metadata/comment" )
|
632
|
+
result = @handler.handle( req )
|
633
|
+
|
634
|
+
expect( result.status ).to eq( HTTP::NO_CONTENT )
|
635
|
+
expect( result.body.string ).to be_empty
|
636
|
+
expect( @handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
|
637
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
638
|
+
end
|
639
|
+
|
640
|
+
|
641
|
+
it "returns FORBIDDEN when attempting to remove a operational metadata value with a DELETE" do
|
642
|
+
uuid = @handler.datastore.save( @png_io )
|
643
|
+
@handler.metastore.save( uuid, {
|
644
|
+
'format' => 'image/png'
|
645
|
+
})
|
646
|
+
|
647
|
+
req = factory.delete( "/#{uuid}/metadata/format" )
|
648
|
+
result = @handler.handle( req )
|
649
|
+
|
650
|
+
expect( result.status ).to eq( HTTP::FORBIDDEN )
|
651
|
+
expect( result.body.string ).to match( /protected metadata/i )
|
652
|
+
expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
|
653
|
+
end
|
654
|
+
end
|
655
|
+
|
656
|
+
|
657
|
+
context "processors" do
|
658
|
+
|
659
|
+
before( :all ) do
|
660
|
+
@original_filters = described_class.filters.dup
|
661
|
+
described_class.filters.replace({ :request => [], :response => [], :both => [] })
|
662
|
+
end
|
663
|
+
|
664
|
+
after( :all ) do
|
665
|
+
described_class.filters.replace( @original_filters )
|
666
|
+
end
|
667
|
+
|
668
|
+
before( :each ) do
|
669
|
+
described_class.processors.clear
|
670
|
+
described_class.filters.values.each( &:clear )
|
671
|
+
end
|
672
|
+
|
673
|
+
|
674
|
+
let( :factory ) do
|
675
|
+
Mongrel2::RequestFactory.new(
|
676
|
+
:route => '/',
|
677
|
+
:headers => {:accept => '*/*'})
|
678
|
+
end
|
679
|
+
|
680
|
+
let!( :test_processor ) do
|
681
|
+
klass = Class.new( Thingfish::Processor ) do
|
682
|
+
extend Loggability
|
683
|
+
log_to :thingfish
|
684
|
+
|
685
|
+
handled_types 'text/plain'
|
686
|
+
|
687
|
+
def initialize( * )
|
688
|
+
super
|
689
|
+
@was_called = false
|
690
|
+
end
|
691
|
+
attr_reader :was_called
|
692
|
+
|
693
|
+
def self::name; 'Thingfish::Processor::Test'; end
|
694
|
+
def on_request( request )
|
695
|
+
@was_called = true
|
696
|
+
self.log.debug "Adding a comment to a request."
|
697
|
+
request.add_metadata( 'test:comment' => "Yo, it totally worked." )
|
698
|
+
|
699
|
+
io = StringIO.new( "Chunkers!" )
|
700
|
+
io.rewind
|
701
|
+
related_metadata = { 'format' => 'text/plain', 'relationship' => 'comment' }
|
702
|
+
request.add_related_resource( io, related_metadata )
|
703
|
+
end
|
704
|
+
def on_response( response )
|
705
|
+
@was_called = true
|
706
|
+
content = response.body.read
|
707
|
+
response.body.rewind
|
708
|
+
response.body.print( content.reverse )
|
709
|
+
response.body.rewind
|
710
|
+
end
|
711
|
+
end
|
712
|
+
# Re-call inherited so it associates the processor plugin with its name
|
713
|
+
Thingfish::Processor.inherited( klass )
|
714
|
+
klass
|
715
|
+
end
|
716
|
+
|
717
|
+
|
718
|
+
it "loads configured processors when it is instantiated" do
|
719
|
+
logger = Loggability[ described_class ]
|
720
|
+
logger.debug( "*** %p" % described_class.filters )
|
721
|
+
logger.debug( "*** %p" % @original_filters )
|
722
|
+
|
723
|
+
described_class.configure( :processors => %w[test] )
|
724
|
+
|
725
|
+
expect( described_class.processors ).to be_an( Array )
|
726
|
+
|
727
|
+
processor = described_class.processors.first
|
728
|
+
expect( processor ).to be_an_instance_of( test_processor )
|
729
|
+
end
|
730
|
+
|
731
|
+
|
732
|
+
it "processes requests" do
|
733
|
+
described_class.configure( :processors => %w[test] )
|
734
|
+
|
735
|
+
req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
|
736
|
+
res = @handler.handle( req )
|
737
|
+
uuid = res.headers.x_thingfish_uuid
|
738
|
+
|
739
|
+
Thingfish.logger.debug "Metastore contains: %p" % [ @handler.metastore.storage ]
|
740
|
+
|
741
|
+
expect( @handler.metastore.fetch(uuid) ).
|
742
|
+
to include( 'test:comment' => 'Yo, it totally worked.')
|
743
|
+
related_uuids = @handler.metastore.fetch_related_oids( uuid )
|
744
|
+
expect( related_uuids.size ).to eq( 1 )
|
745
|
+
|
746
|
+
r_uuid = related_uuids.first.downcase
|
747
|
+
expect( @handler.metastore.fetch_value(r_uuid, 'relation') ).to eq( uuid )
|
748
|
+
expect( @handler.metastore.fetch_value(r_uuid, 'format') ).to eq( 'text/plain' )
|
749
|
+
expect( @handler.metastore.fetch_value(r_uuid, 'extent') ).to eq( 9 )
|
750
|
+
expect( @handler.metastore.fetch_value(r_uuid, 'relationship') ).to eq( 'comment' )
|
751
|
+
|
752
|
+
expect( @handler.datastore.fetch(r_uuid).read ).to eq( 'Chunkers!' )
|
753
|
+
end
|
754
|
+
|
755
|
+
|
756
|
+
it "doesn't process requests for paths under the metadata uri-space" do
|
757
|
+
described_class.configure( :processors => %w[test] )
|
758
|
+
processor = described_class.processors.first
|
759
|
+
|
760
|
+
req = factory.post( "/#{TEST_UUID}/metadata", TEST_TEXT_DATA, content_type: 'text/plain' )
|
761
|
+
@handler.handle( req )
|
762
|
+
|
763
|
+
expect( processor.was_called ).to be_falsey
|
764
|
+
end
|
765
|
+
|
766
|
+
|
767
|
+
it "processes responses" do
|
768
|
+
described_class.configure( :processors => %w[test] )
|
769
|
+
|
770
|
+
uuid = @handler.datastore.save( @text_io )
|
771
|
+
@handler.metastore.save( uuid, {'format' => 'text/plain'} )
|
772
|
+
|
773
|
+
req = factory.get( "/#{uuid}" )
|
774
|
+
res = @handler.handle( req )
|
775
|
+
|
776
|
+
res.body.rewind
|
777
|
+
expect( res.body.read ).to eq( TEST_TEXT_DATA.reverse )
|
778
|
+
end
|
779
|
+
|
780
|
+
|
781
|
+
it "doesn't process responses for paths under the metadata uri-space" do
|
782
|
+
described_class.configure( :processors => %w[test] )
|
783
|
+
processor = described_class.processors.first
|
784
|
+
|
785
|
+
uuid = @handler.datastore.save( @text_io )
|
786
|
+
@handler.metastore.save( uuid, {'format' => 'text/plain'} )
|
787
|
+
|
788
|
+
req = factory.get( "/#{uuid}/metadata" )
|
789
|
+
@handler.handle( req )
|
790
|
+
|
791
|
+
expect( processor.was_called ).to be_falsey
|
792
|
+
end
|
793
|
+
end
|
794
|
+
|
795
|
+
|
796
|
+
context "event hook" do
|
797
|
+
|
798
|
+
let( :factory ) do
|
799
|
+
Mongrel2::RequestFactory.new(
|
800
|
+
:route => '/',
|
801
|
+
:headers => {:accept => '*/*'})
|
802
|
+
end
|
803
|
+
|
804
|
+
before( :each ) do
|
805
|
+
@handler.setup_event_socket
|
806
|
+
|
807
|
+
@subsock = Mongrel2.zmq_context.socket( :SUB )
|
808
|
+
@subsock.linger = 0
|
809
|
+
@subsock.subscribe( '' )
|
810
|
+
@subsock.connect( @handler.event_socket.endpoint )
|
811
|
+
end
|
812
|
+
|
813
|
+
after( :each ) do
|
814
|
+
@subsock.close
|
815
|
+
end
|
816
|
+
|
817
|
+
it "publishes notifications about uploaded assets to a PUBSUB socket" do
|
818
|
+
req = factory.post( '/', TEST_TEXT_DATA, content_type: 'text/plain' )
|
819
|
+
res = @handler.handle( req )
|
820
|
+
|
821
|
+
handles = ZMQ.select( [@subsock], nil, nil, 0 )
|
822
|
+
expect( handles ).to be_an( Array )
|
823
|
+
expect( handles[0].size ).to eq( 1 )
|
824
|
+
expect( handles[0].first ).to be( @subsock )
|
825
|
+
|
826
|
+
event = @subsock.recv
|
827
|
+
expect( @subsock.rcvmore? ).to be_truthy
|
828
|
+
expect( event ).to eq( 'created' )
|
829
|
+
|
830
|
+
resource = @subsock.recv
|
831
|
+
expect( @subsock.rcvmore? ).to be_falsey
|
832
|
+
expect( resource ).to match( /^\{"uuid":"#{UUID_PATTERN}"\}$/ )
|
833
|
+
end
|
834
|
+
end
|
835
|
+
|
836
|
+
end
|
837
|
+
|
838
|
+
# vim: set nosta noet ts=4 sw=4 ft=rspec:
|