resizing 1.2.2 → 1.3.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/Dockerfile +73 -0
  3. data/.devcontainer/compose.yaml +58 -0
  4. data/.devcontainer/devcontainer-lock.json +9 -0
  5. data/.devcontainer/devcontainer.json +32 -0
  6. data/.devcontainer/post-create.sh +41 -0
  7. data/.github/copilot-instructions.md +1 -0
  8. data/.github/labeler.yml +28 -0
  9. data/.github/release.yml +35 -0
  10. data/.github/workflows/labeler.yml +20 -0
  11. data/.github/workflows/lint.yml +37 -0
  12. data/.github/workflows/test.yml +16 -9
  13. data/AGENTS.md +1 -0
  14. data/CHANGELOG.md +3 -0
  15. data/CLAUDE.md +68 -0
  16. data/Gemfile +9 -3
  17. data/README.md +114 -2
  18. data/Rakefile +5 -7
  19. data/bin/setup +3 -0
  20. data/git-hooks/pre-push +6 -0
  21. data/git-hooks/pre-push-ruby.sh +48 -0
  22. data/lib/resizing/carrier_wave/callback_file.rb +88 -0
  23. data/lib/resizing/carrier_wave/storage/file.rb +84 -36
  24. data/lib/resizing/carrier_wave/storage/remote.rb +7 -3
  25. data/lib/resizing/carrier_wave.rb +41 -6
  26. data/lib/resizing/client.rb +0 -1
  27. data/lib/resizing/configurable.rb +1 -1
  28. data/lib/resizing/configuration.rb +40 -10
  29. data/lib/resizing/mock_client.rb +23 -7
  30. data/lib/resizing/public_id.rb +3 -2
  31. data/lib/resizing/version.rb +1 -1
  32. data/lib/resizing.rb +0 -1
  33. data/resizing.gemspec +3 -3
  34. data/test/resizing/carrier_wave/storage/file_test.rb +455 -9
  35. data/test/resizing/carrier_wave/storage/remote_test.rb +91 -9
  36. data/test/resizing/carrier_wave_cache_callbacks_test.rb +224 -0
  37. data/test/resizing/carrier_wave_callback_file_test.rb +77 -0
  38. data/test/resizing/carrier_wave_test.rb +201 -26
  39. data/test/resizing/carrier_wave_uploader_test.rb +211 -0
  40. data/test/resizing/client_test.rb +324 -25
  41. data/test/resizing/configuration_test.rb +235 -10
  42. data/test/resizing/http_clientable_test.rb +27 -0
  43. data/test/resizing/mock_client_test.rb +111 -1
  44. data/test/resizing/public_id_test.rb +96 -0
  45. data/test/resizing_module_test.rb +110 -0
  46. data/test/resizing_test.rb +8 -0
  47. data/test/test_helper.rb +297 -9
  48. metadata +33 -9
data/test/test_helper.rb CHANGED
@@ -5,6 +5,13 @@ require 'simplecov-cobertura'
5
5
 
6
6
  SimpleCov.start do
7
7
  add_filter '/test/'
8
+ # bundle install で vendor/ 配下に入る gem を計測対象から除外し、
9
+ # カバレッジ率が lib の実態を表すようにする
10
+ add_filter '/vendor/'
11
+ # version.rb is loaded by the gemspec before SimpleCov starts, so it always shows
12
+ # 0% coverage. Exclude it so that release PRs do not fail the patch check.
13
+ add_filter '/lib/resizing/version.rb'
14
+ track_files 'lib/**/*.rb'
8
15
 
9
16
  if ENV['CI']
10
17
  formatter SimpleCov::Formatter::CoberturaFormatter
@@ -20,6 +27,8 @@ SimpleCov.start do
20
27
  end
21
28
 
22
29
  $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
30
+ require 'digest'
31
+ require 'socket'
23
32
  require 'time'
24
33
  require 'timecop'
25
34
  require 'vcr'
@@ -47,6 +56,40 @@ VCR.configure do |c|
47
56
  end
48
57
  end
49
58
 
59
+ # Resizing::Client の代わりに使う偽クライアント (delete の応答を固定し、呼び出しを記録する)
60
+ class FakeApiClient
61
+ attr_reader :deleted_ids
62
+
63
+ def initialize(response)
64
+ @response = response
65
+ @deleted_ids = []
66
+ end
67
+
68
+ def delete(image_id)
69
+ @deleted_ids << image_id
70
+ @response.dup
71
+ end
72
+ end
73
+
74
+ # Resizing::Client を FakeApiClient に差し替えるヘルパー
75
+ module FakeApiClientAssertions
76
+ # Resizing::Client.new が FakeApiClient を返す状態でブロックを実行する
77
+ #
78
+ # @param response [Hash] 偽クライアントの delete が返す固定レスポンス
79
+ # @yield [FakeApiClient] 差し替えた偽クライアント
80
+ # @return [Object] ブロックの戻り値
81
+ #
82
+ # @example
83
+ # with_fake_api_client({ 'id' => IMAGE_ID }) do |fake|
84
+ # file.delete
85
+ # assert_equal [IMAGE_ID], fake.deleted_ids
86
+ # end
87
+ def with_fake_api_client(response)
88
+ fake = FakeApiClient.new(response)
89
+ Resizing::Client.stub(:new, fake) { yield fake }
90
+ end
91
+ end
92
+
50
93
  # VCRカセットのリクエストが実際に使用されたかを検証するヘルパー
51
94
  module VCRRequestAssertions
52
95
  # VCRカセット内でブロックを実行し、カセットのインタラクションがすべて使用されたことを確認
@@ -129,15 +172,172 @@ module VCRRequestAssertions
129
172
  end
130
173
  end
131
174
 
132
- ActiveRecord::Base.establish_connection(
133
- adapter: 'mysql2',
134
- host: '127.0.0.1',
135
- port: 3306,
136
- database: 'resizing_gem_test',
137
- encoding: 'utf8',
138
- username: 'root',
139
- password: 'secret'
140
- )
175
+ # 全テストで共通の Resizing の設定と、アップロードのテスト用ヘルパー
176
+ #
177
+ # 設定値と識別子は test/vcr/ のカセットに記録されているホスト・プロジェクト・
178
+ # レスポンスと揃えてあるので、変更するときはカセットも合わせて録り直すこと
179
+ module ResizingTestConfiguration
180
+ CONFIGURATION_TEMPLATE = {
181
+ image_host: 'http://192.168.56.101:5000',
182
+ project_id: 'e06e710d-f026-4dcf-b2c0-eab0de8bb83f',
183
+ secret_token: 'ewbym2r1pk49x1d2lxdbiiavnqp25j2kh00hsg3koy0ppm620x5mhlmgl3rq5ci8',
184
+ open_timeout: 10,
185
+ response_timeout: 20
186
+ }.freeze
187
+
188
+ # carrier_wave_test/save カセットが返す画像の public id
189
+ UPLOADED_IDENTIFIER = '/projects/e06e710d-f026-4dcf-b2c0-eab0de8bb83f/upload/images/' \
190
+ '14ea7aac-a194-4330-931f-6b562aec413d/v_8c5lEhDB5RT3PZp1Fn5PYGm9YVx_x0e'
191
+
192
+ def configuration_template
193
+ CONFIGURATION_TEMPLATE
194
+ end
195
+
196
+ # @param overrides [Hash] 既定の設定を上書きする値
197
+ def configure_resizing(overrides = {})
198
+ Resizing.configure = CONFIGURATION_TEMPLATE.merge(overrides)
199
+ end
200
+
201
+ def expect_identifier
202
+ UPLOADED_IDENTIFIER
203
+ end
204
+
205
+ def expect_url
206
+ "#{CONFIGURATION_TEMPLATE[:image_host]}#{UPLOADED_IDENTIFIER}"
207
+ end
208
+
209
+ # アップロード用のサンプル画像
210
+ # 実際に POST が発行されるので VCR カセット内で使うこと
211
+ def sample_uploaded_file
212
+ file = File.open('test/data/images/sample1.jpg', 'r')
213
+ ActionDispatch::Http::UploadedFile.new(
214
+ filename: File.basename(file.path),
215
+ type: 'image/jpeg',
216
+ tempfile: file
217
+ )
218
+ end
219
+
220
+ # モデルにサンプル画像を添付するヘルパーメソッド
221
+ # VCRカセット内で呼び出す必要がある
222
+ def attach_sample_image(model)
223
+ model.resizing_picture = sample_uploaded_file
224
+ end
225
+
226
+ # `remote_<column>_url=` でダウンロードされたのと同じ形のファイル
227
+ # (CarrierWave::Downloader::RemoteFile) を、HTTP を発行せずに組み立てる
228
+ #
229
+ # @param url [String] ダウンロード元 URL (ファイル名の決定に使われる)
230
+ # @param content_type [String] レスポンスの Content-Type
231
+ # @param body [String] レスポンスボディ (既定はサンプル画像)
232
+ def sample_remote_file(url, content_type:, body: File.binread('test/data/images/sample1.jpg'))
233
+ response = Net::HTTPOK.new('1.1', '200', 'OK')
234
+ response['Content-Type'] = content_type
235
+ response.instance_variable_set(:@body, body)
236
+ response.instance_variable_set(:@read, true)
237
+ response.uri = URI.parse(url)
238
+ ::CarrierWave::Downloader::RemoteFile.new(response)
239
+ end
240
+
241
+ # `remote_<column>_url=` のダウンロードを差し替えて、与えた RemoteFile を返すようにする
242
+ # (テストからは外部 HTTP を発行しないため)
243
+ #
244
+ # @param remote_file [CarrierWave::Downloader::RemoteFile] ダウンロード結果として返すファイル
245
+ def with_stubbed_download(remote_file, &block)
246
+ downloader = Object.new
247
+ downloader.define_singleton_method(:download) { |_url, _headers = {}| remote_file }
248
+ ::CarrierWave::Downloader::Base.stub(:new, downloader, &block)
249
+ end
250
+ end
251
+
252
+ # Test database setup
253
+ #
254
+ # By default each test process creates its own database and drops it when the
255
+ # run finishes, so that several test processes sharing one MySQL server do not
256
+ # overwrite each other's tables and records.
257
+ #
258
+ # The server can be overridden with MYSQL_HOST / MYSQL_PORT / MYSQL_USER /
259
+ # MYSQL_PASSWORD (the dev container runs MySQL in a separate container).
260
+ # Setting MYSQL_DATABASE uses that database as is and never drops it; give each
261
+ # concurrent process a different name in that case.
262
+ module TestDatabase
263
+ BASE_NAME = 'resizing_gem_test'
264
+
265
+ module_function
266
+
267
+ # Host identifier mixed into the per-process database name, so that PIDs from
268
+ # different hosts do not collide on a shared MySQL server
269
+ def host_token
270
+ @host_token ||= Digest::MD5.hexdigest(Socket.gethostname)[0, 8]
271
+ end
272
+
273
+ def per_process_name_pattern
274
+ /\A#{Regexp.escape(BASE_NAME)}_#{host_token}_(\d+)\z/
275
+ end
276
+
277
+ def database_name
278
+ @database_name ||= ENV['MYSQL_DATABASE'] || "#{BASE_NAME}_#{host_token}_#{Process.pid}"
279
+ end
280
+
281
+ # Whether the database belongs to this process alone and can be dropped
282
+ def ephemeral?
283
+ ENV['MYSQL_DATABASE'].nil?
284
+ end
285
+
286
+ def server_config
287
+ {
288
+ adapter: 'mysql2',
289
+ host: ENV.fetch('MYSQL_HOST', '127.0.0.1'),
290
+ port: Integer(ENV.fetch('MYSQL_PORT', '3306')),
291
+ encoding: 'utf8',
292
+ username: ENV.fetch('MYSQL_USER', 'root'),
293
+ password: ENV.fetch('MYSQL_PASSWORD', 'secret')
294
+ }
295
+ end
296
+
297
+ def setup
298
+ ActiveRecord::Base.establish_connection(server_config)
299
+ drop_abandoned_databases if ephemeral?
300
+ ActiveRecord::Base.connection.execute("create database if not exists `#{database_name}`")
301
+ ActiveRecord::Base.establish_connection(server_config.merge(database: database_name))
302
+
303
+ # Minitest.after_run, not at_exit: at_exit handlers run in reverse order of
304
+ # registration, so this one would drop the database before the tests run
305
+ Minitest.after_run { drop_database } if ephemeral?
306
+ end
307
+
308
+ def drop_database
309
+ ActiveRecord::Base.establish_connection(server_config)
310
+ ActiveRecord::Base.connection.execute("drop database if exists `#{database_name}`")
311
+ rescue StandardError => e
312
+ warn "failed to drop test database #{database_name}: #{e.message}"
313
+ end
314
+
315
+ # Clean up databases left behind by processes that were killed before they
316
+ # could drop their own
317
+ def drop_abandoned_databases
318
+ connection = ActiveRecord::Base.connection
319
+
320
+ connection.select_values('show databases').each do |database|
321
+ matched = per_process_name_pattern.match(database)
322
+ next if matched.nil? || process_alive?(matched[1].to_i)
323
+
324
+ connection.execute("drop database if exists `#{database}`")
325
+ end
326
+ rescue StandardError => e
327
+ warn "failed to drop abandoned test databases: #{e.message}"
328
+ end
329
+
330
+ def process_alive?(pid)
331
+ Process.kill(0, pid)
332
+ true
333
+ rescue Errno::ESRCH
334
+ false
335
+ rescue Errno::EPERM
336
+ true
337
+ end
338
+ end
339
+
340
+ TestDatabase.setup
141
341
 
142
342
  ActiveRecord::Schema.define do
143
343
  self.verbose = false
@@ -186,10 +386,98 @@ class ResizingUploaderWithDefaultURL < CarrierWave::Uploader::Base
186
386
  end
187
387
  end
188
388
 
389
+ # before :cache コールバック (拡張子 / content_type / サイズのチェック) のテスト用アップローダ
390
+ class ResizingUploaderWithExtensionAllowlist < CarrierWave::Uploader::Base
391
+ include Resizing::CarrierWave
392
+
393
+ def extension_allowlist
394
+ %w[png]
395
+ end
396
+ end
397
+
398
+ class ResizingUploaderWithExtensionDenylist < CarrierWave::Uploader::Base
399
+ include Resizing::CarrierWave
400
+
401
+ def extension_denylist
402
+ %w[jpg]
403
+ end
404
+ end
405
+
406
+ # 拡張子の判定が content_type から行われることのテスト用アップローダ
407
+ # (sample1.jpg / image/jpeg を受け付け、image/png は拒否する)
408
+ class ResizingUploaderWithJpegAllowlist < CarrierWave::Uploader::Base
409
+ include Resizing::CarrierWave
410
+
411
+ def extension_allowlist
412
+ %w[jpg jpeg]
413
+ end
414
+ end
415
+
416
+ class ResizingUploaderWithContentTypeAllowlist < CarrierWave::Uploader::Base
417
+ include Resizing::CarrierWave
418
+
419
+ def content_type_allowlist
420
+ ['image/png']
421
+ end
422
+ end
423
+
424
+ class ResizingUploaderWithSizeRange < CarrierWave::Uploader::Base
425
+ include Resizing::CarrierWave
426
+
427
+ def size_range
428
+ 0..10
429
+ end
430
+ end
431
+
432
+ # process! が発火しないことのテスト用アップローダ
433
+ # convert / transformation は配信 URL の transform を組み立てるための宣言であり
434
+ # uploader のメソッドとしては存在しないので、process! が走ると NoMethodError になる
435
+ class ResizingUploaderWithUndefinedProcessor < CarrierWave::Uploader::Base
436
+ include Resizing::CarrierWave
437
+
438
+ process convert: 'png'
439
+ end
440
+
189
441
  class TestModel < ::ActiveRecord::Base
190
442
  mount_uploader :resizing_picture, ResizingUploader
191
443
  end
192
444
 
445
+ class TestModelWithExtensionAllowlist < ::ActiveRecord::Base
446
+ self.table_name = 'test_models'
447
+
448
+ mount_uploader :resizing_picture, ResizingUploaderWithExtensionAllowlist
449
+ end
450
+
451
+ class TestModelWithExtensionDenylist < ::ActiveRecord::Base
452
+ self.table_name = 'test_models'
453
+
454
+ mount_uploader :resizing_picture, ResizingUploaderWithExtensionDenylist
455
+ end
456
+
457
+ class TestModelWithJpegAllowlist < ::ActiveRecord::Base
458
+ self.table_name = 'test_models'
459
+
460
+ mount_uploader :resizing_picture, ResizingUploaderWithJpegAllowlist
461
+ end
462
+
463
+ class TestModelWithContentTypeAllowlist < ::ActiveRecord::Base
464
+ self.table_name = 'test_models'
465
+
466
+ mount_uploader :resizing_picture, ResizingUploaderWithContentTypeAllowlist
467
+ end
468
+
469
+ class TestModelWithSizeRange < ::ActiveRecord::Base
470
+ self.table_name = 'test_models'
471
+
472
+ mount_uploader :resizing_picture, ResizingUploaderWithSizeRange
473
+ end
474
+
475
+ class TestModelWithUndefinedProcessor < ::ActiveRecord::Base
476
+ self.table_name = 'test_models'
477
+
478
+ mount_uploader :resizing_picture, ResizingUploaderWithUndefinedProcessor
479
+ end
480
+
193
481
  class TestJPGModel < ::ActiveRecord::Base
194
482
  mount_uploader :resizing_picture, ResizingJPGUploader
195
483
  end
metadata CHANGED
@@ -1,43 +1,49 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resizing
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Junichiro Kasuya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-31 00:00:00.000000000 Z
11
+ date: 2026-09-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '2.3'
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
25
31
  - !ruby/object:Gem::Version
26
- version: '2.3'
32
+ version: '3'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: faraday-multipart
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
37
  - - ">="
32
38
  - !ruby/object:Gem::Version
33
- version: '0'
39
+ version: '1.0'
34
40
  type: :runtime
35
41
  prerelease: false
36
42
  version_requirements: !ruby/object:Gem::Requirement
37
43
  requirements:
38
44
  - - ">="
39
45
  - !ruby/object:Gem::Version
40
- version: '0'
46
+ version: '1.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: carrierwave
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -185,10 +191,22 @@ executables: []
185
191
  extensions: []
186
192
  extra_rdoc_files: []
187
193
  files:
194
+ - ".devcontainer/Dockerfile"
195
+ - ".devcontainer/compose.yaml"
196
+ - ".devcontainer/devcontainer-lock.json"
197
+ - ".devcontainer/devcontainer.json"
198
+ - ".devcontainer/post-create.sh"
199
+ - ".github/copilot-instructions.md"
200
+ - ".github/labeler.yml"
201
+ - ".github/release.yml"
202
+ - ".github/workflows/labeler.yml"
203
+ - ".github/workflows/lint.yml"
188
204
  - ".github/workflows/test.yml"
189
205
  - ".gitignore"
190
206
  - ".rubocop.yml"
207
+ - AGENTS.md
191
208
  - CHANGELOG.md
209
+ - CLAUDE.md
192
210
  - Gemfile
193
211
  - LICENSE.txt
194
212
  - README.md
@@ -197,11 +215,14 @@ files:
197
215
  - bin/generate-changelog
198
216
  - bin/setup
199
217
  - docker-compose.yml
218
+ - git-hooks/pre-push
219
+ - git-hooks/pre-push-ruby.sh
200
220
  - lib/resizing.rb
201
221
  - lib/resizing/active_storage.rb
202
222
  - lib/resizing/active_storage/service.rb
203
223
  - lib/resizing/active_storage/service/resizing_service.rb
204
224
  - lib/resizing/carrier_wave.rb
225
+ - lib/resizing/carrier_wave/callback_file.rb
205
226
  - lib/resizing/carrier_wave/storage/file.rb
206
227
  - lib/resizing/carrier_wave/storage/remote.rb
207
228
  - lib/resizing/client.rb
@@ -218,7 +239,10 @@ files:
218
239
  - test/resizing/active_storage_service_test.rb
219
240
  - test/resizing/carrier_wave/storage/file_test.rb
220
241
  - test/resizing/carrier_wave/storage/remote_test.rb
242
+ - test/resizing/carrier_wave_cache_callbacks_test.rb
243
+ - test/resizing/carrier_wave_callback_file_test.rb
221
244
  - test/resizing/carrier_wave_test.rb
245
+ - test/resizing/carrier_wave_uploader_test.rb
222
246
  - test/resizing/client_test.rb
223
247
  - test/resizing/configurable_test.rb
224
248
  - test/resizing/configuration_test.rb
@@ -253,7 +277,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
253
277
  requirements:
254
278
  - - ">="
255
279
  - !ruby/object:Gem::Version
256
- version: 3.0.0
280
+ version: 3.1.0
257
281
  required_rubygems_version: !ruby/object:Gem::Requirement
258
282
  requirements:
259
283
  - - ">="