thingfish 0.5.0.pre20160707192835
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.
- 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:
|