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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.simplecov +7 -0
  3. data/History.rdoc +5 -0
  4. data/LICENSE +29 -0
  5. data/Manifest.txt +39 -0
  6. data/Procfile +4 -0
  7. data/README.rdoc +92 -0
  8. data/Rakefile +92 -0
  9. data/bin/tfprocessord +6 -0
  10. data/bin/thingfish +10 -0
  11. data/etc/thingfish.conf.example +26 -0
  12. data/lib/strelka/app/metadata.rb +38 -0
  13. data/lib/strelka/httprequest/metadata.rb +70 -0
  14. data/lib/thingfish.rb +43 -0
  15. data/lib/thingfish/behaviors.rb +263 -0
  16. data/lib/thingfish/datastore.rb +55 -0
  17. data/lib/thingfish/datastore/memory.rb +93 -0
  18. data/lib/thingfish/handler.rb +728 -0
  19. data/lib/thingfish/metastore.rb +55 -0
  20. data/lib/thingfish/metastore/memory.rb +201 -0
  21. data/lib/thingfish/mixins.rb +57 -0
  22. data/lib/thingfish/processor.rb +79 -0
  23. data/lib/thingfish/processor/mp3.rb +167 -0
  24. data/lib/thingfish/processordaemon.rb +16 -0
  25. data/lib/thingfish/spechelpers.rb +165 -0
  26. data/spec/data/APIC-1-image.mp3 +0 -0
  27. data/spec/data/APIC-2-images.mp3 +0 -0
  28. data/spec/data/PIC-1-image.mp3 +0 -0
  29. data/spec/data/PIC-2-images.mp3 +0 -0
  30. data/spec/helpers.rb +67 -0
  31. data/spec/spec.opts +4 -0
  32. data/spec/thingfish/datastore/memory_spec.rb +19 -0
  33. data/spec/thingfish/datastore_spec.rb +64 -0
  34. data/spec/thingfish/handler_spec.rb +838 -0
  35. data/spec/thingfish/metastore/memory_spec.rb +17 -0
  36. data/spec/thingfish/metastore_spec.rb +96 -0
  37. data/spec/thingfish/mixins_spec.rb +63 -0
  38. data/spec/thingfish/processor/mp3_spec.rb +50 -0
  39. data/spec/thingfish/processor_spec.rb +65 -0
  40. data/spec/thingfish_spec.rb +23 -0
  41. 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,4 @@
1
+ -f
2
+ s
3
+ -c
4
+ -Du
@@ -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: