cloudkit 0.10.1 → 0.11.0

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