dav4rack_ext 0.0.2
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.
- data/.gitignore +6 -0
- data/.travis.yml +4 -0
- data/Gemfile +27 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +30 -0
- data/Rakefile +27 -0
- data/dav4rack_ext.gemspec +20 -0
- data/example/config.ru +50 -0
- data/example/rack_sniffer.rb +71 -0
- data/example/report.xml +151 -0
- data/example/test.rb +50 -0
- data/lib/dav4rack_ext/carddav/app.rb +78 -0
- data/lib/dav4rack_ext/carddav/controller.rb +117 -0
- data/lib/dav4rack_ext/carddav/resource.rb +140 -0
- data/lib/dav4rack_ext/carddav/resources/addressbook_collection_resource.rb +23 -0
- data/lib/dav4rack_ext/carddav/resources/addressbook_resource.rb +137 -0
- data/lib/dav4rack_ext/carddav/resources/contact_resource.rb +132 -0
- data/lib/dav4rack_ext/carddav/resources/principal_resource.rb +132 -0
- data/lib/dav4rack_ext/carddav.rb +13 -0
- data/lib/dav4rack_ext/handler.rb +51 -0
- data/lib/dav4rack_ext/helpers/properties.rb +55 -0
- data/lib/dav4rack_ext/version.rb +3 -0
- data/lib/dav4rack_ext.rb +4 -0
- data/specs/factories.rb +20 -0
- data/specs/rfc/rfc3744_spec.rb +62 -0
- data/specs/rfc/rfc5397_spec.rb +38 -0
- data/specs/rfc/rfc6352_spec.rb +284 -0
- data/specs/spec_helper.rb +42 -0
- data/specs/support/models.rb +161 -0
- data/specs/unit/carddav/app_spec.rb +36 -0
- data/specs/unit/carddav/resources/addressbook_collection_resource_spec.rb +51 -0
- data/specs/unit/carddav/resources/principal_resource_spec.rb +44 -0
- data/specs/unit/helpers/properties_spec.rb +58 -0
- metadata +133 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'RFC 5397: WebDAV Current Principal Extension' do
|
4
|
+
before do
|
5
|
+
@dav_ns = "DAV:"
|
6
|
+
|
7
|
+
@user = user = stub('User', username: 'john')
|
8
|
+
|
9
|
+
@root_path = root_path = '/'
|
10
|
+
|
11
|
+
app = Rack::Builder.new do
|
12
|
+
# use XMLSniffer
|
13
|
+
run DAV4Rack::Carddav.app(root_path, current_user: user)
|
14
|
+
end
|
15
|
+
|
16
|
+
serve_app(app)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '[3] DAV:current-user-principal' do
|
20
|
+
should 'return current user principal' do
|
21
|
+
response = propfind(@root_path, [
|
22
|
+
['current-user-principal', @dav_ns]
|
23
|
+
])
|
24
|
+
|
25
|
+
ensure_element_exists(response, %{D|prop > D|current-user-principal > D|href[text()="#{@root_path}"]})
|
26
|
+
end
|
27
|
+
|
28
|
+
should 'return unauthenticated if not logged in' do
|
29
|
+
# response = propfind(@root_path, [
|
30
|
+
# ['current-user-principal', @dav_ns]
|
31
|
+
# ])
|
32
|
+
|
33
|
+
# ensure_element_exists(response, %{D|prop > D|current-user-principal > D|unauthenticated})
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'RFC 6352: CardDav' do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@dav_ns = "DAV:"
|
7
|
+
@carddav_ns = "urn:ietf:params:xml:ns:carddav"
|
8
|
+
|
9
|
+
user_builder = proc do |env|
|
10
|
+
contact = FactoryGirl.build(:contact, uid: '1234-5678-9000-1')
|
11
|
+
contact.stubs(:etag).returns('ETAG')
|
12
|
+
contact.stubs(:vcard).returns(@parsed_vcard)
|
13
|
+
|
14
|
+
FactoryGirl.build(:user, env: env, login: 'john', addressbooks: [
|
15
|
+
FactoryGirl.build(:book, path: 'castor', name: "A book", contacts: [contact])
|
16
|
+
])
|
17
|
+
end
|
18
|
+
|
19
|
+
app = Rack::Builder.new do
|
20
|
+
# use XMLSniffer
|
21
|
+
run DAV4Rack::Carddav.app('/', current_user: user_builder)
|
22
|
+
end
|
23
|
+
|
24
|
+
serve_app(app)
|
25
|
+
|
26
|
+
@vcard_raw = <<-EOS
|
27
|
+
BEGIN:VCARD
|
28
|
+
VERSION:3.0
|
29
|
+
FN:Cyrus Daboo
|
30
|
+
N:Daboo;Cyrus
|
31
|
+
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
|
32
|
+
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
33
|
+
NICKNAME:me
|
34
|
+
NOTE:Example VCard.
|
35
|
+
ORG:Self Employed
|
36
|
+
TEL;TYPE=WORK,VOICE:412 605 0499
|
37
|
+
TEL;TYPE=FAX:412 605 0705
|
38
|
+
URL:http://www.example.com
|
39
|
+
UID:1234-5678-9000-1
|
40
|
+
END:VCARD
|
41
|
+
EOS
|
42
|
+
|
43
|
+
|
44
|
+
@vcard_raw2 = <<-EOS
|
45
|
+
BEGIN:VCARD
|
46
|
+
VERSION:3.0
|
47
|
+
FN:John Doe
|
48
|
+
N:John;Doe
|
49
|
+
ADR;TYPE=POSTAL:;2822 Email HQ;Suite 2821;RFCVille;PA;15213;USA
|
50
|
+
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
51
|
+
NOTE:Example VCard.
|
52
|
+
ORG:Self Employed
|
53
|
+
TEL;TYPE=WORK,VOICE:412 605 0499
|
54
|
+
TEL;TYPE=FAX:412 605 0705
|
55
|
+
URL:http://www.example.com
|
56
|
+
UID:1234-5678-9000-9
|
57
|
+
END:VCARD
|
58
|
+
EOS
|
59
|
+
|
60
|
+
@parsed_vcard = VCardParser::VCard.parse(@vcard_raw).first
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
describe '[6] Address Book Feature' do
|
65
|
+
|
66
|
+
describe '[6.1] Address Book Support' do
|
67
|
+
it '[6.1] advertise carddav support (MUST include addressbook in DAV header)' do
|
68
|
+
response = request(:options, '/')
|
69
|
+
response.headers['Dav'].should.include?('addressbook')
|
70
|
+
response.status.should == 200
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '[6.2] AddressBook properties' do
|
75
|
+
|
76
|
+
it '[6.2.1] CARDDAV:addressbook-description' do
|
77
|
+
# request('/', method: 'OPTIONS')
|
78
|
+
end
|
79
|
+
|
80
|
+
it '[6.2.3] CARDDAV:max-resource-size' do
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
describe '[6.3] Creating Resources' do
|
89
|
+
|
90
|
+
it '[6.3.1] Extended MKCOL Method' do
|
91
|
+
# optional
|
92
|
+
end
|
93
|
+
|
94
|
+
describe '[6.3.2] Creating Address Object Resources' do
|
95
|
+
before do
|
96
|
+
@headers = {
|
97
|
+
'HTTP_IF_NONE_MATCH' => '*'
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
should 'create contact' do
|
102
|
+
Testing::Contact.any_instance.expects(:etag).returns("ETAG")
|
103
|
+
|
104
|
+
# the url does not need to match the UID
|
105
|
+
response = request(:put, '/books/castor/new.vcf', @headers.merge(input: @vcard_raw2))
|
106
|
+
response.status.should == 201
|
107
|
+
|
108
|
+
# 6.3.2.3
|
109
|
+
response.headers['ETag'].should == "ETAG"
|
110
|
+
end
|
111
|
+
|
112
|
+
should 'return an error if contact exists' do
|
113
|
+
response = request(:put, '/books/castor/new.vcf', @headers.merge(input: @vcard_raw))
|
114
|
+
response.status.should == 409 # Conflict
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
describe '[6.3.2.1] Additional Preconditions for PUT, COPY, and MOVE' do
|
119
|
+
should 'enforce CARDDAV:supported-address-data' do
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
should 'enforce CARDDAV:valid-address-data' do
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
should 'enforce CARDDAV:no-uid-conflict' do
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
should 'enforce CARDDAV:max-resource-size' do
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
describe '[6.3.2.3] Address Object Resource Entity Tag' do
|
138
|
+
should 'set Etag header on GET' do
|
139
|
+
response = request(:get, '/books/castor/1234-5678-9000-1.vcf')
|
140
|
+
response.status.should == 200
|
141
|
+
response.headers['ETag'].should == "ETAG"
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
describe '[7] Address Book Access Control' do
|
157
|
+
|
158
|
+
it '[7.1.1] CARDDAV:addressbook-home-set Property' do
|
159
|
+
response = propfind('/', [
|
160
|
+
['addressbook-home-set', @carddav_ns]
|
161
|
+
])
|
162
|
+
|
163
|
+
response.status.should == 207
|
164
|
+
|
165
|
+
value = element_content(response, 'D|addressbook-home-set', 'D' => @carddav_ns)
|
166
|
+
value.should == '/books/'
|
167
|
+
|
168
|
+
# should not be returned by all
|
169
|
+
response = propfind('/')
|
170
|
+
value = element_content(response, 'D|addressbook-home-set', 'D' => @carddav_ns)
|
171
|
+
value.should == :missing
|
172
|
+
end
|
173
|
+
|
174
|
+
it '[7.1.2] CARDDAV:principal-address Property' do
|
175
|
+
response = propfind('/', [
|
176
|
+
['principal-address', @carddav_ns]
|
177
|
+
])
|
178
|
+
|
179
|
+
response.status.should == 207
|
180
|
+
|
181
|
+
value = element_content(response, 'D|principal-address', 'D' => @carddav_ns)
|
182
|
+
value.should == :empty
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
describe '[8] Address Book Reports' do
|
191
|
+
should 'advertise supported reports (REPORT method)' do
|
192
|
+
response = propfind('/books/', [
|
193
|
+
['supported-report-set', @dav_ns]
|
194
|
+
])
|
195
|
+
|
196
|
+
elements = ensure_element_exists(response, 'D|supported-report-set > D|report > C|addressbook-multiget',
|
197
|
+
'D' => @dav_ns, 'C' => @carddav_ns
|
198
|
+
)
|
199
|
+
elements[0].text.should == ""
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
describe '[8.3.1] CARDDAV:supported-collation-set Property' do
|
204
|
+
should 'return supported collations' do
|
205
|
+
response = propfind('/books/castor', [
|
206
|
+
['supported-collation-set', @carddav_ns]
|
207
|
+
])
|
208
|
+
|
209
|
+
elements = ensure_element_exists(response, 'D|supported-collation-set', 'D' => @carddav_ns)
|
210
|
+
elements[0].text.should == ""
|
211
|
+
end
|
212
|
+
|
213
|
+
should 'not be returned in allprop query' do
|
214
|
+
# should not be returned by all
|
215
|
+
response = propfind('/books/castor')
|
216
|
+
|
217
|
+
ensure_element_does_not_exists(response, 'D|supported-collation-set', 'D' => @carddav_ns)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it '[8.6] CARDDAV:addressbook-query Report' do
|
222
|
+
# unsupported for now
|
223
|
+
end
|
224
|
+
|
225
|
+
|
226
|
+
describe '[8.7] CARDDAV:addressbook-multiget Report' do
|
227
|
+
before do
|
228
|
+
@contact = FactoryGirl.build(:contact, uid: '1234-5678-9000-1')
|
229
|
+
@contact.stubs(:vcard).returns(@parsed_vcard)
|
230
|
+
|
231
|
+
|
232
|
+
@raw_query = <<-EOS
|
233
|
+
<?xml version="1.0" encoding="utf-8" ?>
|
234
|
+
<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
|
235
|
+
<D:prop>
|
236
|
+
<D:getetag/>
|
237
|
+
<C:address-data>
|
238
|
+
<C:prop name="VERSION"/>
|
239
|
+
<C:prop name="UID"/>
|
240
|
+
<C:prop name="NICKNAME"/>
|
241
|
+
<C:prop name="EMAIL"/>
|
242
|
+
<C:prop name="FN"/>
|
243
|
+
</C:address-data>
|
244
|
+
</D:prop>
|
245
|
+
<D:href>/books/castor/1234-5678-9000-1.vcf</D:href>
|
246
|
+
<D:href>/books/castor/1234-5678-9000-2.vcf</D:href>
|
247
|
+
</C:addressbook-multiget>
|
248
|
+
EOS
|
249
|
+
end
|
250
|
+
|
251
|
+
should 'return multiple cards' do
|
252
|
+
response = request(:report, "/books/castor", input: @raw_query, 'HTTP_DEPTH' => '0')
|
253
|
+
response.status.should == 207
|
254
|
+
|
255
|
+
# '*=' = include
|
256
|
+
ensure_element_exists(response, %{D|href[text()*="1234-5678-9000-2"] + D|status[text()*="404"]}, 'D' => @dav_ns)
|
257
|
+
|
258
|
+
|
259
|
+
vcard = ensure_element_exists(response, %{D|href[text()*="1234-5678-9000-1"] + D|propstat > D|prop > C|address-data}, 'D' => @dav_ns, 'C' => @carddav_ns)
|
260
|
+
vcard.text.should.include? <<-EOS
|
261
|
+
BEGIN:VCARD
|
262
|
+
VERSION:3.0
|
263
|
+
FN:Cyrus Daboo
|
264
|
+
EMAIL;TYPE=INTERNET,PREF:cyrus@example.com
|
265
|
+
NICKNAME:me
|
266
|
+
UID:1234-5678-9000-1
|
267
|
+
END:VCARD
|
268
|
+
EOS
|
269
|
+
end
|
270
|
+
|
271
|
+
should 'return an error with Depth != 0' do
|
272
|
+
response = request(:report, "/books/castor", input: @raw_query, 'HTTP_DEPTH' => '2')
|
273
|
+
response.status.should == 400
|
274
|
+
|
275
|
+
ensure_element_exists(response, 'D|error > D|invalid-depth', 'D' => @dav_ns)
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'eetee'
|
5
|
+
|
6
|
+
if ENV['COVERAGE']
|
7
|
+
|
8
|
+
require 'simplecov'
|
9
|
+
SimpleCov.start do
|
10
|
+
add_filter ".*_spec"
|
11
|
+
add_filter "/helpers/"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
$LOAD_PATH.unshift( File.expand_path('../../lib' , __FILE__) )
|
17
|
+
require 'dav4rack_ext'
|
18
|
+
require 'factory_girl'
|
19
|
+
|
20
|
+
|
21
|
+
require 'eetee/ext/mocha'
|
22
|
+
require 'eetee/ext/rack'
|
23
|
+
|
24
|
+
|
25
|
+
require_relative '../example/rack_sniffer'
|
26
|
+
require_relative 'support/models'
|
27
|
+
require_relative 'factories'
|
28
|
+
|
29
|
+
Thread.abort_on_exception = true
|
30
|
+
|
31
|
+
|
32
|
+
module Rack::Test
|
33
|
+
class Session
|
34
|
+
def propfind(uri, params = {}, env = {}, &block)
|
35
|
+
env = env_for(uri, env.merge(:method => "PROPFIND", :params => params))
|
36
|
+
process_request(uri, env, &block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'virtus'
|
2
|
+
|
3
|
+
|
4
|
+
module Testing
|
5
|
+
|
6
|
+
class DummyBase
|
7
|
+
include Virtus
|
8
|
+
|
9
|
+
attribute :updated_at, Time, default: Time.now
|
10
|
+
attribute :created_at, Time, default: Time.now
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
class Field < DummyBase
|
16
|
+
|
17
|
+
attribute :group, String
|
18
|
+
attribute :name, String
|
19
|
+
attribute :value, String
|
20
|
+
attribute :params, Hash, default: {}
|
21
|
+
|
22
|
+
|
23
|
+
def self.from_vcf_field(f)
|
24
|
+
new(group: f.group, name: f.name, value: f.value, params: f.params)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Contact < DummyBase
|
29
|
+
|
30
|
+
attribute :uid, String
|
31
|
+
attribute :fields, Array[Field], default: []
|
32
|
+
|
33
|
+
alias :path :uid
|
34
|
+
|
35
|
+
def update_from_vcard(vcf)
|
36
|
+
raise "invalid uid" unless vcf['UID'].value == uid
|
37
|
+
self.updated_at = Time.now
|
38
|
+
|
39
|
+
# TODO: addressbook rename fields
|
40
|
+
|
41
|
+
vcf.each_field do |a|
|
42
|
+
existing_field = fields.detect do |f|
|
43
|
+
(f.name == a.name) && (f.group == a.group) && (f.params == a.params)
|
44
|
+
end
|
45
|
+
|
46
|
+
if existing_field
|
47
|
+
puts "Updated '#{a.group}.#{a.name}' to '#{a.value}'"
|
48
|
+
existing_field.value = a.value
|
49
|
+
else
|
50
|
+
puts "Created '#{a.group}.#{a.name}' with '#{a.value}'"
|
51
|
+
fields << Field.from_vcf_field(a)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
def save
|
58
|
+
p fields
|
59
|
+
|
60
|
+
# no-op
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy
|
65
|
+
true
|
66
|
+
end
|
67
|
+
|
68
|
+
def etag
|
69
|
+
rand(1000).to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def vcard
|
73
|
+
vcard = VCardParser::VCard.new("3.0")
|
74
|
+
vcard.add_field('UID', uid)
|
75
|
+
|
76
|
+
fields.each do |f|
|
77
|
+
puts "[vCard] Adding field #{f.name} / #{f.value} / #{f.group} / #{f.params}"
|
78
|
+
vcard.add_field(f.name, f.value, f.group, f.params)
|
79
|
+
end
|
80
|
+
|
81
|
+
vcard
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
|
88
|
+
class AddressBook < DummyBase
|
89
|
+
|
90
|
+
attribute :name, String
|
91
|
+
attribute :path, String
|
92
|
+
attribute :contacts, Array[Contact], default: []
|
93
|
+
|
94
|
+
def find_contact(uid)
|
95
|
+
contacts.detect{|c| c.uid == uid.to_s }
|
96
|
+
end
|
97
|
+
|
98
|
+
def create_contact(uid)
|
99
|
+
Contact.new(uid: uid).tap do |c|
|
100
|
+
contacts << c
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def updated_at
|
105
|
+
Time.now.to_i
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
class User < DummyBase
|
112
|
+
|
113
|
+
attribute :login, String
|
114
|
+
attribute :addressbooks, Array[AddressBook], default: []
|
115
|
+
|
116
|
+
def initialize(env, *args)
|
117
|
+
super(*args)
|
118
|
+
@env = env
|
119
|
+
end
|
120
|
+
|
121
|
+
def all_addressbooks
|
122
|
+
# may filter with router_params, or not
|
123
|
+
addressbooks
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
def find_addressbook(params)
|
128
|
+
path = params[:book_id]
|
129
|
+
addressbooks.detect{|b| b.path == path }
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
def current_addressbook
|
134
|
+
path = router_params[:book_id]
|
135
|
+
addressbooks.detect{|b| b.path == path }
|
136
|
+
end
|
137
|
+
|
138
|
+
def current_contact
|
139
|
+
uid = router_params[:contact_id]
|
140
|
+
if uid && current_addressbook
|
141
|
+
current_addressbook.find_contact(uid)
|
142
|
+
else
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def contacts
|
148
|
+
current_addressbook.contacts
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
def router_params
|
153
|
+
@env['router.params'] || {}
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
|