forest_liana 9.22.3 → 9.22.5
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/app/services/forest_liana/apimap_sorter.rb +7 -6
- data/app/services/forest_liana/base_getter.rb +11 -2
- data/lib/forest_liana/version.rb +1 -1
- data/spec/dummy/app/models/boat.rb +8 -0
- data/spec/dummy/app/models/captain.rb +3 -0
- data/spec/dummy/app/models/harbor.rb +3 -0
- data/spec/dummy/app/models/license.rb +2 -0
- data/spec/dummy/app/models/membership.rb +5 -0
- data/spec/dummy/db/migrate/20261001120000_create_harbors_boats_captains_licenses.rb +23 -0
- data/spec/dummy/db/schema.rb +23 -1
- data/spec/lib/forest_liana/bootstrapper_spec.rb +4 -0
- data/spec/requests/related_list_nested_through_spec.rb +55 -0
- data/spec/services/forest_liana/apimap_sorter_spec.rb +74 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ca262c2db5e774393d0dd9b9266bbd360cea72c8caeea733c7df95a32bd6f3c
|
|
4
|
+
data.tar.gz: 5917abff7f56a4df5bf38c885ab375dc7240f666826c46a5ddb92a4709110d8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c60d6abc5f591ac151cf592e56f7790fc24db53984f4a646da46bf61ca1030b77d21713a4ee7d63a334ad9a70c5b934c982ce1fd76669e485be2c58e634fddcf
|
|
7
|
+
data.tar.gz: 55b8f9f4a71a17dcb8cb3c55b0a240e0576697ae5ca42116f266aa9b429d15aea49193222ea0f599a6feb892405f3fdfb565e1d1a033e4595c22f351e277e112
|
|
@@ -110,15 +110,16 @@ module ForestLiana
|
|
|
110
110
|
private
|
|
111
111
|
|
|
112
112
|
def sort_array_of_objects(array)
|
|
113
|
-
array.
|
|
114
|
-
[element1['type'], element1['id']] <=> [element2['type'], element2['id']]
|
|
115
|
-
end
|
|
113
|
+
array.sort_by! { |element| [element['type'].to_s, element['id'].to_s] }
|
|
116
114
|
end
|
|
117
115
|
|
|
116
|
+
# NOTICE: A field 'type' is a String ('String'), an Array (['String'] for an array column)
|
|
117
|
+
# or a Hash (a nested type), and those are not comparable with one another:
|
|
118
|
+
# `['String'] <=> 'String'` returns nil, which makes `sort` raise an ArgumentError as soon
|
|
119
|
+
# as two fields share a name. Only the tie-breaker ever needs to be total, so JSON-ifying
|
|
120
|
+
# that one alone keeps the primary key byte-for-byte identical and the apimap churn-free.
|
|
118
121
|
def sort_array_of_fields(array)
|
|
119
|
-
array.
|
|
120
|
-
[field1['field'], field1['type']] <=> [field2['field'], field2['type']]
|
|
121
|
-
end
|
|
122
|
+
array.sort_by { |field| [field['field'].to_s, field['type'].to_json] }
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
def reorder_keys_basic(object)
|
|
@@ -544,8 +544,13 @@ module ForestLiana
|
|
|
544
544
|
# Skip ActiveStorage associations - already processed above
|
|
545
545
|
next if is_active_storage_association?(association)
|
|
546
546
|
|
|
547
|
+
# NOTICE: A preloaded :through only reads keys off the rows this query builds: the root
|
|
548
|
+
# row, and the narrowed rows of a first hop it joins. Walking the whole chain would
|
|
549
|
+
# name tables it never joins.
|
|
550
|
+
if through_chain.any? && !joined?(association, joined_relations)
|
|
551
|
+
select_preload_hop_keys(select, [association.name], joined_relations)
|
|
547
552
|
# For :through associations, recursively add all intermediate foreign keys
|
|
548
|
-
|
|
553
|
+
elsif through_chain.any?
|
|
549
554
|
current_resource = projected_resource
|
|
550
555
|
through_chain.reverse.each do |through_name|
|
|
551
556
|
through_assoc = current_resource.reflect_on_association(through_name)
|
|
@@ -694,7 +699,11 @@ module ForestLiana
|
|
|
694
699
|
# those rows are the narrowed ones built off the JOIN, so its key has to be named here too.
|
|
695
700
|
# It was not, and the list answered 500 rather than the one field (PRD-1316).
|
|
696
701
|
def select_dependency_preload_keys(select, relation_path, joined_relations)
|
|
697
|
-
|
|
702
|
+
select_preload_hop_keys(select, relation_path.relations, joined_relations)
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def select_preload_hop_keys(select, relations, joined_relations)
|
|
706
|
+
chains = flatten_dependency_hops(relations)
|
|
698
707
|
return if chains.empty?
|
|
699
708
|
|
|
700
709
|
narrowed_hops(chains, joined_relations).each do |owner, reflection|
|
data/lib/forest_liana/version.rb
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class Boat < ActiveRecord::Base
|
|
2
|
+
belongs_to :harbor
|
|
3
|
+
belongs_to :captain
|
|
4
|
+
|
|
5
|
+
# Its second hop is keyed on captains.license_number, which the preloader reads off the captain
|
|
6
|
+
# rows: narrowed ones when Harbor's boats related list joins captain to sort on it.
|
|
7
|
+
has_one :captain_license, through: :captain, source: :license
|
|
8
|
+
end
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
class Membership < ActiveRecord::Base
|
|
2
2
|
belongs_to :island
|
|
3
3
|
belongs_to :user
|
|
4
|
+
|
|
5
|
+
# A :through of a :through: displayed on Island's memberships related list, the hop past the
|
|
6
|
+
# first reads `locations`, a table that list preloads rather than joins (issue #818).
|
|
7
|
+
has_one :location, through: :island
|
|
8
|
+
has_one :location_island, through: :location, source: :island
|
|
4
9
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateHarborsBoatsCaptainsLicenses < ActiveRecord::Migration[6.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :harbors do |t|
|
|
4
|
+
t.string :name
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
create_table :captains do |t|
|
|
8
|
+
t.string :name
|
|
9
|
+
t.string :license_number
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
create_table :licenses do |t|
|
|
13
|
+
t.string :number
|
|
14
|
+
t.date :expires_on
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
create_table :boats do |t|
|
|
18
|
+
t.string :name
|
|
19
|
+
t.references :harbor
|
|
20
|
+
t.references :captain
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/spec/dummy/db/schema.rb
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
#
|
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
|
12
12
|
|
|
13
|
-
ActiveRecord::Schema.define(version:
|
|
13
|
+
ActiveRecord::Schema.define(version: 2026_10_01_120000) do
|
|
14
14
|
|
|
15
15
|
create_table "addresses", force: :cascade do |t|
|
|
16
16
|
t.string "line1"
|
|
@@ -29,6 +29,19 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
|
|
|
29
29
|
t.datetime "updated_at", precision: 6, null: false
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
create_table "boats", force: :cascade do |t|
|
|
33
|
+
t.string "name"
|
|
34
|
+
t.integer "harbor_id"
|
|
35
|
+
t.integer "captain_id"
|
|
36
|
+
t.index ["captain_id"], name: "index_boats_on_captain_id"
|
|
37
|
+
t.index ["harbor_id"], name: "index_boats_on_harbor_id"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
create_table "captains", force: :cascade do |t|
|
|
41
|
+
t.string "name"
|
|
42
|
+
t.string "license_number"
|
|
43
|
+
end
|
|
44
|
+
|
|
32
45
|
create_table "flags", force: :cascade do |t|
|
|
33
46
|
t.integer "island_id"
|
|
34
47
|
t.string "color"
|
|
@@ -37,6 +50,10 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
|
|
|
37
50
|
t.index ["island_id"], name: "index_flags_on_island_id"
|
|
38
51
|
end
|
|
39
52
|
|
|
53
|
+
create_table "harbors", force: :cascade do |t|
|
|
54
|
+
t.string "name"
|
|
55
|
+
end
|
|
56
|
+
|
|
40
57
|
create_table "isle", force: :cascade do |t|
|
|
41
58
|
t.string "name"
|
|
42
59
|
t.binary "map"
|
|
@@ -44,6 +61,11 @@ ActiveRecord::Schema.define(version: 2026_09_24_120000) do
|
|
|
44
61
|
t.datetime "updated_at"
|
|
45
62
|
end
|
|
46
63
|
|
|
64
|
+
create_table "licenses", force: :cascade do |t|
|
|
65
|
+
t.string "number"
|
|
66
|
+
t.date "expires_on"
|
|
67
|
+
end
|
|
68
|
+
|
|
47
69
|
create_table "locations", force: :cascade do |t|
|
|
48
70
|
t.string "coordinates"
|
|
49
71
|
t.integer "island_id"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
describe 'A related list displaying a has_one :through', type: :request do
|
|
4
|
+
before do
|
|
5
|
+
allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
|
|
6
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
|
|
7
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
|
|
8
|
+
allow_any_instance_of(ForestLiana::Ability).to receive(:forest_authorize!) { true }
|
|
9
|
+
Rails.cache.write('forest.has_permission', false)
|
|
10
|
+
allow(ForestLiana::ScopeManager).to receive(:fetch_scopes)
|
|
11
|
+
.and_return('scopes' => {}, 'team' => { 'id' => '1', 'name' => 'Operations' })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
token = JWT.encode({ id: 38, email: 'michael.kelso@that70.show', first_name: 'Michael',
|
|
15
|
+
last_name: 'Kelso', team: 'Operations', rendering_id: 16,
|
|
16
|
+
exp: Time.now.to_i + 2.weeks.to_i, permission_level: 'admin' },
|
|
17
|
+
ForestLiana.auth_secret, 'HS256')
|
|
18
|
+
headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json',
|
|
19
|
+
'Authorization' => "Bearer #{token}" }
|
|
20
|
+
|
|
21
|
+
let!(:user) { User.create!(name: 'Michel', title: :king) }
|
|
22
|
+
let!(:island) { Island.create!(name: 'Skull') }
|
|
23
|
+
let!(:location) { Location.create!(coordinates: '12345', island: island) }
|
|
24
|
+
let!(:membership) { Membership.create!(island: island, user: user) }
|
|
25
|
+
|
|
26
|
+
it 'serves a :through going through another :through' do
|
|
27
|
+
get "/forest/Island/#{island.id}/relationships/memberships",
|
|
28
|
+
params: { fields: { 'Membership' => 'id,location_island', 'location_island' => 'name' },
|
|
29
|
+
page: { 'number' => '1', 'size' => '15' }, sort: '-id', timezone: 'Europe/Paris' },
|
|
30
|
+
headers: headers
|
|
31
|
+
|
|
32
|
+
expect(response.status).to eq(200)
|
|
33
|
+
body = JSON.parse(response.body)
|
|
34
|
+
expect(body['data'].map { |row| row.dig('relationships', 'location_island', 'data', 'id') }).to eq([island.id.to_s])
|
|
35
|
+
expect(body['included'].map { |record| record.dig('attributes', 'name') }).to eq(['Skull'])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sorting on captain joins it with only the columns the list shows, and the preloader reads the
|
|
39
|
+
# license's key, captains.license_number, off those narrowed rows.
|
|
40
|
+
it 'serves a :through whose first hop is joined and keys the next hop on a non-primary column' do
|
|
41
|
+
harbor = Harbor.create!(name: 'Saint-Malo')
|
|
42
|
+
captain = Captain.create!(name: 'Haddock', license_number: 'FR-4421')
|
|
43
|
+
license = License.create!(number: 'FR-4421', expires_on: Date.new(2030, 1, 1))
|
|
44
|
+
Boat.create!(name: 'Karaboudjan', harbor: harbor, captain: captain)
|
|
45
|
+
|
|
46
|
+
get "/forest/Harbor/#{harbor.id}/relationships/boats",
|
|
47
|
+
params: { fields: { 'Boat' => 'id,captain,captain_license', 'captain' => 'name', 'captain_license' => 'number' },
|
|
48
|
+
page: { 'number' => '1', 'size' => '15' }, sort: 'captain.name', timezone: 'Europe/Paris' },
|
|
49
|
+
headers: headers
|
|
50
|
+
|
|
51
|
+
expect(response.status).to eq(200)
|
|
52
|
+
body = JSON.parse(response.body)
|
|
53
|
+
expect(body['data'].map { |row| row.dig('relationships', 'captain_license', 'data', 'id') }).to eq([license.id.to_s])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -171,6 +171,80 @@ module ForestLiana
|
|
|
171
171
|
expect(apimap_sorted['meta']['stack'].keys).to eq(['database_type', 'orm_version'])
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
|
+
|
|
175
|
+
context 'on an apimap with fields sharing a name but not a comparable type' do
|
|
176
|
+
# NOTICE: a field 'type' is a String ('String'), an Array (['String'] for an array
|
|
177
|
+
# column) or a Hash (a nested type). The sort used to compare those values
|
|
178
|
+
# directly, so `['String'] <=> 'String'` returned nil and `sort` raised
|
|
179
|
+
# `ArgumentError: comparison of Hash with Hash failed`. The rescue in `perform`
|
|
180
|
+
# swallowed it and returned a half-processed apimap: collections that were never
|
|
181
|
+
# reached kept attributes outside KEYS_COLLECTION (such as 'search_fields'), and
|
|
182
|
+
# the Forest API rejects those with `HTTP 400 ValidationFailedError`.
|
|
183
|
+
let(:apimap) do
|
|
184
|
+
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode({
|
|
185
|
+
'meta': {
|
|
186
|
+
stack: {
|
|
187
|
+
'orm_version': '4.34.9',
|
|
188
|
+
'database_type': 'postgresql',
|
|
189
|
+
},
|
|
190
|
+
liana: 'forest-rails',
|
|
191
|
+
'liana_version': '1.5.24',
|
|
192
|
+
},
|
|
193
|
+
'data': [{
|
|
194
|
+
id: 'posts',
|
|
195
|
+
type: 'collections',
|
|
196
|
+
attributes: {
|
|
197
|
+
name: 'posts',
|
|
198
|
+
fields: [
|
|
199
|
+
{ field: 'tags', type: ['String'] },
|
|
200
|
+
{ field: 'tags', type: 'String' },
|
|
201
|
+
{ field: 'meta', type: { fields: [{ field: 'locale', type: 'String' }] } },
|
|
202
|
+
{ field: 'meta', type: 'String' },
|
|
203
|
+
{ field: 'id', type: 'Number' },
|
|
204
|
+
],
|
|
205
|
+
}
|
|
206
|
+
}, {
|
|
207
|
+
id: 'zebras',
|
|
208
|
+
type: 'collections',
|
|
209
|
+
attributes: {
|
|
210
|
+
'search_fields': ['name'],
|
|
211
|
+
fields: [
|
|
212
|
+
{ field: 'id', type: 'Number' },
|
|
213
|
+
],
|
|
214
|
+
name: 'zebras',
|
|
215
|
+
}
|
|
216
|
+
}]
|
|
217
|
+
}))
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
let(:apimap_sorted) { ApimapSorter.new(apimap).perform }
|
|
221
|
+
|
|
222
|
+
it 'should reorder the whole apimap without bailing out' do
|
|
223
|
+
expect(FOREST_LOGGER).not_to receive(:warn)
|
|
224
|
+
|
|
225
|
+
apimap_sorted
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'should sort the fields of the offending collection' do
|
|
229
|
+
expect(apimap_sorted['data'][0]['attributes']['fields'].map { |field| field['field'] })
|
|
230
|
+
.to eq(['id', 'meta', 'meta', 'tags', 'tags'])
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it 'should keep a deterministic order between the non-comparable types' do
|
|
234
|
+
tag_types = apimap_sorted['data'][0]['attributes']['fields']
|
|
235
|
+
.select { |field| field['field'] == 'tags' }
|
|
236
|
+
.map { |field| field['type'] }
|
|
237
|
+
expect(tag_types).to eq(['String', ['String']])
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'should keep processing the collections after the offending one' do
|
|
241
|
+
expect(apimap_sorted['data'][1]['attributes'].keys).to eq(['name', 'fields'])
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'should not leak collection attributes that are not part of the schema' do
|
|
245
|
+
expect(apimap_sorted['data'][1]['attributes']).not_to have_key('search_fields')
|
|
246
|
+
end
|
|
247
|
+
end
|
|
174
248
|
end
|
|
175
249
|
|
|
176
250
|
describe 'a smart field dependencies key' do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.22.
|
|
4
|
+
version: 9.22.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -386,11 +386,15 @@ files:
|
|
|
386
386
|
- spec/dummy/app/models/address.rb
|
|
387
387
|
- spec/dummy/app/models/application_record.rb
|
|
388
388
|
- spec/dummy/app/models/article.rb
|
|
389
|
+
- spec/dummy/app/models/boat.rb
|
|
390
|
+
- spec/dummy/app/models/captain.rb
|
|
389
391
|
- spec/dummy/app/models/car.rb
|
|
390
392
|
- spec/dummy/app/models/driver.rb
|
|
391
393
|
- spec/dummy/app/models/flag.rb
|
|
392
394
|
- spec/dummy/app/models/garage_record.rb
|
|
395
|
+
- spec/dummy/app/models/harbor.rb
|
|
393
396
|
- spec/dummy/app/models/island.rb
|
|
397
|
+
- spec/dummy/app/models/license.rb
|
|
394
398
|
- spec/dummy/app/models/location.rb
|
|
395
399
|
- spec/dummy/app/models/manufacturer.rb
|
|
396
400
|
- spec/dummy/app/models/membership.rb
|
|
@@ -446,6 +450,7 @@ files:
|
|
|
446
450
|
- spec/dummy/db/migrate/20260914120000_create_articles_and_taggings.rb
|
|
447
451
|
- spec/dummy/db/migrate/20260922120000_create_trees_users.rb
|
|
448
452
|
- spec/dummy/db/migrate/20260924120000_add_maker_name_to_products.rb
|
|
453
|
+
- spec/dummy/db/migrate/20261001120000_create_harbors_boats_captains_licenses.rb
|
|
449
454
|
- spec/dummy/db/schema.rb
|
|
450
455
|
- spec/dummy/db/user_schema.rb
|
|
451
456
|
- spec/dummy/lib/forest_liana/collections/address.rb
|
|
@@ -477,6 +482,7 @@ files:
|
|
|
477
482
|
- spec/requests/projection_header_spec.rb
|
|
478
483
|
- spec/requests/projection_inherited_loads_spec.rb
|
|
479
484
|
- spec/requests/query_footprint_spec.rb
|
|
485
|
+
- spec/requests/related_list_nested_through_spec.rb
|
|
480
486
|
- spec/requests/resources_spec.rb
|
|
481
487
|
- spec/requests/stats_spec.rb
|
|
482
488
|
- spec/requests/sti_record_conversion_spec.rb
|
|
@@ -734,11 +740,15 @@ test_files:
|
|
|
734
740
|
- spec/dummy/app/models/address.rb
|
|
735
741
|
- spec/dummy/app/models/application_record.rb
|
|
736
742
|
- spec/dummy/app/models/article.rb
|
|
743
|
+
- spec/dummy/app/models/boat.rb
|
|
744
|
+
- spec/dummy/app/models/captain.rb
|
|
737
745
|
- spec/dummy/app/models/car.rb
|
|
738
746
|
- spec/dummy/app/models/driver.rb
|
|
739
747
|
- spec/dummy/app/models/flag.rb
|
|
740
748
|
- spec/dummy/app/models/garage_record.rb
|
|
749
|
+
- spec/dummy/app/models/harbor.rb
|
|
741
750
|
- spec/dummy/app/models/island.rb
|
|
751
|
+
- spec/dummy/app/models/license.rb
|
|
742
752
|
- spec/dummy/app/models/location.rb
|
|
743
753
|
- spec/dummy/app/models/manufacturer.rb
|
|
744
754
|
- spec/dummy/app/models/membership.rb
|
|
@@ -794,6 +804,7 @@ test_files:
|
|
|
794
804
|
- spec/dummy/db/migrate/20260914120000_create_articles_and_taggings.rb
|
|
795
805
|
- spec/dummy/db/migrate/20260922120000_create_trees_users.rb
|
|
796
806
|
- spec/dummy/db/migrate/20260924120000_add_maker_name_to_products.rb
|
|
807
|
+
- spec/dummy/db/migrate/20261001120000_create_harbors_boats_captains_licenses.rb
|
|
797
808
|
- spec/dummy/db/schema.rb
|
|
798
809
|
- spec/dummy/db/user_schema.rb
|
|
799
810
|
- spec/dummy/lib/forest_liana/collections/address.rb
|
|
@@ -825,6 +836,7 @@ test_files:
|
|
|
825
836
|
- spec/requests/projection_header_spec.rb
|
|
826
837
|
- spec/requests/projection_inherited_loads_spec.rb
|
|
827
838
|
- spec/requests/query_footprint_spec.rb
|
|
839
|
+
- spec/requests/related_list_nested_through_spec.rb
|
|
828
840
|
- spec/requests/resources_spec.rb
|
|
829
841
|
- spec/requests/stats_spec.rb
|
|
830
842
|
- spec/requests/sti_record_conversion_spec.rb
|