thingfish 0.5.0.pre20160707192835

Sign up to get free protection for your applications and to get access to all the features.
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: