standardapi 6.1.0 → 7.1.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.
- checksums.yaml +4 -4
- data/README.md +80 -58
- data/lib/standard_api/access_control_list.rb +40 -6
- data/lib/standard_api/controller.rb +96 -28
- data/lib/standard_api/helpers.rb +22 -22
- data/lib/standard_api/middleware.rb +5 -0
- data/lib/standard_api/railtie.rb +17 -0
- data/lib/standard_api/route_helpers.rb +59 -9
- data/lib/standard_api/test_case/calculate_tests.rb +7 -6
- data/lib/standard_api/test_case/destroy_tests.rb +19 -7
- data/lib/standard_api/test_case/index_tests.rb +7 -13
- data/lib/standard_api/test_case/show_tests.rb +7 -7
- data/lib/standard_api/test_case/update_tests.rb +7 -6
- data/lib/standard_api/version.rb +1 -1
- data/lib/standard_api/views/application/_record.json.jbuilder +17 -6
- data/lib/standard_api/views/application/_record.streamer +4 -3
- data/lib/standard_api/views/application/_schema.json.jbuilder +20 -8
- data/lib/standard_api/views/application/_schema.streamer +22 -8
- data/lib/standard_api.rb +1 -0
- data/test/standard_api/caching_test.rb +2 -2
- data/test/standard_api/controller/include_test.rb +107 -0
- data/test/standard_api/controller/subresource_test.rb +157 -0
- data/test/standard_api/helpers_test.rb +9 -8
- data/test/standard_api/nested_attributes/belongs_to_test.rb +71 -0
- data/test/standard_api/nested_attributes/has_and_belongs_to_many_test.rb +70 -0
- data/test/standard_api/nested_attributes/has_many_test.rb +85 -0
- data/test/standard_api/nested_attributes/has_one_test.rb +71 -0
- data/test/standard_api/route_helpers_test.rb +56 -0
- data/test/standard_api/standard_api_test.rb +110 -50
- data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +5 -1
- data/test/standard_api/test_app/app/controllers/acl/camera_acl.rb +7 -0
- data/test/standard_api/test_app/app/controllers/acl/photo_acl.rb +13 -0
- data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +7 -1
- data/test/standard_api/test_app/controllers.rb +17 -0
- data/test/standard_api/test_app/models.rb +59 -2
- data/test/standard_api/test_app/test/factories.rb +3 -0
- data/test/standard_api/test_app/views/sessions/create.json.jbuilder +1 -0
- data/test/standard_api/test_app/views/sessions/create.streamer +3 -0
- data/test/standard_api/test_app.rb +13 -1
- data/test/standard_api/test_helper.rb +100 -7
- metadata +52 -13
@@ -92,19 +92,34 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
92
92
|
assert_equal @controller.send(:model_includes), []
|
93
93
|
end
|
94
94
|
|
95
|
-
test 'Controller#model_params defaults to
|
95
|
+
test 'Controller#model_params defaults to ActionController::Parameters' do
|
96
|
+
@controller = DocumentsController.new
|
97
|
+
@controller.params = ActionController::Parameters.new
|
98
|
+
assert_equal @controller.send(:model_params), ActionController::Parameters.new
|
99
|
+
end
|
100
|
+
|
101
|
+
test 'Controller#model_params defaults to ActionController::Parameters when no resource_attributes' do
|
96
102
|
@controller = ReferencesController.new
|
97
|
-
@controller.params =
|
103
|
+
@controller.params = ActionController::Parameters.new
|
98
104
|
assert_equal @controller.send(:model_params), ActionController::Parameters.new
|
99
105
|
end
|
100
106
|
|
101
|
-
test 'Controller#
|
107
|
+
test 'Controller#mask' do
|
102
108
|
@controller = ReferencesController.new
|
103
|
-
@controller.
|
109
|
+
@controller.define_singleton_method(:mask_for) do |table_name|
|
110
|
+
{subject_id: 1}
|
111
|
+
end
|
104
112
|
@controller.params = {}
|
105
113
|
assert_equal 'SELECT "references".* FROM "references" WHERE "references"."subject_id" = 1', @controller.send(:resources).to_sql
|
106
114
|
end
|
107
115
|
|
116
|
+
test "Auto includes on a controller without a model" do
|
117
|
+
@controller = SessionsController.new
|
118
|
+
assert_nil @controller.send(:model)
|
119
|
+
post sessions_path(format: :json), params: {session: {user: 'user', pass: 'pass'}}
|
120
|
+
assert_response :ok
|
121
|
+
end
|
122
|
+
|
108
123
|
test 'ApplicationController#schema.json' do
|
109
124
|
get schema_path(format: 'json')
|
110
125
|
|
@@ -112,7 +127,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
112
127
|
controllers = ApplicationController.descendants
|
113
128
|
controllers.select! { |c| c.ancestors.include?(StandardAPI::Controller) && c != StandardAPI::Controller }
|
114
129
|
|
115
|
-
@controller.send(:models).reject { |x| x.name
|
130
|
+
@controller.send(:models).reject { |x| %w(Photo Document).include?(x.name) }.each do |model|
|
116
131
|
assert_equal true, schema['models'].has_key?(model.name)
|
117
132
|
|
118
133
|
model_comment = model.connection.table_comment(model.table_name)
|
@@ -125,7 +140,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
125
140
|
model.columns.each do |column|
|
126
141
|
assert_equal json_column_type(column.sql_type), schema.dig('models', model.name, 'attributes', column.name, 'type')
|
127
142
|
default = column.default
|
128
|
-
if default
|
143
|
+
if default
|
129
144
|
default = model.connection.lookup_cast_type_from_column(column).deserialize(default)
|
130
145
|
assert_equal default, schema.dig('models', model.name, 'attributes', column.name, 'default')
|
131
146
|
else
|
@@ -134,11 +149,15 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
134
149
|
assert_equal column.name == model.primary_key, schema.dig('models', model.name, 'attributes', column.name, 'primary_key')
|
135
150
|
assert_equal column.null, schema.dig('models', model.name, 'attributes', column.name, 'null')
|
136
151
|
assert_equal column.array, schema.dig('models', model.name, 'attributes', column.name, 'array')
|
137
|
-
if column.comment
|
152
|
+
if column.comment
|
138
153
|
assert_equal column.comment, schema.dig('models', model.name, 'attributes', column.name, 'comment')
|
139
154
|
else
|
140
155
|
assert_nil schema.dig('models', model.name, 'attributes', column.name, 'comment')
|
141
156
|
end
|
157
|
+
|
158
|
+
if column.respond_to?(:auto_populated?)
|
159
|
+
assert_equal !!column.auto_populated?, schema.dig('models', model.name, 'attributes', column.name, 'auto_populated')
|
160
|
+
end
|
142
161
|
end
|
143
162
|
end
|
144
163
|
|
@@ -163,6 +182,26 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
163
182
|
assert_nil schema['limit']
|
164
183
|
end
|
165
184
|
|
185
|
+
test 'Controller#schema.json for an enum with default' do
|
186
|
+
get schema_documents_path(format: 'json')
|
187
|
+
|
188
|
+
schema = JSON(response.body)
|
189
|
+
|
190
|
+
assert_equal true, schema.has_key?('attributes')
|
191
|
+
assert_equal 'string', schema['attributes']['level']['type']
|
192
|
+
assert_equal 'public', schema['attributes']['level']['default']
|
193
|
+
end
|
194
|
+
|
195
|
+
test 'Controller#schema.json for an enum without default' do
|
196
|
+
get schema_documents_path(format: 'json')
|
197
|
+
|
198
|
+
schema = JSON(response.body)
|
199
|
+
|
200
|
+
assert_equal true, schema.has_key?('attributes')
|
201
|
+
assert_equal 'string', schema['attributes']['rating']['type']
|
202
|
+
assert_nil schema['attributes']['rating']['default']
|
203
|
+
end
|
204
|
+
|
166
205
|
test 'Controller#index w/o limit' do
|
167
206
|
account = create(:account)
|
168
207
|
get unlimited_index_path(format: 'json')
|
@@ -188,47 +227,6 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
188
227
|
assert_redirected_to document_path(pdf)
|
189
228
|
end
|
190
229
|
|
191
|
-
test 'Controller#add_resource' do
|
192
|
-
property = create(:property, photos: [])
|
193
|
-
photo = create(:photo)
|
194
|
-
|
195
|
-
post "/properties/#{property.id}/photos/#{photo.id}"
|
196
|
-
assert_equal property.photos.reload.map(&:id), [photo.id]
|
197
|
-
assert_response :created
|
198
|
-
|
199
|
-
post "/properties/#{property.id}/photos/9999999"
|
200
|
-
assert_response :not_found
|
201
|
-
end
|
202
|
-
|
203
|
-
test 'Controller#add_resource with has_one' do
|
204
|
-
photo = create(:document)
|
205
|
-
property = create(:property)
|
206
|
-
post "/properties/#{property.id}/document/#{photo.id}"
|
207
|
-
assert_equal property.reload.document, photo
|
208
|
-
assert_response :created
|
209
|
-
end
|
210
|
-
|
211
|
-
test 'Controller#remove_resource' do
|
212
|
-
photo = create(:photo)
|
213
|
-
property = create(:property, photos: [photo])
|
214
|
-
assert_equal property.photos.reload, [photo]
|
215
|
-
delete "/properties/#{property.id}/photos/#{photo.id}"
|
216
|
-
assert_equal property.photos.reload, []
|
217
|
-
assert_response :no_content
|
218
|
-
|
219
|
-
delete "/properties/#{property.id}/photos/9999999"
|
220
|
-
assert_response :not_found
|
221
|
-
end
|
222
|
-
|
223
|
-
test 'Controller#remove_resource with has_one' do
|
224
|
-
photo = create(:document)
|
225
|
-
property = create(:property, document: photo)
|
226
|
-
assert_equal property.document, photo
|
227
|
-
delete "/properties/#{property.id}/document/#{photo.id}"
|
228
|
-
assert_nil property.reload.document
|
229
|
-
assert_response :no_content
|
230
|
-
end
|
231
|
-
|
232
230
|
# = View Tests
|
233
231
|
|
234
232
|
test 'rendering tables' do
|
@@ -236,7 +234,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
236
234
|
assert_response :ok
|
237
235
|
# assert_equal ['properties', 'accounts', 'photos', 'references', 'sessions', 'unlimited'], response.parsed_body
|
238
236
|
# Multiple 'accounts' because multiple controllers with that model for testing.
|
239
|
-
assert_equal ["properties", "accounts", "documents", "photos", "references", "accounts", 'accounts'].sort, response.parsed_body.sort
|
237
|
+
assert_equal ["properties", "accounts", "documents", "photos", "references", "accounts", 'accounts', 'uuid_models'].sort, response.parsed_body.sort
|
240
238
|
end
|
241
239
|
|
242
240
|
test 'rendering null attribute' do
|
@@ -246,6 +244,19 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
246
244
|
assert_nil JSON(response.body)['landlord']
|
247
245
|
end
|
248
246
|
|
247
|
+
test 'rendering binary attribute' do
|
248
|
+
reference = create(:reference, sha: "Hello World")
|
249
|
+
get reference_path(reference, format: 'json'), params: { id: reference.id }
|
250
|
+
assert_equal "48656c6c6f20576f726c64", JSON(response.body)['sha']
|
251
|
+
end
|
252
|
+
|
253
|
+
test 'rendering a custom binary attribute' do
|
254
|
+
reference = create(:reference, custom_binary: 2)
|
255
|
+
get reference_path(reference, format: 'json'), params: { id: reference.id }
|
256
|
+
assert_equal 2, JSON(response.body)['custom_binary']
|
257
|
+
assert_equal "\\x00000002".b,reference.custom_binary_before_type_cast
|
258
|
+
end
|
259
|
+
|
249
260
|
test 'rendering null attribute for has_one through' do
|
250
261
|
property = create(:property)
|
251
262
|
get property_path(property, format: 'json'), params: { id: property.id, include: [:document] }
|
@@ -253,6 +264,25 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
253
264
|
assert_nil JSON(response.body)['document']
|
254
265
|
end
|
255
266
|
|
267
|
+
test 'rendering serialize_attribute' do
|
268
|
+
property = create(:property, description: 'This text will magically change')
|
269
|
+
get property_path(property, format: 'json'), params: { id: property.id, magic: true }
|
270
|
+
|
271
|
+
body = JSON(response.body)
|
272
|
+
assert_equal body['description'], 'See it changed!'
|
273
|
+
end
|
274
|
+
|
275
|
+
test 'rendering an enum' do
|
276
|
+
public_document = create(:document, level: 'public')
|
277
|
+
|
278
|
+
get documents_path(format: 'json'), params: { limit: 1 }
|
279
|
+
assert_equal JSON(response.body)[0]['level'], 'public'
|
280
|
+
|
281
|
+
secret_document = create(:document, level: 'secret')
|
282
|
+
get document_path(secret_document, format: 'json')
|
283
|
+
assert_equal JSON(response.body)['level'], 'secret'
|
284
|
+
end
|
285
|
+
|
256
286
|
test '#index.json uses overridden partial' do
|
257
287
|
create(:property, photos: [create(:photo)])
|
258
288
|
get properties_path(format: 'json'), params: { limit: 100, include: [{:photos => { order: :id }}] }
|
@@ -444,7 +474,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
444
474
|
},
|
445
475
|
format: 'json')
|
446
476
|
|
447
|
-
assert_equal [photos.first
|
477
|
+
assert_equal [photos.map(&:id).sort.first], JSON(response.body)['photos'].map { |x| x['id'] }
|
448
478
|
|
449
479
|
get property_path(property,
|
450
480
|
include: {
|
@@ -696,4 +726,34 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
|
|
696
726
|
assert_equal [1], JSON(response.body)
|
697
727
|
end
|
698
728
|
|
729
|
+
test 'preloading polymorphic associations' do
|
730
|
+
p1 = create(:property)
|
731
|
+
p2 = create(:property)
|
732
|
+
c1 = create(:camera)
|
733
|
+
c2 = create(:camera)
|
734
|
+
a1 = create(:account, subject: p1, subject_cached_at: Time.now)
|
735
|
+
a2 = create(:account, subject: p2, subject_cached_at: Time.now)
|
736
|
+
a3 = create(:account, subject: c1, subject_cached_at: Time.now)
|
737
|
+
a4 = create(:account, subject: c2, subject_cached_at: Time.now)
|
738
|
+
a5 = create(:account, subject: c2, subject_cached_at: Time.now)
|
739
|
+
|
740
|
+
assert_sql(
|
741
|
+
'SELECT "properties".* FROM "properties" WHERE "properties"."id" IN ($1, $2)',
|
742
|
+
'SELECT "cameras".* FROM "cameras" WHERE "cameras"."id" IN ($1, $2)'
|
743
|
+
) do
|
744
|
+
assert_no_sql("SELECT \"properties\".* FROM \"properties\" WHERE \"properties\".\"id\" = $1 LIMIT $2") do
|
745
|
+
get accounts_path(limit: 10, include: { subject: { landlord: { when: { subject_type: 'Property' } } } }, format: 'json')
|
746
|
+
|
747
|
+
assert_equal p1.id, a1.subject_id
|
748
|
+
assert_equal p2.id, a2.subject_id
|
749
|
+
assert_equal c1.id, a3.subject_id
|
750
|
+
assert_equal p1.id, JSON(response.body).dig(0, 'subject', 'id')
|
751
|
+
assert_equal p2.id, JSON(response.body).dig(1, 'subject', 'id')
|
752
|
+
assert_equal c1.id, JSON(response.body).dig(2, 'subject', 'id')
|
753
|
+
assert_equal c2.id, JSON(response.body).dig(3, 'subject', 'id')
|
754
|
+
assert_equal c2.id, JSON(response.body).dig(4, 'subject', 'id')
|
755
|
+
end
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
699
759
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
module PropertyACL
|
2
2
|
|
3
|
+
# Attributes allowed to be updated
|
3
4
|
def attributes
|
4
5
|
[ :name,
|
5
6
|
:aliases,
|
@@ -12,16 +13,21 @@ module PropertyACL
|
|
12
13
|
]
|
13
14
|
end
|
14
15
|
|
16
|
+
# Orderings allowed
|
15
17
|
def orders
|
16
18
|
["id", "name", "aliases", "description", "constructed", "size", "created_at", "active"]
|
17
19
|
end
|
18
20
|
|
21
|
+
# Sub resources allowed to be included in the response
|
19
22
|
def includes
|
20
23
|
[ :photos, :landlord, :english_name, :document ]
|
21
24
|
end
|
22
25
|
|
26
|
+
# Sub resourced allowed to be set during create / update / delete if a user is
|
27
|
+
# allowed to ....
|
28
|
+
# only add to and from the relation, can also create or update the subresource
|
23
29
|
def nested
|
24
|
-
[ :photos ]
|
30
|
+
[ :photos, :accounts ]
|
25
31
|
end
|
26
32
|
|
27
33
|
end
|
@@ -2,6 +2,18 @@ class ApplicationController < ActionController::Base
|
|
2
2
|
include StandardAPI::Controller
|
3
3
|
include StandardAPI::AccessControlList
|
4
4
|
prepend_view_path File.join(File.dirname(__FILE__), 'views')
|
5
|
+
|
6
|
+
helper_method :serialize_attribute
|
7
|
+
|
8
|
+
def serialize_attribute(json, record, attribute, type)
|
9
|
+
value = if attribute == 'description' && params["magic"] === "true"
|
10
|
+
'See it changed!'
|
11
|
+
else
|
12
|
+
record.send(attribute)
|
13
|
+
end
|
14
|
+
|
15
|
+
json.set! attribute, type == :binary ? value&.unpack1('H*') : value
|
16
|
+
end
|
5
17
|
|
6
18
|
end
|
7
19
|
|
@@ -48,6 +60,8 @@ class ReferencesController < ApplicationController
|
|
48
60
|
end
|
49
61
|
|
50
62
|
class SessionsController < ApplicationController
|
63
|
+
def create
|
64
|
+
end
|
51
65
|
end
|
52
66
|
|
53
67
|
class UnlimitedController < ApplicationController
|
@@ -73,3 +87,6 @@ class DefaultLimitController < ApplicationController
|
|
73
87
|
end
|
74
88
|
|
75
89
|
end
|
90
|
+
|
91
|
+
class UuidModelController < ApplicationController
|
92
|
+
end
|
@@ -7,12 +7,16 @@ class Account < ActiveRecord::Base
|
|
7
7
|
end
|
8
8
|
|
9
9
|
class Photo < ActiveRecord::Base
|
10
|
-
belongs_to :account, :
|
10
|
+
belongs_to :account, counter_cache: true
|
11
11
|
has_and_belongs_to_many :properties
|
12
|
+
has_one :camera
|
12
13
|
end
|
13
14
|
|
14
15
|
class Document < ActiveRecord::Base
|
15
16
|
attr_accessor :file
|
17
|
+
|
18
|
+
enum level: { public: 0, secret: 1 }, _suffix: true
|
19
|
+
enum rating: { poor: 0, ok: 1, good: 2 }
|
16
20
|
end
|
17
21
|
|
18
22
|
class Pdf < Document
|
@@ -34,8 +38,36 @@ class Property < ActiveRecord::Base
|
|
34
38
|
end
|
35
39
|
end
|
36
40
|
|
41
|
+
class LSNType < ActiveRecord::Type::Value
|
42
|
+
|
43
|
+
def type
|
44
|
+
:lsn
|
45
|
+
end
|
46
|
+
|
47
|
+
def cast_value(value)
|
48
|
+
case value
|
49
|
+
when Integer
|
50
|
+
[value].pack('N')
|
51
|
+
else
|
52
|
+
value&.to_s&.b
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def serialize(value)
|
57
|
+
PG::TextEncoder::Bytea.new.encode(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def deserialize(value)
|
61
|
+
return nil if value.nil?
|
62
|
+
PG::TextDecoder::Bytea.new.decode(value).unpack1('N')
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
37
67
|
class Reference < ActiveRecord::Base
|
38
68
|
belongs_to :subject, polymorphic: true
|
69
|
+
|
70
|
+
attribute :custom_binary, LSNType.new
|
39
71
|
end
|
40
72
|
|
41
73
|
class Document < ActiveRecord::Base
|
@@ -47,7 +79,16 @@ class Attachment < ActiveRecord::Base
|
|
47
79
|
belongs_to :document
|
48
80
|
end
|
49
81
|
|
50
|
-
|
82
|
+
class Camera < ActiveRecord::Base
|
83
|
+
end
|
84
|
+
|
85
|
+
class UuidModel < ActiveRecord::Base
|
86
|
+
end
|
87
|
+
|
88
|
+
# = Create/recreate database and migration
|
89
|
+
task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new(ActiveRecord::Base.connection_db_config)
|
90
|
+
task.drop
|
91
|
+
task.create
|
51
92
|
|
52
93
|
class CreateModelTables < ActiveRecord::Migration[6.0]
|
53
94
|
|
@@ -93,6 +134,8 @@ class CreateModelTables < ActiveRecord::Migration[6.0]
|
|
93
134
|
create_table "references", force: :cascade do |t|
|
94
135
|
t.integer "subject_id"
|
95
136
|
t.string "subject_type", limit: 255
|
137
|
+
t.binary "sha"
|
138
|
+
t.binary "custom_binary"
|
96
139
|
t.string "key"
|
97
140
|
t.string "value"
|
98
141
|
end
|
@@ -100,6 +143,7 @@ class CreateModelTables < ActiveRecord::Migration[6.0]
|
|
100
143
|
create_table "photos_properties", force: :cascade do |t|
|
101
144
|
t.integer "photo_id"
|
102
145
|
t.integer "property_id"
|
146
|
+
t.index ["photo_id", "property_id"], unique: true
|
103
147
|
end
|
104
148
|
|
105
149
|
create_table "landlords_properties", force: :cascade do |t|
|
@@ -108,14 +152,27 @@ class CreateModelTables < ActiveRecord::Migration[6.0]
|
|
108
152
|
end
|
109
153
|
|
110
154
|
create_table "documents", force: :cascade do |t|
|
155
|
+
t.integer 'level', limit: 2, null: false, default: 0
|
156
|
+
t.integer 'rating', limit: 2
|
111
157
|
t.string 'type'
|
112
158
|
end
|
113
159
|
|
160
|
+
create_table "cameras", force: :cascade do |t|
|
161
|
+
t.integer 'photo_id'
|
162
|
+
t.string 'make'
|
163
|
+
end
|
164
|
+
|
114
165
|
create_table "attachments", force: :cascade do |t|
|
115
166
|
t.string 'record_type'
|
116
167
|
t.integer 'record_id'
|
117
168
|
t.integer 'document_id'
|
118
169
|
end
|
170
|
+
|
171
|
+
create_table "uuid_models", id: :uuid, force: :cascade do |t|
|
172
|
+
t.string 'title', default: 'recruit'
|
173
|
+
t.string 'name', default: -> { 'round(random() * 1000)' }
|
174
|
+
end
|
175
|
+
|
119
176
|
end
|
120
177
|
|
121
178
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
json.set! 'includes', includes
|
@@ -18,7 +18,7 @@ class TestApplication < Rails::Application
|
|
18
18
|
config.cache_classes = true
|
19
19
|
config.action_controller.perform_caching = true
|
20
20
|
config.cache_store = :memory_store, { size: 8.megabytes }
|
21
|
-
config.action_dispatch.show_exceptions =
|
21
|
+
config.action_dispatch.show_exceptions = :none
|
22
22
|
|
23
23
|
# if defined?(FactoryBotRails)
|
24
24
|
# config.factory_bot.definition_file_paths += [ '../factories' ]
|
@@ -28,6 +28,15 @@ end
|
|
28
28
|
# Test Application initialization
|
29
29
|
TestApplication.initialize!
|
30
30
|
|
31
|
+
# Make sure to test the right view files
|
32
|
+
ActionView::Template.unregister_template_handler :streamer, :jbuilder
|
33
|
+
case ENV["TSENCODER"]
|
34
|
+
when "turbostreamer"
|
35
|
+
ActionView::Template.register_template_handler :streamer, TurboStreamer::Handler
|
36
|
+
else
|
37
|
+
ActionView::Template.register_template_handler :jbuilder, JbuilderHandler
|
38
|
+
end
|
39
|
+
|
31
40
|
# Test Application Models
|
32
41
|
require 'standard_api/test_app/models'
|
33
42
|
|
@@ -44,6 +53,9 @@ Rails.application.routes.draw do
|
|
44
53
|
end
|
45
54
|
|
46
55
|
standard_resource :account
|
56
|
+
standard_resources :accounts, only: :index
|
57
|
+
# standard_resources :photos, only: [ :index, :show ]
|
58
|
+
|
47
59
|
end
|
48
60
|
|
49
61
|
# Test Application Helpers
|
@@ -64,6 +64,15 @@ class ActiveSupport::TestCase
|
|
64
64
|
|
65
65
|
# = Helper Methods
|
66
66
|
|
67
|
+
def debug
|
68
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
69
|
+
$debugging = true
|
70
|
+
yield
|
71
|
+
ensure
|
72
|
+
ActiveRecord::Base.logger = nil
|
73
|
+
$debugging = false
|
74
|
+
end
|
75
|
+
|
67
76
|
def controller_path
|
68
77
|
if defined?(@controller)
|
69
78
|
@controller.controller_path
|
@@ -76,16 +85,58 @@ class ActiveSupport::TestCase
|
|
76
85
|
{ :controller => controller_path, :action => action }.merge(options)
|
77
86
|
end
|
78
87
|
|
79
|
-
def assert_sql(
|
80
|
-
|
81
|
-
|
82
|
-
|
88
|
+
def assert_sql(*expected)
|
89
|
+
return_value = nil
|
90
|
+
|
91
|
+
queries_ran = if block_given?
|
92
|
+
queries_ran = SQLLogger.log.size
|
93
|
+
return_value = yield if block_given?
|
94
|
+
SQLLogger.log[queries_ran...]
|
95
|
+
else
|
96
|
+
[expected.pop]
|
83
97
|
end
|
84
98
|
|
85
|
-
|
99
|
+
failed_patterns = []
|
100
|
+
expected.each do |pattern|
|
101
|
+
failed_patterns << pattern unless queries_ran.any?{ |sql| sql_equal(pattern, sql) }
|
102
|
+
end
|
103
|
+
|
104
|
+
assert failed_patterns.empty?, <<~MSG
|
105
|
+
Query pattern(s) not found:
|
106
|
+
- #{failed_patterns.map{|l| l.gsub(/\n\s*/, " ")}.join('\n - ')}
|
107
|
+
Queries Ran (queries_ran.size):
|
108
|
+
- #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
|
109
|
+
MSG
|
110
|
+
|
111
|
+
return_value
|
112
|
+
end
|
86
113
|
|
87
|
-
|
88
|
-
|
114
|
+
def assert_no_sql(*not_expected)
|
115
|
+
return_value = nil
|
116
|
+
queries_ran = block_given? ? SQLLogger.log.size : 0
|
117
|
+
return_value = yield if block_given?
|
118
|
+
ensure
|
119
|
+
failed_patterns = []
|
120
|
+
queries_ran = SQLLogger.log[queries_ran...]
|
121
|
+
not_expected.each do |pattern|
|
122
|
+
failed_patterns << pattern if queries_ran.any?{ |sql| sql_equal(pattern, sql) }
|
123
|
+
end
|
124
|
+
assert failed_patterns.empty?, <<~MSG
|
125
|
+
Unexpected Query pattern(s) found:
|
126
|
+
- #{failed_patterns.map(&:inspect).join('\n - ')}
|
127
|
+
Queries Ran (queries_ran.size):
|
128
|
+
- #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
|
129
|
+
MSG
|
130
|
+
|
131
|
+
return_value
|
132
|
+
end
|
133
|
+
def sql_equal(expected, sql)
|
134
|
+
sql = sql.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/\s+/, ' ')
|
135
|
+
if expected.is_a?(String)
|
136
|
+
expected = Regexp.new(Regexp.escape(expected.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/\s+/, ' ')), Regexp::IGNORECASE)
|
137
|
+
end
|
138
|
+
|
139
|
+
expected.match(sql)
|
89
140
|
end
|
90
141
|
|
91
142
|
def assert_rendered(options = {}, message = nil)
|
@@ -225,6 +276,48 @@ class ActiveSupport::TestCase
|
|
225
276
|
end
|
226
277
|
end
|
227
278
|
|
279
|
+
class SQLLogger
|
280
|
+
class << self
|
281
|
+
attr_accessor :ignored_sql, :log, :log_all
|
282
|
+
def clear_log; self.log = []; self.log_all = []; end
|
283
|
+
end
|
284
|
+
|
285
|
+
self.clear_log
|
286
|
+
|
287
|
+
self.ignored_sql = [/^PRAGMA/i, /^SELECT currval/i, /^SELECT CAST/i, /^SELECT @@IDENTITY/i, /^SELECT @@ROWCOUNT/i, /^SAVEPOINT/i, /^ROLLBACK TO SAVEPOINT/i, /^RELEASE SAVEPOINT/i, /^SHOW max_identifier_length/i, /^BEGIN/i, /^COMMIT/i]
|
288
|
+
|
289
|
+
# FIXME: this needs to be refactored so specific database can add their own
|
290
|
+
# ignored SQL, or better yet, use a different notification for the queries
|
291
|
+
# instead examining the SQL content.
|
292
|
+
oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
|
293
|
+
mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
|
294
|
+
postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
|
295
|
+
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
|
296
|
+
|
297
|
+
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
|
298
|
+
ignored_sql.concat db_ignored_sql
|
299
|
+
end
|
300
|
+
|
301
|
+
attr_reader :ignore
|
302
|
+
|
303
|
+
def initialize(ignore = Regexp.union(self.class.ignored_sql))
|
304
|
+
@ignore = ignore
|
305
|
+
end
|
306
|
+
|
307
|
+
def call(name, start, finish, message_id, values)
|
308
|
+
sql = values[:sql]
|
309
|
+
|
310
|
+
# FIXME: this seems bad. we should probably have a better way to indicate
|
311
|
+
# the query was cached
|
312
|
+
return if 'CACHE' == values[:name]
|
313
|
+
|
314
|
+
self.class.log_all << sql
|
315
|
+
# puts sql
|
316
|
+
self.class.log << sql unless ignore =~ sql
|
317
|
+
end
|
318
|
+
end
|
319
|
+
ActiveSupport::Notifications.subscribe('sql.active_record', SQLLogger.new)
|
320
|
+
|
228
321
|
end
|
229
322
|
|
230
323
|
class ActionController::TestCase
|