sunstone 5.0.0.beta3 → 5.0.0.1

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.tm_properties +1 -0
  4. data/.travis.yml +36 -0
  5. data/README.md +1 -2
  6. data/Rakefile.rb +1 -1
  7. data/ext/active_record/associations/collection_association.rb +48 -6
  8. data/ext/active_record/attribute_methods.rb +25 -21
  9. data/ext/active_record/callbacks.rb +17 -0
  10. data/ext/active_record/finder_methods.rb +44 -2
  11. data/ext/active_record/persistence.rb +127 -1
  12. data/ext/active_record/relation.rb +13 -5
  13. data/ext/active_record/relation/calculations.rb +25 -0
  14. data/ext/active_record/statement_cache.rb +3 -2
  15. data/ext/active_record/transactions.rb +60 -0
  16. data/ext/arel/attributes/empty_relation.rb +31 -0
  17. data/ext/arel/attributes/relation.rb +3 -2
  18. data/lib/active_record/connection_adapters/sunstone/database_statements.rb +13 -2
  19. data/lib/active_record/connection_adapters/sunstone/schema_dumper.rb +16 -0
  20. data/lib/active_record/connection_adapters/sunstone/schema_statements.rb +2 -2
  21. data/lib/active_record/connection_adapters/sunstone/type/uuid.rb +21 -0
  22. data/lib/active_record/connection_adapters/sunstone_adapter.rb +54 -30
  23. data/lib/arel/collectors/sunstone.rb +6 -4
  24. data/lib/arel/visitors/sunstone.rb +61 -39
  25. data/lib/sunstone.rb +18 -11
  26. data/lib/sunstone/connection.rb +62 -22
  27. data/lib/sunstone/exception.rb +3 -0
  28. data/lib/sunstone/gis.rb +1 -0
  29. data/lib/sunstone/version.rb +2 -2
  30. data/sunstone.gemspec +4 -5
  31. data/test/active_record/associations/has_and_belongs_to_many_test.rb +12 -0
  32. data/test/active_record/associations/has_many_test.rb +72 -0
  33. data/test/active_record/eager_loading_test.rb +15 -0
  34. data/test/active_record/persistance_test.rb +190 -0
  35. data/test/active_record/preload_test.rb +16 -0
  36. data/test/active_record/query_test.rb +91 -0
  37. data/test/models.rb +91 -0
  38. data/test/sunstone/connection/configuration_test.rb +44 -0
  39. data/test/sunstone/connection/cookie_store_test.rb +37 -0
  40. data/test/sunstone/connection/request_helper_test.rb +105 -0
  41. data/test/sunstone/connection/send_request_test.rb +164 -0
  42. data/test/sunstone/connection_test.rb +2 -298
  43. data/test/test_helper.rb +45 -2
  44. metadata +52 -47
  45. data/ext/active_record/associations/builder/has_and_belongs_to_many.rb +0 -48
  46. data/ext/active_record/calculations.rb +0 -32
  47. data/ext/active_record/query_methods.rb +0 -30
  48. data/ext/active_record/relation/predicate_builder.rb +0 -23
  49. data/test/models/ship.rb +0 -14
  50. data/test/query_test.rb +0 -134
  51. data/test/sunstone/parser_test.rb +0 -124
  52. data/test/sunstone_test.rb +0 -303
@@ -1,33 +1,40 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
4
+
5
+ require 'json'
1
6
  require 'msgpack'
2
- require 'wankel'
3
- require 'cookie_store'
4
- require 'active_record'
7
+ require 'cookie_store' # optional
8
+
9
+ require "active_record"
5
10
 
11
+ # Adapter
6
12
  require File.expand_path(File.join(__FILE__, '../sunstone/version'))
7
- require File.expand_path(File.join(__FILE__, '../sunstone/connection'))
8
13
  require File.expand_path(File.join(__FILE__, '../sunstone/exception'))
14
+ require File.expand_path(File.join(__FILE__, '../sunstone/connection'))
15
+ require File.expand_path(File.join(__FILE__, '../active_record/connection_adapters/sunstone_adapter'))
16
+ require File.expand_path(File.join(__FILE__, '../active_record/connection_adapters/sunstone/type_metadata'))
9
17
 
18
+ # Arel Adapters
10
19
  require File.expand_path(File.join(__FILE__, '../arel/visitors/sunstone'))
11
20
  require File.expand_path(File.join(__FILE__, '../arel/collectors/sunstone'))
12
21
 
13
- require File.expand_path(File.join(__FILE__, '../active_record/connection_adapters/sunstone_adapter'))
14
- require File.expand_path(File.join(__FILE__, '../active_record/connection_adapters/sunstone/type_metadata'))
15
-
22
+ # ActiveRecord Extensions
16
23
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/statement_cache'))
17
24
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation'))
25
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/calculations'))
18
26
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/persistence'))
27
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/callbacks'))
19
28
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/attribute_methods'))
20
- # require File.expand_path(File.join(__FILE__, '../../ext/active_record/relation/predicate_builder'))
21
- # require File.expand_path(File.join(__FILE__, '../../ext/active_record/calculations'))
22
- require File.expand_path(File.join(__FILE__, '../../ext/active_record/query_methods'))
29
+ require File.expand_path(File.join(__FILE__, '../../ext/active_record/transactions'))
23
30
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/associations/collection_association'))
24
- # require File.expand_path(File.join(__FILE__, '../../ext/active_record/associations/builder/has_and_belongs_to_many'))
25
31
 
26
32
  require File.expand_path(File.join(__FILE__, '../../ext/active_support/core_ext/object/to_query'))
27
33
 
28
34
  require File.expand_path(File.join(__FILE__, '../../ext/arel/select_manager'))
29
35
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/eager_load'))
30
36
  require File.expand_path(File.join(__FILE__, '../../ext/arel/attributes/relation'))
37
+ require File.expand_path(File.join(__FILE__, '../../ext/arel/attributes/empty_relation'))
31
38
  require File.expand_path(File.join(__FILE__, '../../ext/arel/nodes/select_statement'))
32
39
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/finder_methods'))
33
40
  require File.expand_path(File.join(__FILE__, '../../ext/active_record/batches'))
@@ -1,29 +1,26 @@
1
- require 'uri'
2
- require 'net/http'
3
- require 'net/https'
4
-
5
1
  # _Sunstone_ is a low-level API. It provides basic HTTP #get, #post, #put, and
6
2
  # #delete calls to the Sunstone Server. It can also provides basic error
7
3
  # checking of responses.
8
4
  module Sunstone
9
5
  class Connection
10
6
 
11
- # Set the User-Agent of the client. Will be joined with other User-Agent info
12
7
  attr_reader :api_key, :host, :port, :use_ssl
13
8
 
14
9
  # Initialize a connection a Sunstone API server.
15
10
  #
16
11
  # Options:
17
12
  #
18
- # * <tt>:site</tt> - An optional url used to set the protocol, host, port,
13
+ # * <tt>:url</tt> - An optional url used to set the protocol, host, port,
19
14
  # and api_key
20
15
  # * <tt>:host</tt> - The default is to connect to 127.0.0.1.
21
16
  # * <tt>:port</tt> - Defaults to 80.
22
17
  # * <tt>:use_ssl</tt> - Defaults to false.
23
18
  # * <tt>:api_key</tt> - An optional token to send in the `Api-Key` header
19
+ # * <tt>:user_agent</tt> - An optional string. Will be joined with other
20
+ # User-Agent info.
24
21
  def initialize(config)
25
- if config[:site]
26
- uri = URI.parse(config.delete(:site))
22
+ if config[:url]
23
+ uri = URI.parse(config.delete(:url))
27
24
  config[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
28
25
  config[:host] ||= uri.host
29
26
  config[:port] ||= uri.port
@@ -66,6 +63,10 @@ module Sunstone
66
63
  RUBY_PLATFORM
67
64
  ].compact.join(' ')
68
65
  end
66
+
67
+ def url(path=nil)
68
+ "http#{use_ssl ? 's' : ''}://#{host}#{port != 80 ? (port == 443 && use_ssl ? '' : ":#{port}") : ''}#{path}"
69
+ end
69
70
 
70
71
  # Sends a Net::HTTPRequest to the server. The headers returned from
71
72
  # Sunestone#headers are automatically added to the request. The appropriate
@@ -103,7 +104,33 @@ module Sunstone
103
104
  # end
104
105
  # end
105
106
  def send_request(request, body=nil, &block)
106
- request_uri = "http#{use_ssl ? 's' : ''}://#{host}#{port != 80 ? (port == 443 && use_ssl ? '' : ":#{port}") : ''}#{request.path}"
107
+ if request.method != 'GET' && Thread.current[:sunstone_transaction_count]
108
+ if Thread.current[:sunstone_transaction_count] == 1 && !Thread.current[:sunstone_request_sent]
109
+ Thread.current[:sunstone_request_sent] = request
110
+ elsif Thread.current[:sunstone_request_sent]
111
+ log_mess = request.path.split('?', 2)
112
+ log_mess += Thread.current[:sunstone_request_sent].path.split('?', 2)
113
+ raise <<~MSG
114
+ Cannot send multiple request in a transaction.
115
+
116
+ Trying to send:
117
+ #{request.method} #{log_mess[0]} #{(log_mess[1] && !log_mess[1].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[1])) : '' }
118
+
119
+ Already sent:
120
+ #{Thread.current[:sunstone_request_sent].method} #{log_mess[2]} #{(log_mess[3] && !log_mess[3].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[3])) : '' }
121
+ MSG
122
+ else
123
+ log_mess = request.path.split('?', 2)
124
+ raise <<~MSG
125
+ Cannot send multiple request in a transaction.
126
+
127
+ Trying to send:
128
+ #{request.method} #{log_mess[0]} #{(log_mess[1] && !log_mess[1].empty?) ? MessagePack.unpack(CGI.unescape(log_mess[1])) : '' }
129
+ MSG
130
+ end
131
+ end
132
+
133
+ request_uri = url(request.path)
107
134
  request_headers.each { |k, v| request[k] = v }
108
135
  request['Content-Type'] ||= 'application/json'
109
136
 
@@ -117,15 +144,15 @@ module Sunstone
117
144
  elsif body.is_a?(String)
118
145
  request.body = body
119
146
  elsif body
120
- request.body = Wankel.encode(body)
147
+ request.body = JSON.generate(body)
121
148
  end
122
149
 
123
150
  return_value = nil
124
151
  retry_count = 0
125
152
  begin
126
153
  @connection.request(request) do |response|
127
- if response['API-Version-Deprecated']
128
- logger.warn("DEPRECATION WARNING: API v#{API_VERSION} is being phased out")
154
+ if response['Deprecation-Notice']
155
+ ActiveSupport::Deprecation.warn(response['Deprecation-Notice'])
129
156
  end
130
157
 
131
158
  validate_response_code(response)
@@ -285,7 +312,7 @@ module Sunstone
285
312
  end
286
313
 
287
314
  def server_config
288
- @server_config ||= Wankel.parse(get('/config').body, :symbolize_keys => true)
315
+ @server_config ||= JSON.parse(get('/config').body, symbolize_names: true)
289
316
  end
290
317
 
291
318
  private
@@ -329,26 +356,39 @@ module Sunstone
329
356
  when 400
330
357
  raise Sunstone::Exception::BadRequest, response.body
331
358
  when 401
332
- raise Sunstone::Exception::Unauthorized, response
359
+ raise Sunstone::Exception::Unauthorized, response.body
360
+ when 403
361
+ raise Sunstone::Exception::Forbidden, response.body
333
362
  when 404
334
- raise Sunstone::Exception::NotFound, response
363
+ raise Sunstone::Exception::NotFound, response.body
335
364
  when 410
336
- raise Sunstone::Exception::Gone, response
365
+ raise Sunstone::Exception::Gone, response.body
337
366
  when 422
338
- raise Sunstone::Exception::ApiVersionUnsupported, response
367
+ raise Sunstone::Exception::ApiVersionUnsupported, response.body
339
368
  when 503
340
- raise Sunstone::Exception::ServiceUnavailable, response
369
+ raise Sunstone::Exception::ServiceUnavailable, response.body
341
370
  when 301
342
- raise Sunstone::Exception::MovedPermanently, response
371
+ raise Sunstone::Exception::MovedPermanently, response.body
343
372
  when 502
344
- raise ActiveRecord::ConnectionNotEstablished, response
373
+ raise ActiveRecord::ConnectionNotEstablished, response.body
345
374
  when 300..599
346
- raise Sunstone::Exception, response
375
+ raise Sunstone::Exception, response.body
347
376
  else
348
- raise Sunstone::Exception, response
377
+ raise Sunstone::Exception, response.body
349
378
  end
350
379
  end
351
380
  end
381
+
382
+ def self.use_cookie_store(store)
383
+ Thread.current[:sunstone_cookie_store] = store
384
+ end
385
+
386
+ def self.with_cookie_store(store)
387
+ Thread.current[:sunstone_cookie_store] = store
388
+ yield
389
+ ensure
390
+ Thread.current[:sunstone_cookie_store] = nil
391
+ end
352
392
 
353
393
  end
354
394
  end
@@ -11,6 +11,9 @@ module Sunstone
11
11
  class Unauthorized < Sunstone::Exception
12
12
  end
13
13
 
14
+ class Forbidden < Sunstone::Exception
15
+ end
16
+
14
17
  class NotFound < Sunstone::Exception
15
18
  end
16
19
 
@@ -0,0 +1 @@
1
+ require 'active_record/connection_adapters/sunstone/type/ewkb'
@@ -1,3 +1,3 @@
1
1
  module Sunstone
2
- VERSION = '5.0.0.beta3'
3
- end
2
+ VERSION = '5.0.0.1'
3
+ end
@@ -26,13 +26,12 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'factory_girl'
27
27
  s.add_development_dependency 'webmock'
28
28
  s.add_development_dependency 'sdoc-templates-42floors'
29
+ s.add_development_dependency 'rgeo'
30
+ s.add_development_dependency 'simplecov'
29
31
 
30
32
  # Runtime
31
33
  s.add_runtime_dependency 'msgpack'
32
- s.add_runtime_dependency 'wankel'
33
34
  s.add_runtime_dependency 'cookie_store'
34
35
  s.add_runtime_dependency 'arel', '~> 7.0'
35
- s.add_runtime_dependency 'activesupport', '>= 5.0.0.beta3'
36
- s.add_runtime_dependency 'activemodel', '>= 5.0.0.beta3'
37
- s.add_runtime_dependency 'activerecord', '>= 5.0.0.beta3'
38
- end
36
+ s.add_runtime_dependency 'activerecord', '5.0.0.1'
37
+ end
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveRecord::Associations::HasAndBelongsToManyTest < Minitest::Test
4
+
5
+ test '#relation_ids' do
6
+ webmock(:get, "/ships", where: {id: 42}, limit: 1).to_return(body: [{id: 42, name: "The Niña"}].to_json)
7
+ webmock(:get, "/sailors", where: {sailors_ships: {ship_id: {eq: 42}}}).to_return(body: [{id: 43, name: "Chris"}].to_json)
8
+
9
+ assert_equal [43], Ship.find(42).sailor_ids
10
+ end
11
+
12
+ end
@@ -0,0 +1,72 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveRecord::Associations::HasManyTest < Minitest::Test
4
+
5
+ test '#create with has_many_ids=' do
6
+ webmock(:get, "/ships", where: {id: 2}).to_return(body: [{id: 2, fleet_id: nil, name: 'Duo'}].to_json)
7
+ webmock(:post, "/fleets").with(
8
+ body: {
9
+ fleet: {
10
+ name: 'Spanish Armada',
11
+ ships_attributes: [{id: 2, name: 'Duo'}]
12
+ }
13
+ }
14
+ ).to_return(body: {id: 42, name: "Spanish Armada"}.to_json)
15
+
16
+ Fleet.create(name: 'Spanish Armada', ship_ids: [2])
17
+ end
18
+
19
+
20
+ test '#update with has_many_ids=' do
21
+ webmock(:get, "/fleets", where: {id: 42}, limit: 1).to_return(body: [{id: 42, name: "Spanish Armada"}].to_json)
22
+ webmock(:get, "/ships", where: {fleet_id: 42}).to_return(body: [].to_json)
23
+ webmock(:get, "/ships", where: {id: 2}).to_return(body: [{id: 2, fleet_id: nil, name: 'Duo'}].to_json)
24
+
25
+ webmock(:patch, "/fleets/42").with(
26
+ body: {
27
+ fleet: {
28
+ ships_attributes: [{id: 2, name: 'Duo'}]
29
+ }
30
+ }).to_return(body: {id: 42, name: "Spanish Armada"}.to_json)
31
+
32
+ Fleet.find(42).update(ship_ids: ["2"])
33
+ end
34
+
35
+ test '#save includes modified has_many associations' do
36
+ webmock(:get, '/fleets', where: {id: 1}, limit: 1, include: [:ships]).to_return({
37
+ body: [{
38
+ id: 1,
39
+ name: 'Armada Trio',
40
+ ships: [
41
+ {id: 2, fleet_id: 1, name: 'Definant'}
42
+ ]
43
+ }].to_json
44
+ })
45
+
46
+ req_stub = webmock(:patch, '/fleets/1').with(
47
+ body: {
48
+ fleet: {
49
+ ships_attributes: [{ name: 'Voyager' }]
50
+ }
51
+ }.to_json
52
+ ).to_return(
53
+ body: {
54
+ id: 1,
55
+ name: 'Armada Trio',
56
+ ships: [{ id: 3, fleet_id: 1, name: 'Voyager' }]
57
+ }.to_json
58
+ )
59
+
60
+ # fleet.ships = [ship]
61
+ fleet = Fleet.eager_load(:ships).find(1)
62
+ assert fleet.update(ships: [Ship.new(name: 'Voyager')])
63
+ assert_equal 1, fleet.id
64
+ assert_equal [3], fleet.ships.map(&:id)
65
+ # assert_equal 3, ship.fleet.id
66
+ # assert_equal 'Definant 001', ship.name
67
+ # assert_equal 'Armada 2', ship.fleet.name
68
+
69
+ assert_requested req_stub
70
+ end
71
+
72
+ end
@@ -0,0 +1,15 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveRecord::EagerLoadingTest < Minitest::Test
4
+
5
+ test '#eager_load' do
6
+ webmock(:get, "/fleets", include: [{:ships => :sailors}]).to_return(body: [{
7
+ id: 1, ships: [{id: 1, fleet_id: 1}]
8
+ }].to_json)
9
+
10
+ fleets = Fleet.eager_load(:ships => :sailors)
11
+ assert_equal [1], fleets.map(&:id)
12
+ assert_equal [1], fleets.first.ships.map(&:id)
13
+ end
14
+
15
+ end
@@ -0,0 +1,190 @@
1
+ require 'test_helper'
2
+
3
+ class ActiveRecord::PersistanceTest < Minitest::Test
4
+
5
+ class TestModelA < ExampleRecord
6
+ end
7
+ class TestModelB < ExampleRecord
8
+ before_save do
9
+ TestModelA.create
10
+ end
11
+ end
12
+
13
+ test '#create with errors' do
14
+ req_stub = webmock(:post, "/fleets").with(
15
+ body: { fleet: {} }.to_json
16
+ ).to_return(
17
+ status: 400,
18
+ body: {name: 'Armada Uno', errors: {name: 'is required'}}.to_json
19
+ )
20
+
21
+ fleet = Fleet.create()
22
+ assert_equal ["is required"], fleet.errors[:name]
23
+ assert_requested req_stub
24
+ end
25
+
26
+ test '#create' do
27
+ req_stub = webmock(:post, "/fleets").with(
28
+ body: { fleet: {name: 'Armada Uno'} }.to_json
29
+ ).to_return(
30
+ body: {id: 1, name: 'Armada Uno'}.to_json
31
+ )
32
+
33
+ Fleet.create(name: 'Armada Uno')
34
+
35
+ assert_requested req_stub
36
+ end
37
+
38
+ test '#save w/o changes' do
39
+ webmock(:get, '/fleets', where: {id: 1}, limit: 1).to_return(
40
+ body: [{id: 1, name: 'Armada Duo'}].to_json
41
+ )
42
+
43
+ fleet = Fleet.find(1)
44
+ fleet.save
45
+
46
+ assert fleet.save
47
+ assert_equal 1, fleet.id
48
+ assert_equal 'Armada Duo', fleet.name
49
+ end
50
+
51
+
52
+ test '#save attempts another request while in transaction' do
53
+ webmock(:get, '/test_model_bs/schema').to_return(
54
+ body: {
55
+ id: {type: 'integer', primary_key: true, null: false, array: false},
56
+ name: {type: 'string', primary_key: false, null: true, array: false}
57
+ }.to_json
58
+ )
59
+ webmock(:get, '/test_model_as/schema').to_return(
60
+ body: {
61
+ id: {type: 'integer', primary_key: true, null: false, array: false},
62
+ name: {type: 'string', primary_key: false, null: true, array: false}
63
+ }.to_json
64
+ )
65
+
66
+ assert_raises ActiveRecord::StatementInvalid do
67
+ TestModelB.create
68
+ end
69
+ end
70
+
71
+ test '#save includes modified belongs_to associations' do
72
+ ship = Ship.new(name: 'Definant', fleet: Fleet.new(name: 'Armada Duo'))
73
+
74
+ req_stub = webmock(:post, '/ships', {include: :fleet}).with(
75
+ body: {
76
+ ship: {
77
+ name: 'Definant', fleet_attributes: { name: 'Armada Duo' }
78
+ }
79
+ }.to_json
80
+ ).to_return(
81
+ body: {
82
+ id: 2,
83
+ fleet_id: 3,
84
+ name: 'Definant 001',
85
+ fleet: { id: 3, name: 'Armada 2' }
86
+ }.to_json
87
+ )
88
+
89
+ assert ship.save
90
+ assert_equal 2, ship.id
91
+ assert_equal 3, ship.fleet_id
92
+ assert_equal 3, ship.fleet.id
93
+ assert_equal 'Definant 001', ship.name
94
+ assert_equal 'Armada 2', ship.fleet.name
95
+
96
+ assert_requested req_stub
97
+ end
98
+
99
+
100
+ test '#update clears belongs_to relationship' do
101
+ webmock(:get, "/ships", where: {id: 1}, limit: 1).to_return(
102
+ body: [{id: 1, fleet_id: 1, name: 'Armada Uno'}].to_json
103
+ )
104
+ req_stub = webmock(:patch, '/ships/1').with(
105
+ body: {ship: {fleet_id: nil}}.to_json
106
+ ).to_return(
107
+ body: {id: 1, name: 'Armada Uno'}.to_json
108
+ )
109
+
110
+ ship = Ship.find(1)
111
+ assert ship.update(fleet: nil)
112
+ assert_requested req_stub
113
+ end
114
+
115
+ test '#update' do
116
+ webmock(:get, "/ships", where: {id: 1}, limit: 1).to_return(
117
+ body: [{id: 1, fleet_id: nil, name: 'Armada Uno'}].to_json
118
+ )
119
+ req_stub = webmock(:patch, "/ships").with(
120
+ body: { ship: { name: 'Armada Trio' } }.to_json
121
+ ).to_return(
122
+ body: {id: 1, name: 'Armada Trio'}.to_json
123
+ )
124
+
125
+ Ship.find(1).update(name: 'Armada Trio')
126
+
127
+ assert_requested req_stub
128
+ end
129
+
130
+ test '#update habtm relationships' do
131
+ webmock(:get, "/ships", where: {id: 1}, limit: 1).to_return(
132
+ body: [{id: 1, fleet_id: nil, name: 'Armada Uno'}].to_json
133
+ )
134
+ webmock(:get, "/sailors", where: {id: 1}, limit: 1).to_return(
135
+ body: [{id: 1, name: 'Captain'}].to_json
136
+ )
137
+ webmock(:get, "/sailors", where: {sailors_ships: {ship_id: {eq: 1}}}).to_return(
138
+ body: [].to_json
139
+ )
140
+ req_stub = webmock(:patch, '/ships/1').with(
141
+ body: {ship: {sailors_attributes: [{id: 1, name: "Captain"}]}}.to_json
142
+ ).to_return(
143
+ body: {id: 1, name: 'Armada Uno'}.to_json
144
+ )
145
+
146
+ ship = Ship.find(1)
147
+ assert ship.update(sailors: [Sailor.find(1)])
148
+ assert_requested req_stub
149
+ end
150
+
151
+ test '#update clears habtm relationship' do
152
+ webmock(:get, "/ships", where: {id: 1}, limit: 1).to_return(
153
+ body: [{id: 1, fleet_id: nil, name: 'Armada Uno'}].to_json
154
+ )
155
+ webmock(:get, "/sailors", where: {id: 1}, limit: 1).to_return(
156
+ body: [{id: 1, name: 'Captain'}].to_json
157
+ )
158
+ webmock(:get, "/sailors", where: {sailors_ships: {ship_id: {eq: 1}}}).to_return(
159
+ body: [{id: 1, name: 'Captain'}].to_json
160
+ )
161
+ req_stub = webmock(:patch, '/ships/1').with(
162
+ body: {ship: {sailors_attributes: []}}.to_json
163
+ ).to_return(
164
+ body: {id: 1, name: 'Armada Uno'}.to_json
165
+ )
166
+
167
+ ship = Ship.find(1)
168
+ assert ship.update(sailors: [])
169
+ assert_requested req_stub
170
+ end
171
+
172
+ test '#update clears has_many relationship' do
173
+ webmock(:get, "/fleets", where: {id: 1}, limit: 1).to_return(
174
+ body: [{id: 1, name: 'Armada Uno'}].to_json
175
+ )
176
+ webmock(:get, "/ships", where: {fleet_id: 1}).to_return(
177
+ body: [{id: 1, name: 'Saucer Trio'}].to_json
178
+ )
179
+ req_stub = webmock(:patch, '/fleets/1').with(
180
+ body: {fleet: {ships_attributes: []}}.to_json
181
+ ).to_return(
182
+ body: {id: 1, name: 'Armada Uno'}.to_json
183
+ )
184
+
185
+ fleet = Fleet.find(1)
186
+ assert fleet.update(ships: [])
187
+ assert_requested req_stub
188
+ end
189
+
190
+ end