rdf-ldp 0.9.2 → 2.1.0

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,152 @@
1
+ require 'rdf/ldp/spec/direct_container'
2
+
3
+ shared_examples 'an IndirectContainer' do
4
+ it_behaves_like 'a DirectContainer'
5
+
6
+ shared_context 'with a relation' do
7
+ before do
8
+ subject.create(StringIO.new(graph.dump(:ntriples)),
9
+ 'application/n-triples')
10
+ end
11
+
12
+ let(:graph) { RDF::Graph.new << inserted_content_statement }
13
+ let(:relation_predicate) { RDF::Vocab::DC.creator }
14
+
15
+ let(:inserted_content_statement) do
16
+ RDF::Statement(uri,
17
+ RDF::Vocab::LDP.insertedContentRelation,
18
+ relation_predicate)
19
+ end
20
+ end
21
+
22
+ describe '#inserted_content_relation' do
23
+ it 'returns a uri' do
24
+ subject.create(StringIO.new, 'application/n-triples')
25
+ expect(subject.inserted_content_relation).to be_a RDF::URI
26
+ end
27
+
28
+ context 'with a relation' do
29
+ include_context 'with a relation'
30
+
31
+ it 'gives the relation' do
32
+ expect(subject.inserted_content_relation).to eq relation_predicate
33
+ end
34
+
35
+ it 'raises an error when more than one exists' do
36
+ new_statement = inserted_content_statement.clone
37
+ new_statement.object = RDF::Vocab::DC.relation
38
+ subject.graph << new_statement
39
+ expect { subject.inserted_content_relation }
40
+ .to raise_error RDF::LDP::NotAcceptable
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#add' do
46
+ include_context 'with a relation'
47
+
48
+ subject { described_class.new(uri, repo) }
49
+
50
+ let(:repo) { RDF::Repository.new }
51
+ let(:resource_uri) { RDF::URI('http://example.org/too-ticky') }
52
+ let(:contained_resource) { RDF::LDP::RDFSource.new(resource_uri, repo) }
53
+
54
+ context 'when no derived URI is found' do
55
+ it 'raises NotAcceptable' do
56
+ expect { subject.add(contained_resource) }
57
+ .to raise_error RDF::LDP::NotAcceptable
58
+ end
59
+
60
+ it 'does not create the resource' do
61
+ begin; subject.add(contained_resource); rescue; end
62
+ expect(contained_resource).not_to exist
63
+ end
64
+ end
65
+
66
+ context 'with expected predicate' do
67
+ before { contained_resource.graph << statement }
68
+
69
+ let(:target_uri) { contained_resource.to_uri / '#me' }
70
+
71
+ let(:statement) do
72
+ RDF::Statement(contained_resource.to_uri,
73
+ relation_predicate,
74
+ target_uri)
75
+ end
76
+
77
+ it 'when membership resource does not exist raises NotAcceptable' do
78
+ new_resource = described_class.new(uri / 'new', repo)
79
+ expect { new_resource.add(contained_resource) }
80
+ .to raise_error RDF::LDP::NotAcceptable
81
+ end
82
+
83
+ context 'when the container exists' do
84
+ it 'adds membership triple' do
85
+ subject.add(contained_resource)
86
+ expect(subject.graph.statements)
87
+ .to include RDF::Statement(subject.to_uri,
88
+ subject.membership_predicate,
89
+ target_uri)
90
+ end
91
+
92
+ it 'for multiple predicates raises NotAcceptable' do
93
+ new_statement = statement.clone
94
+ new_statement.object = contained_resource.to_uri / '#you'
95
+ contained_resource.graph << new_statement
96
+ expect { subject.add(contained_resource) }
97
+ .to raise_error RDF::LDP::NotAcceptable
98
+ end
99
+
100
+ it 'for an LDP-NR raises NotAcceptable' do
101
+ nr_resource = RDF::LDP::NonRDFSource.new(resource_uri, repo)
102
+ expect { subject.add(nr_resource) }
103
+ .to raise_error RDF::LDP::NotAcceptable
104
+ end
105
+
106
+ context 'with membership resource' do
107
+ before do
108
+ subject.graph
109
+ .delete([uri, RDF::Vocab::LDP.membershipResource, nil])
110
+ subject.graph << RDF::Statement(uri,
111
+ RDF::Vocab::LDP.membershipResource,
112
+ membership_resource)
113
+ end
114
+
115
+ let(:membership_resource) { uri }
116
+
117
+ it 'raises error when resource does not exist' do
118
+ new_resource = described_class.new(uri / 'new', repo)
119
+ expect { new_resource.add(contained_resource) }
120
+ .to raise_error RDF::LDP::NotAcceptable
121
+ end
122
+
123
+ context 'when the membership resource is not in the server' do
124
+ let(:membership_resource) { uri / '#me' }
125
+
126
+ it 'adds membership triple to container' do
127
+ contained_resource.create(StringIO.new, 'application/n-triples')
128
+ subject.add(contained_resource)
129
+
130
+ expect(subject.graph.statements)
131
+ .to include RDF::Statement(membership_resource,
132
+ subject.membership_predicate,
133
+ target_uri)
134
+ end
135
+
136
+ it 'removes membership triple to container' do
137
+ contained_resource.create(StringIO.new, 'application/n-triples')
138
+
139
+ subject.add(contained_resource)
140
+ subject.remove(contained_resource)
141
+
142
+ expect(subject.graph.statements)
143
+ .not_to include RDF::Statement(membership_resource,
144
+ subject.membership_predicate,
145
+ target_uri)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,138 @@
1
+ require 'rdf/ldp/spec/resource'
2
+
3
+ shared_examples 'a NonRDFSource' do
4
+ it_behaves_like 'a Resource'
5
+
6
+ subject { described_class.new(uri) }
7
+ let(:uri) { RDF::URI 'http://example.org/moomin' }
8
+
9
+ let(:contents) { StringIO.new('mummi') }
10
+
11
+ after { subject.destroy }
12
+
13
+ describe '#non_rdf_source?' do
14
+ it { is_expected.to be_non_rdf_source }
15
+ end
16
+
17
+ describe '#create' do
18
+ it 'writes the input to body' do
19
+ subject.create(contents, 'text/plain')
20
+ contents.rewind
21
+ expect(subject.to_response.each.to_a).to eq contents.each.to_a
22
+ end
23
+
24
+ it 'sets #content_type' do
25
+ expect { subject.create(StringIO.new(''), 'text/plain') }
26
+ .to change { subject.content_type }.to('text/plain')
27
+ end
28
+
29
+ it 'persists to resource' do
30
+ repo = RDF::Repository.new
31
+ saved = described_class.new(uri, repo)
32
+
33
+ saved.create(contents, 'text/plain')
34
+ contents.rewind
35
+
36
+ loaded = RDF::LDP::Resource.find(uri, repo)
37
+ expect(loaded.to_response.each.to_a).to eq contents.each.to_a
38
+ end
39
+
40
+ it 'creates an LDP::RDFSource' do
41
+ repo = RDF::Repository.new
42
+ saved = described_class.new(uri, repo)
43
+ description = RDF::LDP::RDFSource.new(subject.description_uri, repo)
44
+
45
+ expect { saved.create(contents, 'text/plain') }
46
+ .to change { description.exists? }.from(false).to(true)
47
+ end
48
+ end
49
+
50
+ describe '#update' do
51
+ before { subject.create(contents, 'text/plain') }
52
+
53
+ it 'writes the input to body' do
54
+ new_contents = StringIO.new('snorkmaiden')
55
+ expect { subject.update(new_contents, 'text/plain') }
56
+ .to change { subject.to_response.to_a }
57
+ .from(a_collection_containing_exactly('mummi'))
58
+ .to(a_collection_containing_exactly('snorkmaiden'))
59
+ end
60
+
61
+ it 'updates #content_type' do
62
+ expect { subject.update(contents, 'text/prs.moomin') }
63
+ .to change { subject.content_type }
64
+ .from('text/plain').to('text/prs.moomin')
65
+ end
66
+ end
67
+
68
+ describe '#description' do
69
+ it 'is not found' do
70
+ expect { subject.description }.to raise_error RDF::LDP::NotFound
71
+ end
72
+
73
+ context 'when it exists' do
74
+ before { subject.create(StringIO.new(''), 'text/plain') }
75
+
76
+ it 'is an RDFSource' do
77
+ expect(subject.description).to be_rdf_source
78
+ end
79
+
80
+ it 'is the description uri' do
81
+ expect(subject.description.to_uri).to eq subject.description_uri
82
+ end
83
+ end
84
+ end
85
+
86
+ describe '#description_uri' do
87
+ it 'is a uri' do
88
+ expect(subject.description_uri).to be_a RDF::URI
89
+ end
90
+ end
91
+
92
+ describe '#storage' do
93
+ it 'sets a default storage adapter' do
94
+ expect(subject.storage).to be_a RDF::LDP::NonRDFSource::FileStorageAdapter
95
+ end
96
+
97
+ it 'explicitly sets a storage adapter' do
98
+ class DummyAdapter < RDF::LDP::NonRDFSource::FileStorageAdapter
99
+ end
100
+
101
+ dummy_subject = described_class.new(uri, nil, DummyAdapter)
102
+ expect(dummy_subject.storage).to be_a DummyAdapter
103
+ end
104
+ end
105
+
106
+ describe '#to_response' do
107
+ it 'gives an empty response if it is new' do
108
+ expect(subject.to_response.to_a).to eq []
109
+ end
110
+
111
+ it 'does not create a non-existant file' do
112
+ subject.to_response
113
+ expect(subject.storage.send(:file_exists?)).to be false
114
+ end
115
+ end
116
+
117
+ describe '#destroy' do
118
+ before { subject.create(contents, 'text/plain') }
119
+
120
+ it 'deletes the content' do
121
+ expect { subject.destroy }
122
+ .to change { subject.to_response.to_a }
123
+ .from(a_collection_containing_exactly('mummi')).to([])
124
+ end
125
+
126
+ it 'marks resource as destroyed' do
127
+ expect { subject.destroy }
128
+ .to change { subject.destroyed? }.from(false).to(true)
129
+ end
130
+ end
131
+
132
+ describe '#content_type' do
133
+ it 'sets and gets a content_type' do
134
+ expect { subject.content_type = 'text/plain' }
135
+ .to change { subject.content_type }.to('text/plain')
136
+ end
137
+ end
138
+ end
@@ -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.new, 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