activehistory 0.1 → 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.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -2
- data/activehistory.gemspec +2 -1
- data/lib/activehistory.rb +17 -4
- data/lib/activehistory/action.rb +5 -3
- data/lib/activehistory/adapters/active_record.rb +329 -105
- data/lib/activehistory/connection.rb +11 -23
- data/lib/activehistory/event.rb +79 -25
- data/lib/activehistory/version.rb +2 -2
- data/test/active_record_adapter/association_test/belongs_to_association_test.rb +46 -38
- data/test/active_record_adapter/association_test/has_and_belongs_to_many_test.rb +96 -63
- data/test/active_record_adapter/association_test/has_many_association_test.rb +166 -27
- data/test/active_record_adapter/association_test/has_one_association_test.rb +36 -40
- data/test/active_record_adapter/create_test.rb +10 -14
- data/test/active_record_adapter/destroy_test.rb +4 -6
- data/test/active_record_adapter/event_test.rb +33 -7
- data/test/active_record_adapter/save_test.rb +8 -12
- data/test/activehistory_test.rb +96 -0
- data/test/models.rb +1 -1
- data/test/test_helper.rb +12 -1
- metadata +17 -2
@@ -8,19 +8,16 @@ class ActiveHistory::Connection
|
|
8
8
|
def initialize(config)
|
9
9
|
if config[:url]
|
10
10
|
uri = URI.parse(config.delete(:url))
|
11
|
-
config[:api_key]
|
12
|
-
config[:host]
|
13
|
-
config[:port]
|
14
|
-
config[:ssl]
|
11
|
+
config[:api_key] ||= (uri.user ? CGI.unescape(uri.user) : nil)
|
12
|
+
config[:host] ||= uri.host
|
13
|
+
config[:port] ||= uri.port
|
14
|
+
config[:ssl] ||= (uri.scheme == 'https')
|
15
15
|
end
|
16
16
|
|
17
17
|
[:api_key, :host, :port, :ssl, :user_agent].each do |key|
|
18
18
|
self.instance_variable_set(:"@#{key}", config[key])
|
19
19
|
end
|
20
20
|
|
21
|
-
@connection = Net::HTTP.new(host, port)
|
22
|
-
@connection.use_ssl = ssl
|
23
|
-
|
24
21
|
true
|
25
22
|
end
|
26
23
|
|
@@ -61,27 +58,18 @@ class ActiveHistory::Connection
|
|
61
58
|
return_value = nil
|
62
59
|
retry_count = 0
|
63
60
|
begin
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
end
|
68
|
-
|
61
|
+
connection = Net::HTTP.new(host, port)
|
62
|
+
connection.use_ssl = ssl
|
63
|
+
connection.request(request) do |response|
|
69
64
|
validate_response_code(response)
|
70
|
-
|
71
|
-
# Get the cookies
|
72
|
-
response.each_header do |key, value|
|
73
|
-
if key.downcase == 'set-cookie' && Thread.current[:sunstone_cookie_store]
|
74
|
-
Thread.current[:sunstone_cookie_store].set_cookie(request_uri, value)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
65
|
+
|
78
66
|
if block_given?
|
79
|
-
return_value =yield(response)
|
67
|
+
return_value = yield(response)
|
80
68
|
else
|
81
|
-
return_value =response
|
69
|
+
return_value = response
|
82
70
|
end
|
83
71
|
end
|
84
|
-
rescue
|
72
|
+
rescue ActiveHistory::Exception::ServiceUnavailable
|
85
73
|
retry_count += 1
|
86
74
|
retry_count == 1 ? retry : raise
|
87
75
|
end
|
data/lib/activehistory/event.rb
CHANGED
@@ -1,50 +1,104 @@
|
|
1
|
+
require 'globalid'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
GlobalID::Locator.use :activehistory do |gid|
|
5
|
+
ActiveHistory::Event.new({ id: gid.model_id })
|
6
|
+
end
|
7
|
+
|
1
8
|
class ActiveHistory::Event
|
2
|
-
|
3
|
-
|
9
|
+
include GlobalID::Identification
|
10
|
+
|
11
|
+
attr_accessor :id, :ip, :user_agent, :session_id, :metadata, :timestamp, :performed_by_id, :performed_by_type, :actions
|
4
12
|
|
5
13
|
def initialize(attrs={})
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
14
|
+
attrs.each do |k,v|
|
15
|
+
self.send("#{k}=", v)
|
16
|
+
end
|
17
|
+
|
18
|
+
if id
|
19
|
+
@persisted = true
|
20
|
+
else
|
21
|
+
@persisted = false
|
22
|
+
@id ||= SecureRandom.uuid
|
23
|
+
end
|
24
|
+
|
25
|
+
@actions ||= []
|
26
|
+
@timestamp ||= Time.now
|
10
27
|
end
|
11
28
|
|
29
|
+
def persisted?
|
30
|
+
@persisted
|
31
|
+
end
|
32
|
+
|
12
33
|
def action!(action)
|
13
34
|
action = ActiveHistory::Action.new(action)
|
14
35
|
@actions << action
|
15
36
|
action
|
16
37
|
end
|
17
38
|
|
18
|
-
def action_for(id)
|
19
|
-
|
39
|
+
def action_for(type, id, new_options=nil)
|
40
|
+
type = type.base_class.model_name.name if !type.is_a?(String)
|
41
|
+
action = @actions.find { |a| a.subject_type.to_s == type.to_s && a.subject_id.to_s == id.to_s }
|
42
|
+
|
43
|
+
if new_options
|
44
|
+
action || action!({ subject_type: type, subject_id: id, type: :update }.merge(new_options))
|
45
|
+
else
|
46
|
+
action
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.create!(attrs={})
|
51
|
+
event = self.new(attrs)
|
52
|
+
event.save!
|
53
|
+
event
|
54
|
+
end
|
55
|
+
|
56
|
+
def save!
|
57
|
+
persisted? ? _update : _create
|
20
58
|
end
|
21
59
|
|
22
|
-
def
|
23
|
-
|
60
|
+
def _update
|
61
|
+
return if actions.empty?
|
62
|
+
ActiveHistory.connection.post('/actions', {
|
63
|
+
actions: actions.as_json.map{|json| json[:event_id] = id; json}
|
64
|
+
})
|
65
|
+
@actions = []
|
24
66
|
end
|
25
67
|
|
26
|
-
def
|
27
|
-
return if @actions.empty?
|
28
|
-
|
68
|
+
def _create
|
29
69
|
ActiveHistory.connection.post('/events', self.as_json)
|
70
|
+
@actions = []
|
71
|
+
@persisted = true
|
30
72
|
end
|
31
|
-
|
73
|
+
|
32
74
|
def as_json
|
33
75
|
{
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
76
|
+
id: id,
|
77
|
+
ip: ip,
|
78
|
+
user_agent: user_agent,
|
79
|
+
session_id: session_id,
|
80
|
+
metadata: metadata,
|
81
|
+
performed_by_type: performed_by_type,
|
82
|
+
performed_by_id: performed_by_id,
|
83
|
+
timestamp: timestamp.utc.iso8601(3),
|
84
|
+
actions: actions.as_json
|
43
85
|
}
|
44
86
|
end
|
87
|
+
|
88
|
+
def to_gid_param(options={})
|
89
|
+
to_global_id(options).to_param
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_global_id(options={})
|
93
|
+
@global_id ||= GlobalID.create(self, { app: :activehistory }.merge(options))
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_sgid_param(options={})
|
97
|
+
to_signed_global_id(options).to_param
|
98
|
+
end
|
45
99
|
|
46
|
-
def
|
47
|
-
|
100
|
+
def to_signed_global_id(options={})
|
101
|
+
SignedGlobalID.create(self, { app: :activehistory }.merge(options))
|
48
102
|
end
|
49
103
|
|
50
104
|
end
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module ActiveHistory
|
2
|
-
VERSION = '0.
|
3
|
-
end
|
2
|
+
VERSION = '0.2'
|
3
|
+
end
|
@@ -13,70 +13,78 @@ class BelongsToAssociationTest < ActiveSupport::TestCase
|
|
13
13
|
|
14
14
|
@target = travel_to(@time) { create(:photo, account: @model) }
|
15
15
|
|
16
|
-
assert_posted("/events") do
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
16
|
+
assert_posted("/events") do
|
17
|
+
assert_action_for @model, {
|
18
|
+
diff: { photo_ids: [[], [@target.id]] },
|
19
|
+
timestamp: @time.iso8601(3),
|
20
|
+
type: 'update',
|
21
|
+
subject_type: "Account",
|
22
|
+
subject_id: @model.id,
|
23
|
+
}
|
23
24
|
end
|
24
25
|
end
|
25
|
-
|
26
|
+
|
26
27
|
test 'TargetModel.update removes from association' do
|
27
28
|
@model = create(:account)
|
28
29
|
@target = create(:photo, account: @model)
|
29
30
|
WebMock::RequestRegistry.instance.reset!
|
30
|
-
|
31
|
+
|
31
32
|
travel_to(@time) { @target.update(account: nil) }
|
32
|
-
|
33
|
-
assert_posted("/events") do |req|
|
34
|
-
req_data = JSON.parse(req.body)
|
35
|
-
assert_equal 2, req_data['actions'].size
|
36
33
|
|
37
|
-
|
38
|
-
|
39
|
-
|
34
|
+
assert_posted("/events") do
|
35
|
+
assert_action_for @model, {
|
36
|
+
diff: { photo_ids: [[@target.id], []] },
|
37
|
+
timestamp: @time.iso8601(3),
|
38
|
+
type: 'update',
|
39
|
+
subject_type: "Account",
|
40
|
+
subject_id: @model.id,
|
41
|
+
}
|
40
42
|
end
|
41
43
|
end
|
42
|
-
|
44
|
+
|
43
45
|
test 'TargetModel.update changes association' do
|
44
46
|
@model1 = create(:account)
|
45
47
|
@model2 = create(:account)
|
46
48
|
@target = create(:photo, account: @model1)
|
47
49
|
WebMock::RequestRegistry.instance.reset!
|
48
|
-
|
50
|
+
|
49
51
|
travel_to(@time) { @target.update(account: @model2) }
|
50
|
-
|
51
|
-
assert_posted("/events") do |req|
|
52
|
-
req_data = JSON.parse(req.body)
|
53
|
-
assert_equal 3, req_data['actions'].size
|
54
52
|
|
55
|
-
|
56
|
-
|
57
|
-
|
53
|
+
assert_posted("/events") do
|
54
|
+
assert_action_for @model1, {
|
55
|
+
diff: { photo_ids: [[@target.id], []] },
|
56
|
+
timestamp: @time.iso8601(3),
|
57
|
+
type: 'update',
|
58
|
+
subject_type: "Account",
|
59
|
+
subject_id: @model1.id,
|
60
|
+
}
|
58
61
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
+
assert_action_for @model2, {
|
63
|
+
diff: { photo_ids: [[], [@target.id]] },
|
64
|
+
timestamp: @time.iso8601(3),
|
65
|
+
type: 'update',
|
66
|
+
subject_type: "Account",
|
67
|
+
subject_id: @model2.id,
|
68
|
+
}
|
62
69
|
end
|
63
70
|
end
|
64
|
-
|
71
|
+
|
65
72
|
test 'TargetModel.destroy removes from association' do
|
66
73
|
@model = create(:account)
|
67
74
|
@target = create(:photo, account: @model)
|
68
75
|
WebMock::RequestRegistry.instance.reset!
|
69
|
-
|
76
|
+
|
70
77
|
travel_to(@time) { @target.destroy }
|
71
|
-
|
72
|
-
assert_posted("/events") do |req|
|
73
|
-
req_data = JSON.parse(req.body)
|
74
|
-
assert_equal 2, req_data['actions'].size
|
75
78
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
assert_posted("/events") do
|
80
|
+
assert_action_for @model, {
|
81
|
+
diff: { photo_ids: [[@target.id], []] },
|
82
|
+
timestamp: @time.iso8601(3),
|
83
|
+
type: 'update',
|
84
|
+
subject_type: "Account",
|
85
|
+
subject_id: @model.id,
|
86
|
+
}
|
79
87
|
end
|
80
88
|
end
|
81
|
-
|
89
|
+
|
82
90
|
end
|
@@ -12,57 +12,54 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
12
12
|
WebMock::RequestRegistry.instance.reset!
|
13
13
|
|
14
14
|
@region = travel_to(@time) { create(:region, properties: [@property]) }
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
assert_equal req_data['actions'][0], {
|
15
|
+
|
16
|
+
assert_posted("/events") do
|
17
|
+
assert_action_for @region, {
|
20
18
|
diff: {
|
21
19
|
id: [nil, @region.id],
|
22
20
|
name: [nil, @region.name],
|
23
21
|
property_ids: [[], [@property.id]]
|
24
22
|
},
|
25
|
-
|
23
|
+
subject_type: "Region",
|
24
|
+
subject_id: @region.id,
|
26
25
|
timestamp: @time.iso8601(3),
|
27
26
|
type: 'create'
|
28
|
-
}
|
29
|
-
|
30
|
-
|
27
|
+
}
|
28
|
+
|
29
|
+
assert_action_for @property, {
|
31
30
|
timestamp: @time.iso8601(3),
|
32
31
|
type: 'update',
|
33
|
-
|
32
|
+
subject_type: "Property",
|
33
|
+
subject_id: @property.id,
|
34
34
|
diff: {
|
35
35
|
region_ids: [[], [@region.id]]
|
36
36
|
}
|
37
|
-
}
|
37
|
+
}
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
41
|
test '::create with new has_and_belongs_to_many association' do
|
42
|
-
WebMock::RequestRegistry.instance.reset!
|
43
|
-
|
44
42
|
@region = travel_to(@time) { create(:region, properties: [build(:property)]) }
|
45
43
|
@property = @region.properties.first
|
46
44
|
|
47
|
-
assert_posted("/events") do
|
48
|
-
|
49
|
-
assert_equal 2, req_data['actions'].size
|
50
|
-
|
51
|
-
assert_equal req_data['actions'][0], {
|
45
|
+
assert_posted("/events") do
|
46
|
+
assert_action_for @region, {
|
52
47
|
diff: {
|
53
48
|
id: [nil, @region.id],
|
54
49
|
name: [nil, @region.name],
|
55
50
|
property_ids: [[], [@property.id]]
|
56
51
|
},
|
57
|
-
|
52
|
+
subject_type: "Region",
|
53
|
+
subject_id: @region.id,
|
58
54
|
timestamp: @time.iso8601(3),
|
59
55
|
type: 'create'
|
60
|
-
}
|
56
|
+
}
|
61
57
|
|
62
|
-
|
58
|
+
assert_action_for @property, {
|
63
59
|
timestamp: @time.iso8601(3),
|
64
60
|
type: 'create',
|
65
|
-
|
61
|
+
subject_type: "Property",
|
62
|
+
subject_id: @property.id,
|
66
63
|
diff: {
|
67
64
|
id: [nil, @property.id],
|
68
65
|
name: [nil, @property.name],
|
@@ -74,7 +71,7 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
74
71
|
active: [nil, @property.active],
|
75
72
|
region_ids: [[], [@region.id]]
|
76
73
|
}
|
77
|
-
}
|
74
|
+
}
|
78
75
|
end
|
79
76
|
end
|
80
77
|
|
@@ -85,27 +82,26 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
85
82
|
|
86
83
|
travel_to(@time) { @region.update(properties: [@property]) }
|
87
84
|
|
88
|
-
assert_posted("/events") do
|
89
|
-
|
90
|
-
assert_equal 2, req_data['actions'].size
|
91
|
-
|
92
|
-
assert_equal req_data['actions'][0], {
|
85
|
+
assert_posted("/events") do
|
86
|
+
assert_action_for @region, {
|
93
87
|
diff: {
|
94
88
|
property_ids: [[], [@property.id]]
|
95
89
|
},
|
96
|
-
|
90
|
+
subject_type: "Region",
|
91
|
+
subject_id: @region.id,
|
97
92
|
timestamp: @time.iso8601(3),
|
98
93
|
type: 'update'
|
99
|
-
}
|
94
|
+
}
|
100
95
|
|
101
|
-
|
96
|
+
assert_action_for @property, {
|
102
97
|
timestamp: @time.iso8601(3),
|
103
98
|
type: 'update',
|
104
|
-
|
99
|
+
subject_type: "Property",
|
100
|
+
subject_id: @property.id,
|
105
101
|
diff: {
|
106
102
|
region_ids: [[], [@region.id]]
|
107
103
|
}
|
108
|
-
}
|
104
|
+
}
|
109
105
|
end
|
110
106
|
end
|
111
107
|
|
@@ -116,14 +112,12 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
116
112
|
travel_to(@time) { @region.update(properties: [build(:property)]) }
|
117
113
|
@property = @region.properties.first
|
118
114
|
|
119
|
-
assert_posted("/events") do
|
120
|
-
|
121
|
-
assert_equal 2, req_data['actions'].size
|
122
|
-
|
123
|
-
assert_equal req_data['actions'][0], {
|
115
|
+
assert_posted("/events") do
|
116
|
+
assert_action_for @property, {
|
124
117
|
timestamp: @time.iso8601(3),
|
125
118
|
type: 'create',
|
126
|
-
|
119
|
+
subject_type: "Property",
|
120
|
+
subject_id: @property.id,
|
127
121
|
diff: {
|
128
122
|
id: [nil, @property.id],
|
129
123
|
name: [nil, @property.name],
|
@@ -135,16 +129,17 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
135
129
|
active: [nil, @property.active],
|
136
130
|
region_ids: [[], [@region.id]]
|
137
131
|
}
|
138
|
-
}
|
132
|
+
}
|
139
133
|
|
140
|
-
|
134
|
+
assert_action_for @region, {
|
141
135
|
diff: {
|
142
136
|
property_ids: [[], [@property.id]]
|
143
137
|
},
|
144
|
-
|
138
|
+
subject_type: "Region",
|
139
|
+
subject_id: @region.id,
|
145
140
|
timestamp: @time.iso8601(3),
|
146
141
|
type: 'update'
|
147
|
-
}
|
142
|
+
}
|
148
143
|
end
|
149
144
|
end
|
150
145
|
|
@@ -152,30 +147,70 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
152
147
|
@property = create(:property)
|
153
148
|
@region = create(:region, properties: [@property])
|
154
149
|
WebMock::RequestRegistry.instance.reset!
|
155
|
-
|
150
|
+
|
156
151
|
travel_to(@time) { @region.update(properties: []) }
|
157
152
|
|
158
|
-
assert_posted("/events") do
|
159
|
-
|
160
|
-
assert_equal 2, req_data['actions'].size
|
161
|
-
|
162
|
-
assert_equal req_data['actions'][0], {
|
153
|
+
assert_posted("/events") do
|
154
|
+
assert_action_for @region, {
|
163
155
|
diff: {
|
164
156
|
property_ids: [[@property.id], []]
|
165
157
|
},
|
166
|
-
|
158
|
+
subject_type: "Region",
|
159
|
+
subject_id: @region.id,
|
167
160
|
timestamp: @time.iso8601(3),
|
168
161
|
type: 'update'
|
169
|
-
}
|
162
|
+
}
|
170
163
|
|
171
|
-
|
164
|
+
assert_action_for @property, {
|
172
165
|
timestamp: @time.iso8601(3),
|
173
166
|
type: 'update',
|
174
|
-
|
167
|
+
subject_type: "Property",
|
168
|
+
subject_id: @property.id,
|
175
169
|
diff: {
|
176
170
|
region_ids: [[@region.id], []]
|
177
171
|
}
|
178
|
-
}
|
172
|
+
}
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
test '::update with replacing has_and_belongs_to_many association' do
|
177
|
+
@property1 = create(:property)
|
178
|
+
@property2 = create(:property)
|
179
|
+
@region = create(:region, properties: [@property1])
|
180
|
+
WebMock::RequestRegistry.instance.reset!
|
181
|
+
|
182
|
+
travel_to(@time) { @region.update(properties: [@property2]) }
|
183
|
+
|
184
|
+
assert_posted("/events") do
|
185
|
+
assert_action_for @region, {
|
186
|
+
diff: {
|
187
|
+
property_ids: [[@property1.id], [@property2.id]]
|
188
|
+
},
|
189
|
+
subject_type: "Region",
|
190
|
+
subject_id: @region.id,
|
191
|
+
timestamp: @time.iso8601(3),
|
192
|
+
type: 'update'
|
193
|
+
}
|
194
|
+
|
195
|
+
assert_action_for @property1, {
|
196
|
+
timestamp: @time.iso8601(3),
|
197
|
+
type: 'update',
|
198
|
+
subject_type: "Property",
|
199
|
+
subject_id: @property1.id,
|
200
|
+
diff: {
|
201
|
+
region_ids: [[@region.id], []]
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
assert_action_for @property2, {
|
206
|
+
timestamp: @time.iso8601(3),
|
207
|
+
type: 'update',
|
208
|
+
subject_type: "Property",
|
209
|
+
subject_id: @property2.id,
|
210
|
+
diff: {
|
211
|
+
region_ids: [[], [@region.id]]
|
212
|
+
}
|
213
|
+
}
|
179
214
|
end
|
180
215
|
end
|
181
216
|
|
@@ -186,25 +221,24 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
186
221
|
|
187
222
|
travel_to(@time) { @region.destroy }
|
188
223
|
|
189
|
-
assert_posted("/events") do
|
190
|
-
|
191
|
-
assert_equal 2, req_data['actions'].size
|
192
|
-
|
193
|
-
assert_equal req_data['actions'][0], {
|
224
|
+
assert_posted("/events") do
|
225
|
+
assert_action_for @region, {
|
194
226
|
diff: {
|
195
227
|
id: [@region.id, nil],
|
196
228
|
name: [@region.name, nil],
|
197
229
|
property_ids: [[@property.id], []]
|
198
230
|
},
|
199
|
-
|
231
|
+
subject_type: "Region",
|
232
|
+
subject_id: @region.id,
|
200
233
|
timestamp: @time.iso8601(3),
|
201
234
|
type: 'destroy'
|
202
235
|
}.as_json
|
203
236
|
|
204
|
-
|
237
|
+
assert_action_for @property, {
|
205
238
|
timestamp: @time.iso8601(3),
|
206
239
|
type: 'update',
|
207
|
-
|
240
|
+
subject_type: "Property",
|
241
|
+
subject_id: @property.id,
|
208
242
|
diff: {
|
209
243
|
region_ids: [[@region.id], []]
|
210
244
|
}
|
@@ -212,7 +246,6 @@ class HasAndBelongsToManyAssociationTest < ActiveSupport::TestCase
|
|
212
246
|
end
|
213
247
|
end
|
214
248
|
|
215
|
-
|
216
249
|
test 'has_and_belongs_to_many <<'
|
217
250
|
test 'has_and_belongs_to_many.delete'
|
218
251
|
test 'has_and_belongs_to_many.destroy'
|