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