rdf-ldp 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,370 @@
1
+ require 'rdf/ldp/spec/resource'
2
+
3
+ shared_examples 'an RDFSource' do
4
+ it_behaves_like 'a Resource'
5
+
6
+ let(:uri) { RDF::URI('http://ex.org/moomin') }
7
+ subject { described_class.new('http://ex.org/moomin') }
8
+ it { is_expected.to be_rdf_source }
9
+ it { is_expected.not_to be_non_rdf_source }
10
+
11
+ describe '#parse_graph' do
12
+ it 'raises UnsupportedMediaType if no reader found' do
13
+ expect { subject.send(:parse_graph, StringIO.new('graph'), 'text/fake') }
14
+ .to raise_error RDF::LDP::UnsupportedMediaType
15
+ end
16
+
17
+ it 'raises BadRequest if graph cannot be parsed' do
18
+ expect do
19
+ subject.send(:parse_graph,
20
+ StringIO.new('graph'),
21
+ 'application/n-triples')
22
+ end.to raise_error RDF::LDP::BadRequest
23
+ end
24
+
25
+ describe 'parsing the graph' do
26
+ let(:graph) { RDF::Repository.new }
27
+
28
+ before do
29
+ graph << RDF::Statement(RDF::URI('http://ex.org/moomin'),
30
+ RDF.type,
31
+ RDF::Vocab::FOAF.Person,
32
+ graph_name: subject.subject_uri)
33
+
34
+ 10.times do
35
+ graph << RDF::Statement(RDF::Node.new,
36
+ RDF::Vocab::DC.creator,
37
+ RDF::Node.new,
38
+ graph_name: subject.subject_uri)
39
+ end
40
+ end
41
+
42
+ it 'parses turtle' do
43
+ expect(subject.send(:parse_graph,
44
+ StringIO.new(graph.dump(:ttl)),
45
+ 'text/turtle'))
46
+ .to be_isomorphic_with graph
47
+ end
48
+
49
+ it 'parses ntriples' do
50
+ expect(subject.send(:parse_graph,
51
+ StringIO.new(graph.dump(:ntriples)),
52
+ 'application/n-triples'))
53
+ .to be_isomorphic_with graph
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '#etag' do
59
+ before do
60
+ subject.graph << statement
61
+ other.graph << statement
62
+ end
63
+
64
+ let(:other) { described_class.new(RDF::URI('http://ex.org/blah')) }
65
+
66
+ let(:statement) do
67
+ RDF::Statement(RDF::URI('http://ex.org/m'),
68
+ RDF::Vocab::DC.title,
69
+ 'moomin')
70
+ end
71
+
72
+ it 'is the same for equal graphs' do
73
+ expect(subject.etag).to eq other.etag
74
+ end
75
+
76
+ xit 'is different for different graphs' do
77
+ subject.graph <<
78
+ RDF::Statement(RDF::Node.new, RDF::Vocab::DC.title, 'mymble')
79
+ expect(subject.etag).not_to eq other.etag
80
+ end
81
+ end
82
+
83
+ describe '#create' do
84
+ let(:subject) { described_class.new(RDF::URI('http://ex.org/m')) }
85
+ let(:graph) { RDF::Graph.new }
86
+
87
+ it 'does not create when graph fails to parse' do
88
+ begin
89
+ subject.create(StringIO.new(graph.dump(:ttl)), 'text/moomin')
90
+ rescue; end
91
+
92
+ expect(subject).not_to exist
93
+ end
94
+
95
+ it 'returns itself' do
96
+ expect(subject.create(StringIO.new(graph.dump(:ttl)), 'text/turtle'))
97
+ .to eq subject
98
+ end
99
+
100
+ it 'yields a transaction' do
101
+ expect do |b|
102
+ subject.create(StringIO.new(graph.dump(:ttl)), 'text/turtle', &b)
103
+ end.to yield_with_args(be_kind_of(RDF::Transaction))
104
+ end
105
+
106
+ it 'interprets NULL URI as this resource' do
107
+ graph << RDF::Statement(RDF::URI(), RDF::Vocab::DC.title, 'moomin')
108
+
109
+ created =
110
+ subject.create(StringIO.new(graph.dump(:ttl)), 'text/turtle').graph
111
+
112
+ expect(created)
113
+ .to have_statement RDF::Statement(subject.subject_uri,
114
+ RDF::Vocab::DC.title,
115
+ 'moomin')
116
+ end
117
+
118
+ it 'interprets Relative URIs as this based on this resource' do
119
+ graph << RDF::Statement(subject.subject_uri,
120
+ RDF::Vocab::DC.isPartOf,
121
+ RDF::URI('#moomin'))
122
+
123
+ created =
124
+ subject.create(StringIO.new(graph.dump(:ttl)), 'text/turtle').graph
125
+
126
+ expect(created)
127
+ .to have_statement RDF::Statement(subject.subject_uri,
128
+ RDF::Vocab::DC.isPartOf,
129
+ subject.subject_uri / '#moomin')
130
+ end
131
+ end
132
+
133
+ describe '#update' do
134
+ let(:statement) do
135
+ RDF::Statement(subject.subject_uri, RDF::Vocab::DC.title, 'moomin')
136
+ end
137
+
138
+ let(:graph) { RDF::Graph.new << statement }
139
+ let(:content_type) { 'text/turtle' }
140
+
141
+ shared_examples 'updating rdf_sources' do
142
+ it 'changes the response' do
143
+ expect { subject.update(StringIO.new(graph.dump(:ttl)), content_type) }
144
+ .to change { subject.to_response }
145
+ end
146
+
147
+ it 'changes etag' do
148
+ expect { subject.update(StringIO.new(graph.dump(:ttl)), content_type) }
149
+ .to change { subject.etag }
150
+ end
151
+
152
+ it 'yields a transaction' do
153
+ expect do |b|
154
+ subject.update(StringIO.new(graph.dump(:ttl)), content_type, &b)
155
+ end.to yield_with_args(be_kind_of(RDF::Transaction))
156
+ end
157
+
158
+ context 'with bad media type' do
159
+ it 'raises UnsupportedMediaType' do
160
+ graph_io = StringIO.new(graph.dump(:ttl))
161
+
162
+ expect { subject.update(graph_io, 'text/moomin') }
163
+ .to raise_error RDF::LDP::UnsupportedMediaType
164
+ end
165
+
166
+ it 'does not update #last_modified' do
167
+ modified = subject.last_modified
168
+ begin
169
+ subject.update(StringIO.new(graph.dump(:ttl)), 'text/moomin')
170
+ rescue; end
171
+
172
+ expect(subject.last_modified).to eq modified
173
+ end
174
+ end
175
+ end
176
+
177
+ include_examples 'updating rdf_sources'
178
+
179
+ context 'when it exists' do
180
+ before { subject.create(StringIO.new, 'application/n-triples') }
181
+
182
+ include_examples 'updating rdf_sources'
183
+ end
184
+ end
185
+
186
+ describe '#patch' do
187
+ it 'raises UnsupportedMediaType when no media type is given' do
188
+ expect { subject.request(:patch, 200, {}, {}) }
189
+ .to raise_error RDF::LDP::UnsupportedMediaType
190
+ end
191
+
192
+ it 'gives PreconditionFailed when trying to update with wrong Etag' do
193
+ env = { 'HTTP_IF_MATCH' => 'not an Etag' }
194
+ expect { subject.request(:PATCH, 200, { 'abc' => 'def' }, env) }
195
+ .to raise_error RDF::LDP::PreconditionFailed
196
+ end
197
+
198
+ context 'ldpatch' do
199
+ it 'raises BadRequest when invalid document' do
200
+ env = { 'CONTENT_TYPE' => 'text/ldpatch',
201
+ 'rack.input' => StringIO.new('---invalid---') }
202
+ expect { subject.request(:patch, 200, {}, env) }
203
+ .to raise_error RDF::LDP::BadRequest
204
+ end
205
+
206
+ it 'handles patch' do
207
+ statement =
208
+ RDF::Statement(subject.subject_uri, RDF::Vocab::FOAF.name, 'Moomin')
209
+ patch = '@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .' \
210
+ "\n\nAdd { #{statement.subject.to_base} " \
211
+ "#{statement.predicate.to_base} #{statement.object.to_base} } ."
212
+ env = { 'CONTENT_TYPE' => 'text/ldpatch',
213
+ 'rack.input' => StringIO.new(patch) }
214
+
215
+ expect { subject.request(:patch, 200, {}, env) }
216
+ .to change { subject.graph.statements.to_a }
217
+ .to(contain_exactly(statement))
218
+ end
219
+ end
220
+
221
+ context 'sparql update' do
222
+ it 'raises BadRequest when invalid document' do
223
+ env = { 'CONTENT_TYPE' => 'application/sparql-update',
224
+ 'rack.input' => StringIO.new('---invalid---') }
225
+
226
+ expect { subject.request(:patch, 200, {}, env) }
227
+ .to raise_error RDF::LDP::BadRequest
228
+ end
229
+
230
+ it 'runs sparql update' do
231
+ update = "INSERT DATA { #{subject.subject_uri.to_base} "\
232
+ "#{RDF::Vocab::DC.title.to_base} 'moomin' . }"
233
+
234
+ env = { 'CONTENT_TYPE' => 'application/sparql-update',
235
+ 'rack.input' => StringIO.new(update) }
236
+
237
+ expect { subject.request(:patch, 200, {}, env) }
238
+ .to change { subject.graph.count }.from(0).to(1)
239
+ end
240
+ end
241
+ end
242
+
243
+ describe '#graph' do
244
+ it 'has a graph' do
245
+ expect(subject.graph).to be_a RDF::Enumerable
246
+ end
247
+ end
248
+
249
+ describe '#subject_uri' do
250
+ let(:uri) { RDF::URI('http://ex.org/moomin') }
251
+
252
+ it 'has a uri getter' do
253
+ expect(subject.subject_uri).to eq uri
254
+ end
255
+
256
+ it 'aliases to #to_uri' do
257
+ expect(subject.to_uri).to eq uri
258
+ end
259
+ end
260
+
261
+ describe '#to_response' do
262
+ it 'gives the graph minus context' do
263
+ expect(subject.to_response.graph_name).to eq nil
264
+ end
265
+ end
266
+
267
+ describe '#request' do
268
+ context 'with :GET' do
269
+ it 'gives the subject' do
270
+ expect(subject.request(:GET, 200, { 'abc' => 'def' }, {}))
271
+ .to contain_exactly(200, a_hash_including('abc' => 'def'), subject)
272
+ end
273
+
274
+ it 'does not call the graph' do
275
+ expect(subject).not_to receive(:graph)
276
+ subject.request(:GET, 200, { 'abc' => 'def' }, {})
277
+ end
278
+
279
+ it 'returns 410 GONE when destroyed' do
280
+ allow(subject).to receive(:destroyed?).and_return true
281
+ expect { subject.request(:GET, 200, { 'abc' => 'def' }, {}) }
282
+ .to raise_error RDF::LDP::Gone
283
+ end
284
+ end
285
+
286
+ context 'with :DELETE' do
287
+ before { subject.create(StringIO.new, 'application/n-triples') }
288
+
289
+ it 'returns 204' do
290
+ expect(subject.request(:DELETE, 200, {}, {}).first).to eq 204
291
+ end
292
+
293
+ it 'returns an empty body' do
294
+ expect(subject.request(:DELETE, 200, {}, {}).last)
295
+ .to be_empty
296
+ end
297
+
298
+ it 'marks resource as destroyed' do
299
+ expect { subject.request(:DELETE, 200, {}, {}) }
300
+ .to change { subject.destroyed? }.from(false).to(true)
301
+ end
302
+ end
303
+
304
+ context 'with :PUT',
305
+ if: described_class.private_method_defined?(:put) do
306
+ let(:graph) { RDF::Graph.new }
307
+ let(:env) do
308
+ { 'rack.input' => StringIO.new(graph.dump(:ntriples)),
309
+ 'CONTENT_TYPE' => 'application/n-triples' }
310
+ end
311
+
312
+ it 'creates the resource' do
313
+ expect { subject.request(:PUT, 200, { 'abc' => 'def' }, env) }
314
+ .to change { subject.exists? }.from(false).to(true)
315
+ end
316
+
317
+ it 'responds 201' do
318
+ expect(subject.request(:PUT, 200, { 'abc' => 'def' }, env).first)
319
+ .to eq 201
320
+ end
321
+
322
+ it 'returns the etag' do
323
+ expect(subject.request(:PUT, 200, { 'abc' => 'def' }, env)[1]['ETag'])
324
+ .to eq subject.etag
325
+ end
326
+
327
+ context 'when subject exists' do
328
+ before { subject.create(StringIO.new, 'application/n-triples') }
329
+
330
+ it 'responds 200' do
331
+ expect(subject.request(:PUT, 200, { 'abc' => 'def' }, env))
332
+ .to contain_exactly(200, a_hash_including('abc' => 'def'), subject)
333
+ end
334
+
335
+ it 'replaces the graph with the input' do
336
+ graph <<
337
+ RDF::Statement(subject.subject_uri, RDF::Vocab::DC.title, 'moomin')
338
+ expect { subject.request(:PUT, 200, { 'abc' => 'def' }, env) }
339
+ .to change { subject.graph.statements.count }.to(1)
340
+ end
341
+
342
+ it 'updates the etag' do
343
+ graph <<
344
+ RDF::Statement(subject.subject_uri, RDF::Vocab::DC.title, 'moomin')
345
+ expect { subject.request(:PUT, 200, { 'abc' => 'def' }, env) }
346
+ .to change { subject.etag }
347
+ end
348
+
349
+ it 'returns the etag' do
350
+ expect(subject.request(:PUT, 200, { 'abc' => 'def' }, env)[1]['ETag'])
351
+ .to eq subject.etag
352
+ end
353
+
354
+ it 'gives PreconditionFailed when trying to update with wrong Etag' do
355
+ env['HTTP_IF_MATCH'] = 'not an Etag'
356
+ expect { subject.request(:PUT, 200, { 'abc' => 'def' }, env) }
357
+ .to raise_error RDF::LDP::PreconditionFailed
358
+ end
359
+
360
+ it 'succeeds when giving correct Etag' do
361
+ graph <<
362
+ RDF::Statement(subject.subject_uri, RDF::Vocab::DC.title, 'moomin')
363
+ env['HTTP_IF_MATCH'] = subject.etag
364
+ expect { subject.request(:PUT, 200, { 'abc' => 'def' }, env) }
365
+ .to change { subject.graph.statements.count }
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,242 @@
1
+ require 'rspec'
2
+ require 'rdf/spec'
3
+ require 'rdf/spec/matchers'
4
+ require 'timecop'
5
+
6
+ shared_examples 'a Resource' do
7
+ describe '.to_uri' do
8
+ it { expect(described_class.to_uri).to be_a RDF::URI }
9
+ end
10
+
11
+ subject { described_class.new(uri) }
12
+ let(:uri) { RDF::URI 'http://example.org/moomin' }
13
+
14
+ it { is_expected.to be_ldp_resource }
15
+ it { is_expected.to respond_to :container? }
16
+ it { is_expected.to respond_to :rdf_source? }
17
+ it { is_expected.to respond_to :non_rdf_source? }
18
+
19
+ it { subject.send(:set_last_modified) }
20
+
21
+ describe '#exists?' do
22
+ it 'does not exist' do
23
+ expect(subject).not_to exist
24
+ end
25
+
26
+ context 'while existing' do
27
+ before { subject.create(StringIO.new, 'application/n-triples') }
28
+
29
+ subject { described_class.new(uri, repository) }
30
+ let(:repository) { RDF::Repository.new }
31
+
32
+ it 'exists' do
33
+ expect(subject).to exist
34
+ end
35
+
36
+ it 'is different from same URI with trailing /' do
37
+ expect(described_class.new(uri + '/', repository)).not_to exist
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '#allowed_methods' do
43
+ it 'responds to all methods returned' do
44
+ subject.allowed_methods.each do |method|
45
+ expect(subject.respond_to?(method.downcase, true)).to be true
46
+ end
47
+ end
48
+
49
+ it 'includes the MUST methods' do
50
+ expect(subject.allowed_methods).to include(:GET, :OPTIONS, :HEAD)
51
+ end
52
+ end
53
+
54
+ describe '#create' do
55
+ it 'accepts two args' do
56
+ expect(described_class.instance_method(:create).arity).to eq 2
57
+ end
58
+
59
+ describe 'modified time' do
60
+ before { Timecop.freeze }
61
+ after { Timecop.return }
62
+
63
+ it 'sets last_modified' do
64
+ subject.create(StringIO.new, 'text/turtle')
65
+ expect(subject.last_modified).to eq DateTime.now
66
+ end
67
+ end
68
+
69
+ it 'adds a type triple to metagraph' do
70
+ subject.create(StringIO.new, 'application/n-triples')
71
+ expect(subject.metagraph)
72
+ .to have_statement RDF::Statement(subject.subject_uri,
73
+ RDF.type,
74
+ described_class.to_uri)
75
+ end
76
+
77
+ it 'yields a transaction' do
78
+ expect { |b| subject.create(StringIO.new, 'application/n-triples', &b) }
79
+ .to yield_with_args(be_kind_of(RDF::Transaction))
80
+ end
81
+
82
+ it 'marks resource as existing' do
83
+ expect { subject.create(StringIO.new, 'application/n-triples') }
84
+ .to change { subject.exists? }.from(false).to(true)
85
+ end
86
+
87
+ it 'returns self' do
88
+ expect(subject.create(StringIO.new, 'application/n-triples'))
89
+ .to eq subject
90
+ end
91
+
92
+ it 'raises Conlict when already exists' do
93
+ subject.create(StringIO.new, 'application/n-triples')
94
+ expect { subject.create(StringIO.new, 'application/n-triples') }
95
+ .to raise_error RDF::LDP::Conflict
96
+ end
97
+ end
98
+
99
+ describe '#update' do
100
+ it 'accepts two args' do
101
+ expect(described_class.instance_method(:update).arity).to eq 2
102
+ end
103
+
104
+ it 'returns self' do
105
+ expect(subject.update(StringIO.new, 'application/n-triples'))
106
+ .to eq subject
107
+ end
108
+
109
+ it 'yields a changeset' do
110
+ expect { |b| subject.update(StringIO.new, 'application/n-triples', &b) }
111
+ .to yield_with_args(be_kind_of(RDF::Transaction))
112
+ end
113
+ end
114
+
115
+ describe '#destroy' do
116
+ it 'accepts no args' do
117
+ expect(described_class.instance_method(:destroy).arity).to eq 0
118
+ end
119
+ end
120
+
121
+ describe '#metagraph' do
122
+ it 'returns a graph' do
123
+ expect(subject.metagraph).to be_a RDF::Graph
124
+ end
125
+
126
+ it 'has the metagraph name for the resource' do
127
+ expect(subject.metagraph.graph_name).to eq subject.subject_uri / '#meta'
128
+ end
129
+ end
130
+
131
+ describe '#etag' do
132
+ before { subject.create(StringIO.new, 'application/n-triples') }
133
+
134
+ it 'has an etag' do
135
+ expect(subject.etag).to be_a String
136
+ end
137
+
138
+ it 'updates etag on change' do
139
+ expect { subject.update(StringIO.new, 'application/n-triples') }
140
+ .to change { subject.etag }
141
+ end
142
+ end
143
+
144
+ describe '#last_modified' do
145
+ it 'returns nil when no dc:modified triple is present' do
146
+ expect(subject.last_modified).to be_nil
147
+ end
148
+
149
+ it 'raises an error when exists without dc:modified triple is present' do
150
+ allow(subject).to receive(:exists?).and_return true
151
+ expect { subject.last_modified }.to raise_error RDF::LDP::RequestError
152
+ end
153
+
154
+ context 'with dc:modified triple' do
155
+ before do
156
+ subject.metagraph.update([subject.subject_uri,
157
+ RDF::Vocab::DC.modified,
158
+ datetime])
159
+ end
160
+
161
+ let(:datetime) { DateTime.now }
162
+
163
+ it 'returns date in `dc:modified`' do
164
+ expect(subject.last_modified).to eq datetime
165
+ end
166
+ end
167
+ end
168
+
169
+ describe '#to_response' do
170
+ it 'returns an object that responds to #each' do
171
+ expect(subject.to_response).to respond_to :each
172
+ end
173
+ end
174
+
175
+ describe '#request' do
176
+ it 'sends the message to itself' do
177
+ expect(subject).to receive(:blah)
178
+ subject.request(:BLAH, 200, {}, {})
179
+ end
180
+
181
+ it 'raises MethodNotAllowed when method is unimplemented' do
182
+ allow(subject).to receive(:not_implemented)
183
+ .and_raise NotImplementedError
184
+ expect { subject.request(:not_implemented, 200, {}, {}) }
185
+ .to raise_error(RDF::LDP::MethodNotAllowed)
186
+ end
187
+
188
+ [:GET, :OPTIONS, :HEAD].each do |method|
189
+ it "responds to #{method}" do
190
+ expect(subject.request(method, 200, {}, {}).size).to eq 3
191
+ end
192
+ end
193
+
194
+ [:PATCH, :POST, :PUT, :DELETE, :TRACE, :CONNECT].each do |method|
195
+ it "responds to or errors on #{method}" do
196
+ g = RDF::Graph.new << [RDF::Node.new, RDF.type, 'moomin']
197
+ env = { 'CONTENT_TYPE' => 'application/n-triples',
198
+ 'rack.input' => StringIO.new(g.dump(:ntriples)) }
199
+
200
+ begin
201
+ response = subject.request(method, 200, {}, env)
202
+ expect(response.size).to eq 3
203
+ rescue => e
204
+ expect(e).to be_a RDF::LDP::RequestError
205
+ end
206
+ end
207
+ end
208
+
209
+ describe 'HTTP headers' do
210
+ before { subject.create(StringIO.new, 'text/turtle') }
211
+ let(:headers) { subject.request(:GET, 200, {}, {})[1] }
212
+
213
+ it 'has ETag' do
214
+ expect(headers['ETag']).to eq subject.etag
215
+ end
216
+
217
+ it 'has Last-Modified' do
218
+ expect(headers['Last-Modified']).to eq subject.last_modified.httpdate
219
+ end
220
+
221
+ it 'has Allow' do
222
+ expect(headers['Allow']).to be_a String
223
+ end
224
+
225
+ it 'has Link' do
226
+ expect(headers['Link']).to be_a String
227
+ end
228
+ end
229
+
230
+ it 'responds to :GET' do
231
+ expect { subject.request(:GET, 200, {}, {}) }.not_to raise_error
232
+ end
233
+
234
+ it 'responds to :HEAD' do
235
+ expect { subject.request(:OPTIONS, 200, {}, {}) }.not_to raise_error
236
+ end
237
+
238
+ it 'responds to :OPTIONS' do
239
+ expect { subject.request(:OPTIONS, 200, {}, {}) }.not_to raise_error
240
+ end
241
+ end
242
+ end