dav4rack_ext 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+