cloudkit 0.10.1 → 0.11.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.
Files changed (58) hide show
  1. data/CHANGES +11 -0
  2. data/README +7 -6
  3. data/Rakefile +13 -6
  4. data/TODO +7 -5
  5. data/cloudkit.gemspec +23 -23
  6. data/doc/curl.html +2 -2
  7. data/doc/index.html +4 -6
  8. data/examples/5.ru +2 -3
  9. data/examples/TOC +1 -3
  10. data/lib/cloudkit.rb +17 -10
  11. data/lib/cloudkit/constants.rb +0 -6
  12. data/lib/cloudkit/exceptions.rb +10 -0
  13. data/lib/cloudkit/flash_session.rb +1 -3
  14. data/lib/cloudkit/oauth_filter.rb +8 -16
  15. data/lib/cloudkit/oauth_store.rb +9 -13
  16. data/lib/cloudkit/openid_filter.rb +25 -7
  17. data/lib/cloudkit/openid_store.rb +14 -17
  18. data/lib/cloudkit/request.rb +6 -1
  19. data/lib/cloudkit/service.rb +15 -15
  20. data/lib/cloudkit/store.rb +97 -284
  21. data/lib/cloudkit/store/memory_table.rb +105 -0
  22. data/lib/cloudkit/store/resource.rb +256 -0
  23. data/lib/cloudkit/store/response_helpers.rb +0 -1
  24. data/lib/cloudkit/uri.rb +88 -0
  25. data/lib/cloudkit/user_store.rb +7 -14
  26. data/spec/ext_spec.rb +76 -0
  27. data/spec/flash_session_spec.rb +20 -0
  28. data/spec/memory_table_spec.rb +86 -0
  29. data/spec/oauth_filter_spec.rb +326 -0
  30. data/spec/oauth_store_spec.rb +10 -0
  31. data/spec/openid_filter_spec.rb +64 -0
  32. data/spec/openid_store_spec.rb +101 -0
  33. data/spec/rack_builder_spec.rb +39 -0
  34. data/spec/request_spec.rb +185 -0
  35. data/spec/resource_spec.rb +291 -0
  36. data/spec/service_spec.rb +974 -0
  37. data/{test/helper.rb → spec/spec_helper.rb} +14 -2
  38. data/spec/store_spec.rb +10 -0
  39. data/spec/uri_spec.rb +93 -0
  40. data/spec/user_store_spec.rb +10 -0
  41. data/spec/util_spec.rb +11 -0
  42. metadata +37 -61
  43. data/examples/6.ru +0 -10
  44. data/lib/cloudkit/store/adapter.rb +0 -8
  45. data/lib/cloudkit/store/extraction_view.rb +0 -57
  46. data/lib/cloudkit/store/sql_adapter.rb +0 -36
  47. data/test/ext_test.rb +0 -76
  48. data/test/flash_session_test.rb +0 -22
  49. data/test/oauth_filter_test.rb +0 -331
  50. data/test/oauth_store_test.rb +0 -12
  51. data/test/openid_filter_test.rb +0 -60
  52. data/test/openid_store_test.rb +0 -12
  53. data/test/rack_builder_test.rb +0 -41
  54. data/test/request_test.rb +0 -197
  55. data/test/service_test.rb +0 -971
  56. data/test/store_test.rb +0 -93
  57. data/test/user_store_test.rb +0 -12
  58. data/test/util_test.rb +0 -13
@@ -0,0 +1,10 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "An OAuthStore" do
4
+
5
+ it "should know its version" do
6
+ store = CloudKit::UserStore.new
7
+ store.version.should == 1
8
+ end
9
+
10
+ end
@@ -0,0 +1,64 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "An OpenIDFilter" do
4
+
5
+ before(:each) do
6
+ openid_app = Rack::Builder.new {
7
+ use Rack::Lint
8
+ use Rack::Session::Pool
9
+ use CloudKit::OpenIDFilter, :allow => ['/foo']
10
+ run echo_env(CLOUDKIT_AUTH_KEY)
11
+ }
12
+ @request = Rack::MockRequest.new(openid_app)
13
+ end
14
+
15
+ it "should allow root url pass through" do
16
+ response = @request.get('/')
17
+ response.status.should == 200
18
+ end
19
+
20
+ it "should allow pass through of URIs defined in :allow" do
21
+ response = @request.get('/foo')
22
+ response.status.should == 200
23
+ end
24
+
25
+ it "should redirect to the login page if authorization is required" do
26
+ response = @request.get('/protected')
27
+ response.status.should == 302
28
+ response['Location'].should == '/login'
29
+ end
30
+
31
+ it "should notify downstream nodes of its presence" do
32
+ app = Rack::Builder.new do
33
+ use Rack::Session::Pool
34
+ use CloudKit::OpenIDFilter
35
+ run echo_env(CLOUDKIT_VIA)
36
+ end
37
+ response = Rack::MockRequest.new(app).get('/')
38
+ response.body.should == CLOUDKIT_OPENID_FILTER_KEY
39
+ end
40
+
41
+ describe "with upstream authorization middleware" do
42
+
43
+ it "should allow pass through if the auth env variable is populated" do
44
+ response = @request.get('/protected', VALID_TEST_AUTH)
45
+ response.status.should == 200
46
+ response.body.should == TEST_REMOTE_USER
47
+ end
48
+
49
+ it "should return the auth challenge header" do
50
+ response = @request.get('/protected',
51
+ CLOUDKIT_VIA => CLOUDKIT_OAUTH_FILTER_KEY,
52
+ CLOUDKIT_AUTH_CHALLENGE => {'WWW-Authenticate' => 'etc.'})
53
+ response['WWW-Authenticate'].should_not be_nil
54
+ end
55
+
56
+ it "should return a 401 status if authorization is required" do
57
+ response = @request.get('/protected',
58
+ CLOUDKIT_VIA => CLOUDKIT_OAUTH_FILTER_KEY,
59
+ CLOUDKIT_AUTH_CHALLENGE => {'WWW-Authenticate' => 'etc.'})
60
+ response.status.should == 401
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,101 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "An OpenIDStore" do
4
+
5
+ before(:each) do
6
+ CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
7
+ @store = CloudKit::OpenIDStore.new
8
+ @server = 'http://openid.claimid.com/server'
9
+ @handle = "{HMAC-SHA1}{736kwv3j}{wbhwEK==}"
10
+ @secret = "\350\068\753\436\567\8327\232\241\025\254\3117&\016\031\355#sV"
11
+ @issued = Time.now
12
+ @lifetime = 120960
13
+ @type = "HMAC-SHA1"
14
+ @association = OpenID::Association.new(@handle, @secret, @issued, @lifetime, @type)
15
+ @store.store_association(@server, @association)
16
+ @result = CloudKit::Resource.all.first.parsed_json
17
+ end
18
+
19
+ after(:each) do
20
+ CloudKit.storage_adapter.clear
21
+ end
22
+
23
+ it "should know its version" do
24
+ @store.version.should == 1
25
+ end
26
+
27
+ describe "when storing an association" do
28
+
29
+ it "should base64 encode the handle" do
30
+ @result['handle'].should == Base64.encode64(@handle)
31
+ end
32
+
33
+ it "should base64 encode the secret" do
34
+ @result['secret'].should == Base64.encode64(@secret)
35
+ end
36
+
37
+ it "should convert the issue time to an integer" do
38
+ @result['issued'].should == @issued.to_i
39
+ end
40
+
41
+ it "should store the lifetime as given" do
42
+ @result['lifetime'].should == @lifetime
43
+ end
44
+
45
+ it "should store the association type as given" do
46
+ @result['assoc_type'].should == @type
47
+ end
48
+
49
+ it "should remove previous associations with the given server_url and association handle" do
50
+ association = OpenID::Association.new(@handle, @secret, @issued, @lifetime, @type)
51
+ @store.store_association(@server, association)
52
+ associations = CloudKit::Resource.current(
53
+ :collection_reference => "/cloudkit_openid_associations")
54
+ associations.size.should == 1
55
+ result = associations.first.parsed_json
56
+ result['secret'].should == Base64.encode64(@secret)
57
+ end
58
+ end
59
+
60
+ describe "when removing an association" do
61
+
62
+ it "should succeed" do
63
+ @store.remove_association(@server, @association.handle)
64
+ associations = CloudKit::Resource.current(
65
+ :collection_reference => "/cloudkit_openid_associations")
66
+ associations.size.should == 0
67
+ end
68
+ end
69
+
70
+ describe "when finding an association" do
71
+
72
+ it "should return the correct object" do
73
+ association = @store.get_association(@server, @association.handle)
74
+ association.should == @association
75
+ end
76
+ end
77
+
78
+ describe "when using a nonce" do
79
+
80
+ before(:each) do
81
+ @time = Time.now.to_i
82
+ @salt = 'salt'
83
+ @store.use_nonce(@server, @time, @salt)
84
+ end
85
+
86
+ it "should store the nonce" do
87
+ nonce = CloudKit::Resource.first(
88
+ :collection_reference => "/cloudkit_openid_nonces",
89
+ :deleted => false)
90
+ nonce.should_not be_nil
91
+ end
92
+
93
+ it "should reject the nonce if it has already been used" do
94
+ @store.use_nonce(@server, @time, @salt).should_not be_nil
95
+ nonces = CloudKit::Resource.all(
96
+ :collection_reference => "/cloudkit_openid_nonces",
97
+ :deleted => false)
98
+ nonces.size.should == 1
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,39 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "Rack::Builder" do
4
+
5
+ it "should expose services" do
6
+ app = Rack::Builder.new do
7
+ expose :items, :things
8
+ run lambda {|app| [200, {}, ['hello']]}
9
+ end
10
+ response = Rack::MockRequest.new(app).get('/items')
11
+ response.status.should == 200
12
+ documents = JSON.parse(response.body)['uris']
13
+ documents.should == []
14
+ end
15
+
16
+ it "should expose services with auth using 'contain'" do
17
+ app = Rack::Builder.new do
18
+ contain :items, :things
19
+ run lambda {|app| [200, {}, ['hello']]}
20
+ end
21
+ response = Rack::MockRequest.new(app).get('/items')
22
+ response.status.should == 401
23
+ response = Rack::MockRequest.new(app).get('/things')
24
+ response.status.should == 401
25
+ response = Rack::MockRequest.new(app).get('/')
26
+ response.status.should == 200
27
+ response.body.should == 'hello'
28
+ end
29
+
30
+ it "should insert a default app if one does not exist" do
31
+ app = Rack::Builder.new { contain :items }
32
+ response = Rack::MockRequest.new(app).get('/items')
33
+ response.status.should == 401
34
+ response = Rack::MockRequest.new(app).get('/')
35
+ response.status.should == 200
36
+ response.body.match('CloudKit').should_not be_nil
37
+ end
38
+
39
+ end
@@ -0,0 +1,185 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "A Request" do
4
+
5
+ it "should match requests with routes" do
6
+ CloudKit::Request.new(Rack::MockRequest.env_for(
7
+ 'http://example.com')).match?('GET', '/').should be_true
8
+ CloudKit::Request.new(Rack::MockRequest.env_for(
9
+ 'http://example.com/')).match?('GET', '/').should be_true
10
+ CloudKit::Request.new(Rack::MockRequest.env_for(
11
+ 'http://example.com/')).match?('POST', '/').should_not be_true
12
+ CloudKit::Request.new(Rack::MockRequest.env_for(
13
+ 'http://example.com/hello')).match?('GET', '/hello').should be_true
14
+ CloudKit::Request.new(Rack::MockRequest.env_for(
15
+ 'http://example.com/hello')).match?('GET', '/hello').should be_true
16
+ CloudKit::Request.new(Rack::MockRequest.env_for(
17
+ 'http://example.com/hello', :method => 'POST')).match?(
18
+ 'POST', '/hello').should be_true
19
+ CloudKit::Request.new(Rack::MockRequest.env_for(
20
+ 'http://example.com/hello?q=a', :method => 'POST')).match?(
21
+ 'POST', '/hello', [{'q' => 'a'}]).should be_true
22
+ CloudKit::Request.new(Rack::MockRequest.env_for(
23
+ 'http://example.com/hello?q=a', :method => 'POST')).match?(
24
+ 'POST', '/hello', ['q']).should be_true
25
+ CloudKit::Request.new(Rack::MockRequest.env_for(
26
+ 'http://example.com/hello?q=a', :method => 'POST')).match?(
27
+ 'POST', '/hello', [{'q' => 'b'}]).should_not be_true
28
+ CloudKit::Request.new(Rack::MockRequest.env_for(
29
+ 'http://example.com/hello?q', :method => 'POST')).match?(
30
+ 'POST', '/hello', [{'q' => nil}]).should be_true
31
+ CloudKit::Request.new(Rack::MockRequest.env_for(
32
+ 'http://example.com/hello?q=a', :method => 'POST')).match?(
33
+ 'POST', '/hello', [{'q' => nil}]).should_not be_true
34
+ CloudKit::Request.new(Rack::MockRequest.env_for(
35
+ 'http://example.com/hello?q=a', :method => 'POST')).match?(
36
+ 'POST', '/hello', [{'q' => ''}]).should_not be_true
37
+ CloudKit::Request.new(Rack::MockRequest.env_for(
38
+ 'http://example.com/hello?q&x=y', :method => 'PUT')).match?(
39
+ 'PUT', '/hello', ['q', {'x' => 'y'}]).should be_true
40
+ CloudKit::Request.new(Rack::MockRequest.env_for(
41
+ 'http://example.com/hello?q&x=y&z', :method => 'PUT')).match?(
42
+ 'PUT', '/hello', ['q', {'x' => 'y'}]).should be_true
43
+ CloudKit::Request.new(Rack::MockRequest.env_for(
44
+ 'http://example.com/hello?q&x=y', :method => 'PUT')).match?(
45
+ 'PUT', '/hello', [{'q' => 'a'},{'x' => 'y'}]).should_not be_true
46
+ end
47
+
48
+ it "should treat a trailing :id as a wildcard for path matching" do
49
+ CloudKit::Request.new(Rack::MockRequest.env_for(
50
+ 'http://example.com/hello/123')).match?('GET', '/hello/:id').should be_true
51
+ end
52
+
53
+ it "should inject stack-internal via-style env vars" do
54
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/test'))
55
+ request.via.should == []
56
+ request.inject_via('a.b')
57
+ request.via.include?('a.b').should be_true
58
+ request.inject_via('c.d')
59
+ request.via.include?('a.b').should be_true
60
+ request.via.include?('c.d').should be_true
61
+ end
62
+
63
+ it "should announce the use of auth middleware" do
64
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
65
+ request.announce_auth(CLOUDKIT_OAUTH_FILTER_KEY)
66
+ request.via.include?(CLOUDKIT_OAUTH_FILTER_KEY).should be_true
67
+ end
68
+
69
+ it "should know if auth provided by upstream middleware" do
70
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
71
+ request.announce_auth(CLOUDKIT_OAUTH_FILTER_KEY)
72
+ request.using_auth?.should be_true
73
+ end
74
+
75
+ it "should know the current user" do
76
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
77
+ request.current_user.should be_nil
78
+ request = CloudKit::Request.new(
79
+ Rack::MockRequest.env_for('/', CLOUDKIT_AUTH_KEY => 'cecil'))
80
+ request.current_user.should_not be_nil
81
+ request.current_user.should == 'cecil'
82
+ end
83
+
84
+ it "should set the current user" do
85
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
86
+ request.current_user = 'cecil'
87
+ request.current_user.should_not be_nil
88
+ request.current_user.should == 'cecil'
89
+ end
90
+
91
+ it "should know the login url" do
92
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
93
+ request.login_url.should == '/login'
94
+ request = CloudKit::Request.new(
95
+ Rack::MockRequest.env_for('/', CLOUDKIT_LOGIN_URL => '/sessions'))
96
+ request.login_url.should == '/sessions'
97
+ end
98
+
99
+ it "should set the login url" do
100
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
101
+ request.login_url = '/welcome'
102
+ request.login_url.should == '/welcome'
103
+ end
104
+
105
+ it "should know the logout url" do
106
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
107
+ request.logout_url.should == '/logout'
108
+ request = CloudKit::Request.new(
109
+ Rack::MockRequest.env_for('/', CLOUDKIT_LOGOUT_URL => '/sessions'))
110
+ request.logout_url.should == '/sessions'
111
+ end
112
+
113
+ it "should set the logout url" do
114
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
115
+ request.logout_url = '/goodbye'
116
+ request.logout_url.should == '/goodbye'
117
+ end
118
+
119
+ it "should get the session" do
120
+ request = CloudKit::Request.new(
121
+ Rack::MockRequest.env_for('/', 'rack.session' => 'this'))
122
+ request.session.should_not be_nil
123
+ request.session.should == 'this'
124
+ end
125
+
126
+ it "should know the flash" do
127
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
128
+ '/', 'rack.session' => {}))
129
+ request.flash.is_a?(CloudKit::FlashSession).should be_true
130
+ end
131
+
132
+ it "should parse if-match headers" do
133
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
134
+ '/items/123/versions'))
135
+ request.if_match.should be_nil
136
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
137
+ '/items/123/versions', 'HTTP_IF_MATCH' => '"a"'))
138
+ request.if_match.should == 'a'
139
+ end
140
+
141
+ it "should treat a list of etags in an if-match header as a single etag" do
142
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
143
+ '/items/123/versions', 'HTTP_IF_MATCH' => '"a", "b"'))
144
+ # See CloudKit::Request#if_match for more info on this expectation
145
+ request.if_match.should == 'a", "b'
146
+ end
147
+
148
+ it "should ignore if-match when set to *" do
149
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
150
+ '/items/123/versions', 'HTTP_IF_MATCH' => '*'))
151
+ request.if_match.should be_nil
152
+ end
153
+
154
+ it "should understand header auth" do
155
+ request = CloudKit::Request.new(Rack::MockRequest.env_for(
156
+ 'http://photos.example.net/photos?file=vacation.jpg&size=original',
157
+ 'Authorization' =>
158
+ 'OAuth realm="",' +
159
+ 'oauth_version="1.0",' +
160
+ 'oauth_consumer_key="dpf43f3p2l4k3l03",' +
161
+ 'oauth_token="nnch734d00sl2jdk",' +
162
+ 'oauth_timestamp="1191242096",' +
163
+ 'oauth_nonce="kllo9940pd9333jh",' +
164
+ 'oauth_signature="tR3%2BTy81lMeYAr%2FFid0kMTYa%2FWM%3D",' +
165
+ 'oauth_signature_method="HMAC-SHA1"'))
166
+ request['oauth_consumer_key'].should == 'dpf43f3p2l4k3l03'
167
+ request['oauth_token'].should == 'nnch734d00sl2jdk'
168
+ request['oauth_timestamp'].should == '1191242096'
169
+ request['oauth_nonce'].should == 'kllo9940pd9333jh'
170
+ request['oauth_signature'].should == 'tR3+Ty81lMeYAr/Fid0kMTYa/WM='
171
+ request['oauth_signature_method'].should == 'HMAC-SHA1'
172
+ end
173
+
174
+ it "should know the last path element" do
175
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/'))
176
+ request.last_path_element.should be_nil
177
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/abc'))
178
+ request.last_path_element.should == 'abc'
179
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/abc/'))
180
+ request.last_path_element.should == 'abc'
181
+ request = CloudKit::Request.new(Rack::MockRequest.env_for('/abc/def'))
182
+ request.last_path_element.should == 'def'
183
+ end
184
+
185
+ end
@@ -0,0 +1,291 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "A Resource" do
4
+
5
+ before(:all) do
6
+ CloudKit.setup_storage_adapter unless CloudKit.storage_adapter
7
+ end
8
+
9
+ after(:each) do
10
+ CloudKit.storage_adapter.clear
11
+ end
12
+
13
+ describe "on initialization" do
14
+
15
+ before(:each) do
16
+ @resource = CloudKit::Resource.new(
17
+ CloudKit::URI.new('/items/123'),
18
+ JSON.generate({:foo => 'bar'}),
19
+ 'http://eric.dolphy.info')
20
+ end
21
+
22
+ it "should know its uri" do
23
+ @resource.uri.string.should == CloudKit::URI.new('/items/123').string
24
+ end
25
+
26
+ it "should know its json" do
27
+ @resource.json.should == "{\"foo\":\"bar\"}"
28
+ end
29
+
30
+ it "should know its remote user" do
31
+ @resource.remote_user.should == 'http://eric.dolphy.info'
32
+ end
33
+
34
+ it "should default its deleted status to false" do
35
+ @resource.should_not be_deleted
36
+ end
37
+
38
+ it "should default its archived status to false" do
39
+ @resource.should_not be_archived
40
+ end
41
+
42
+ it "should default its etag to nil" do
43
+ @resource.etag.should be_nil
44
+ end
45
+
46
+ it "should default its last-modified date to nil" do
47
+ @resource.last_modified.should be_nil
48
+ end
49
+
50
+ it "should know if it is current" do
51
+ @resource.should be_current
52
+ end
53
+
54
+ end
55
+
56
+ describe "on save" do
57
+
58
+ before(:each) do
59
+ @resource = CloudKit::Resource.new(
60
+ CloudKit::URI.new('/items/123'),
61
+ JSON.generate({:foo => 'bar'}),
62
+ 'http://eric.dolphy.info')
63
+ @resource.save
64
+ end
65
+
66
+ it "should set its etag" do
67
+ @resource.etag.should_not be_nil
68
+ end
69
+
70
+ it "should set its last modified date" do
71
+ @resource.last_modified.should_not be_nil
72
+ end
73
+
74
+ it "should adjust its URI when adding to a resource collection" do
75
+ resource = CloudKit::Resource.new(
76
+ CloudKit::URI.new('/items'),
77
+ JSON.generate({:foo => 'bar'}),
78
+ 'http://eric.dolphy.info')
79
+ resource.save
80
+ resource.uri.string.should_not == '/items'
81
+ end
82
+
83
+ it "should flatten its json structure for querying" do
84
+ hash = CloudKit.storage_adapter.query.first
85
+ hash.keys.include?('foo').should be_true
86
+ end
87
+
88
+ it "should know it is current" do
89
+ @resource.should be_current
90
+ end
91
+
92
+ end
93
+
94
+ describe "on create" do
95
+
96
+ before(:each) do
97
+ resource = CloudKit::Resource.create(
98
+ CloudKit::URI.new('/items/123'),
99
+ JSON.generate({:foo => 'bar'}),
100
+ 'http://eric.dolphy.info')
101
+ @result = CloudKit.storage_adapter.query { |q|
102
+ q.add_condition 'uri', :eql, '/items/123'
103
+ }
104
+ end
105
+
106
+ it "should save the resource" do
107
+ @result.size.should == 1
108
+ @result.first['json'].should == "{\"foo\":\"bar\"}"
109
+ end
110
+
111
+ end
112
+
113
+ describe "on update" do
114
+
115
+ before(:each) do
116
+ @resource = CloudKit::Resource.create(
117
+ CloudKit::URI.new('/items/123'),
118
+ JSON.generate({:foo => 'bar'}),
119
+ 'http://eric.dolphy.info')
120
+ @original_resource = @resource.dup
121
+ now = Time.now
122
+ Time.stub!(:now).and_return(now+1)
123
+ @resource.update(JSON.generate({:foo => 'baz'}))
124
+ end
125
+
126
+ it "should version the resource" do
127
+ @resource.versions.size.should == 2
128
+ @resource.versions[-1].should be_archived
129
+ end
130
+
131
+ it "should set a new etag" do
132
+ @resource.etag.should_not == @original_resource.etag
133
+ end
134
+
135
+ it "should set a new last modified date" do
136
+ @resource.last_modified.should_not == @original_resource.last_modified
137
+ end
138
+
139
+ it "should fail on archived resource versions" do
140
+ lambda {
141
+ @resource.versions[-1].update({:foo => 'box'})
142
+ }.should raise_error(CloudKit::HistoricalIntegrityViolation)
143
+ end
144
+
145
+ it "should fail on deleted resource versions" do
146
+ lambda {
147
+ @resource.delete
148
+ @resource.update({:foo => 'box'})
149
+ }.should raise_error(CloudKit::HistoricalIntegrityViolation)
150
+ end
151
+
152
+ end
153
+
154
+ describe "on delete" do
155
+
156
+ before(:each) do
157
+ @resource = CloudKit::Resource.create(
158
+ CloudKit::URI.new('/items/123'),
159
+ JSON.generate({:foo => 'bar'}),
160
+ 'http://eric.dolphy.info')
161
+ now = Time.now
162
+ Time.stub!(:now).and_return(now+1)
163
+ @resource.delete
164
+ end
165
+
166
+ it "should version the resource" do
167
+ @resource.versions.size.should == 2
168
+ @resource.versions[-1].should be_archived
169
+ end
170
+
171
+ it "should set the etag on the main resource to nil" do
172
+ @resource.etag.should be_nil
173
+ end
174
+
175
+ it "should know it has been deleted" do
176
+ @resource.deleted?.should be_true
177
+ end
178
+
179
+ it "should fail on archived resource versions" do
180
+ lambda {
181
+ @resource.versions[-1].update({:foo => 'box'})
182
+ }.should raise_error(CloudKit::HistoricalIntegrityViolation)
183
+ end
184
+
185
+ end
186
+
187
+ describe "with versions" do
188
+
189
+ before(:each) do
190
+ @resource = CloudKit::Resource.create(
191
+ CloudKit::URI.new('/items/123'),
192
+ JSON.generate({:foo => 'bar'}),
193
+ 'http://eric.dolphy.info')
194
+ @resource_list = [@resource.dup]
195
+
196
+ 2.times { |i|
197
+ now = Time.now
198
+ Time.stub!(:now).and_return(now+1)
199
+ @resource.update(JSON.generate({:foo => i}))
200
+ @resource_list << @resource.dup
201
+ }
202
+ @resource_list.reverse!
203
+ end
204
+
205
+ it "should keep an ordered list of versions" do
206
+ @resource.versions.map { |version| version.last_modified }.
207
+ should == @resource_list.map { |version| version.last_modified }
208
+ end
209
+
210
+ it "should include the current version in the version list" do
211
+ current_ts = @resource.last_modified
212
+ @resource_list.map { |version| version.last_modified }.should include(current_ts)
213
+ end
214
+
215
+ it "should know its previous version" do
216
+ @resource.previous_version.last_modified.should == @resource_list[1].last_modified
217
+ end
218
+
219
+ it "should know its previous versions" do
220
+ expected_times = @resource_list[1..-1].map { |version| version.last_modified }
221
+ @resource.previous_versions.map { |version| version.last_modified }.should == expected_times
222
+ end
223
+
224
+ end
225
+
226
+ describe "when finding" do
227
+
228
+ before(:each) do
229
+ ['bar', 'baz'].each do |value|
230
+ CloudKit::Resource.create(
231
+ CloudKit::URI.new('/items'),
232
+ JSON.generate({:foo => value}),
233
+ "http://eric.dolphy.info/#{value}_user")
234
+ end
235
+ CloudKit::Resource.create(
236
+ CloudKit::URI.new('/items'),
237
+ JSON.generate({:foo => 'box'}),
238
+ "http://eric.dolphy.info/bar_user")
239
+ end
240
+
241
+ describe "using #all" do
242
+
243
+ it "should find matching resources" do
244
+ result = CloudKit::Resource.all(
245
+ :remote_user => 'http://eric.dolphy.info/bar_user')
246
+ result.size.should == 2
247
+ result.map { |item| item.remote_user.should == 'http://eric.dolphy.info/bar_user' }
248
+ end
249
+
250
+ it "should return all elements if no restrictions are given" do
251
+ CloudKit::Resource.all.size.should == 3
252
+ end
253
+
254
+ it "should return an empty array if no resources are found" do
255
+ CloudKit::Resource.all(:uri => 'fail').should be_empty
256
+ end
257
+
258
+ it "should find with query parameters referencing JSON elements" do
259
+ resources = CloudKit::Resource.all(
260
+ :collection_reference => '/items',
261
+ :foo => 'bar')
262
+ resources.size.should == 1
263
+ resources.first.json.should == "{\"foo\":\"bar\"}"
264
+ end
265
+
266
+ end
267
+
268
+ describe "on #first" do
269
+
270
+ it "should find the first matching resource" do
271
+ result = CloudKit::Resource.first(:remote_user => 'http://eric.dolphy.info/bar_user')
272
+ result.should_not === Array
273
+ result.remote_user.should == 'http://eric.dolphy.info/bar_user'
274
+ result.parsed_json['foo'].should == 'box' # all listings are reverse ordered
275
+ end
276
+
277
+ end
278
+
279
+ describe "on #current" do
280
+
281
+ it "should find only current matching resources" do
282
+ resource = CloudKit::Resource.first(:remote_user => 'http://eric.dolphy.info/bar_user')
283
+ resource.update(JSON.generate({:foo => 'x'}))
284
+ CloudKit::Resource.current(:collection_reference => '/items').size.should == 3
285
+ end
286
+
287
+ end
288
+
289
+ end
290
+
291
+ end