rdf-ldp 0.9.2 → 0.9.3

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.
@@ -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