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