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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +10 -0
- data/CREDITS +1 -0
- data/README.md +66 -10
- data/UNLICENSE +1 -1
- data/VERSION +1 -1
- data/app/lamprey.rb +119 -40
- data/bin/lamprey +15 -1
- data/lib/rack/ldp.rb +23 -25
- data/lib/rdf/ldp/container.rb +32 -22
- data/lib/rdf/ldp/direct_container.rb +9 -7
- data/lib/rdf/ldp/indirect_container.rb +8 -6
- data/lib/rdf/ldp/interaction_model.rb +39 -29
- data/lib/rdf/ldp/non_rdf_source.rb +10 -6
- data/lib/rdf/ldp/rdf_source.rb +3 -3
- data/lib/rdf/ldp/resource.rb +33 -31
- data/lib/rdf/ldp/spec/container.rb +337 -0
- data/lib/rdf/ldp/spec/direct_container.rb +245 -0
- data/lib/rdf/ldp/spec/indirect_container.rb +152 -0
- data/lib/rdf/ldp/spec/non_rdf_source.rb +138 -0
- data/lib/rdf/ldp/spec/rdf_source.rb +370 -0
- data/lib/rdf/ldp/spec/resource.rb +242 -0
- data/lib/rdf/ldp/spec.rb +6 -0
- data/lib/rdf/ldp/storage_adapters/file_storage_adapter.rb +4 -4
- data/lib/rdf/ldp/version.rb +16 -3
- data/lib/rdf/ldp.rb +4 -3
- metadata +89 -55
@@ -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
|