media_attachment_spec.rb 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. # frozen_string_literal: true
  2. require 'rails_helper'
  3. RSpec.describe MediaAttachment, :attachment_processing do
  4. describe 'local?' do
  5. subject { media_attachment.local? }
  6. let(:media_attachment) { described_class.new(remote_url: remote_url) }
  7. context 'when remote_url is blank' do
  8. let(:remote_url) { '' }
  9. it 'returns true' do
  10. expect(subject).to be true
  11. end
  12. end
  13. context 'when remote_url is present' do
  14. let(:remote_url) { 'remote_url' }
  15. it 'returns false' do
  16. expect(subject).to be false
  17. end
  18. end
  19. end
  20. describe 'needs_redownload?' do
  21. subject { media_attachment.needs_redownload? }
  22. let(:media_attachment) { described_class.new(remote_url: remote_url, file: file) }
  23. context 'when file is blank' do
  24. let(:file) { nil }
  25. context 'when remote_url is present' do
  26. let(:remote_url) { 'remote_url' }
  27. it 'returns true' do
  28. expect(subject).to be true
  29. end
  30. end
  31. end
  32. context 'when file is present' do
  33. let(:file) { attachment_fixture('avatar.gif') }
  34. context 'when remote_url is blank' do
  35. let(:remote_url) { '' }
  36. it 'returns false' do
  37. expect(subject).to be false
  38. end
  39. end
  40. context 'when remote_url is present' do
  41. let(:remote_url) { 'remote_url' }
  42. it 'returns true' do
  43. expect(subject).to be false
  44. end
  45. end
  46. end
  47. end
  48. describe '#to_param' do
  49. let(:media_attachment) { Fabricate.build(:media_attachment, shortcode: shortcode, id: id) }
  50. context 'when media attachment has a shortcode' do
  51. let(:shortcode) { 'foo' }
  52. let(:id) { 123 }
  53. it 'returns shortcode' do
  54. expect(media_attachment.to_param).to eq shortcode
  55. end
  56. end
  57. context 'when media attachment does not have a shortcode' do
  58. let(:shortcode) { nil }
  59. let(:id) { 123 }
  60. it 'returns string representation of id' do
  61. expect(media_attachment.to_param).to eq id.to_s
  62. end
  63. end
  64. end
  65. shared_examples 'static 600x400 image' do |content_type, extension|
  66. after do
  67. media.destroy
  68. end
  69. it 'saves media attachment with correct file and size metadata' do
  70. expect(media)
  71. .to be_persisted
  72. .and be_processing_complete
  73. .and have_attributes(
  74. file: be_present,
  75. type: eq('image'),
  76. file_content_type: eq(content_type),
  77. file_file_name: end_with(extension)
  78. )
  79. # Rack::Mime (used by PublicFileServerMiddleware) recognizes file extension
  80. expect(Rack::Mime.mime_type(extension, nil)).to eq content_type
  81. # Strip original file name
  82. expect(media.file_file_name)
  83. .to_not start_with '600x400'
  84. # Set meta for original and thumbnail
  85. expect(media.file.meta.deep_symbolize_keys)
  86. .to include(
  87. original: include(
  88. width: eq(600),
  89. height: eq(400),
  90. aspect: eq(1.5)
  91. ),
  92. small: include(
  93. width: eq(588),
  94. height: eq(392),
  95. aspect: eq(1.5)
  96. )
  97. )
  98. end
  99. end
  100. describe 'jpeg' do
  101. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.jpeg')) }
  102. it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  103. end
  104. describe 'png' do
  105. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.png')) }
  106. it_behaves_like 'static 600x400 image', 'image/png', '.png'
  107. end
  108. describe 'monochrome jpg' do
  109. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('monochrome.png')) }
  110. it_behaves_like 'static 600x400 image', 'image/png', '.png'
  111. end
  112. describe 'webp' do
  113. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.webp')) }
  114. it_behaves_like 'static 600x400 image', 'image/webp', '.webp'
  115. end
  116. describe 'avif' do
  117. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.avif')) }
  118. it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  119. end
  120. describe 'heic' do
  121. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('600x400.heic')) }
  122. it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  123. end
  124. describe 'base64-encoded image' do
  125. let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('600x400.jpeg').read)}" }
  126. let(:media) { Fabricate(:media_attachment, file: base64_attachment) }
  127. it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  128. end
  129. describe 'animated gif' do
  130. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('avatar.gif')) }
  131. it 'sets correct file metadata' do
  132. expect(media)
  133. .to have_attributes(
  134. type: eq('gifv'),
  135. file_content_type: eq('video/mp4')
  136. )
  137. expect(media_metadata)
  138. .to include(
  139. original: include(
  140. width: eq(128),
  141. height: eq(128)
  142. )
  143. )
  144. end
  145. end
  146. describe 'static gif' do
  147. fixtures = [
  148. { filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 },
  149. { filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 },
  150. ]
  151. fixtures.each do |fixture|
  152. context fixture[:filename] do
  153. let(:media) { Fabricate(:media_attachment, file: attachment_fixture(fixture[:filename])) }
  154. it 'sets correct file metadata' do
  155. expect(media)
  156. .to have_attributes(
  157. type: eq('image'),
  158. file_content_type: eq('image/gif')
  159. )
  160. expect(media_metadata)
  161. .to include(
  162. original: include(
  163. width: eq(fixture[:width]),
  164. height: eq(fixture[:height]),
  165. aspect: eq(fixture[:aspect])
  166. )
  167. )
  168. end
  169. end
  170. end
  171. end
  172. describe 'ogg with cover art' do
  173. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.ogg')) }
  174. let(:expected_media_duration) { 0.235102 }
  175. # The libvips and ImageMagick implementations produce different results
  176. let(:expected_background_color) { Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4' }
  177. it 'sets correct file metadata' do
  178. expect(media)
  179. .to have_attributes(
  180. type: eq('audio'),
  181. thumbnail: be_present,
  182. file_file_name: not_eq('boop.ogg')
  183. )
  184. expect(media_metadata)
  185. .to include(
  186. original: include(duration: be_within(0.05).of(expected_media_duration)),
  187. colors: include(background: eq(expected_background_color))
  188. )
  189. end
  190. end
  191. describe 'mp3 with large cover art' do
  192. let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.mp3')) }
  193. let(:expected_media_duration) { 0.235102 }
  194. it 'detects file type and sets correct metadata' do
  195. expect(media)
  196. .to have_attributes(
  197. type: eq('audio'),
  198. thumbnail: be_present,
  199. file_file_name: not_eq('boop.mp3')
  200. )
  201. expect(media_metadata)
  202. .to include(
  203. original: include(duration: be_within(0.05).of(expected_media_duration))
  204. )
  205. end
  206. end
  207. it { is_expected.to validate_presence_of(:file) }
  208. describe 'size limit validation' do
  209. it 'rejects video files that are too large' do
  210. stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes
  211. stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte
  212. expect { Fabricate(:media_attachment, file: attachment_fixture('attachment.webm')) }.to raise_error(ActiveRecord::RecordInvalid)
  213. end
  214. it 'accepts video files that are small enough' do
  215. stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte
  216. stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes
  217. media = Fabricate(:media_attachment, file: attachment_fixture('attachment.webm'))
  218. expect(media.valid?).to be true
  219. end
  220. it 'rejects image files that are too large' do
  221. stub_const 'MediaAttachment::IMAGE_LIMIT', 1.kilobyte
  222. stub_const 'MediaAttachment::VIDEO_LIMIT', 100.megabytes
  223. expect { Fabricate(:media_attachment, file: attachment_fixture('attachment.jpg')) }.to raise_error(ActiveRecord::RecordInvalid)
  224. end
  225. it 'accepts image files that are small enough' do
  226. stub_const 'MediaAttachment::IMAGE_LIMIT', 100.megabytes
  227. stub_const 'MediaAttachment::VIDEO_LIMIT', 1.kilobyte
  228. media = Fabricate(:media_attachment, file: attachment_fixture('attachment.jpg'))
  229. expect(media.valid?).to be true
  230. end
  231. end
  232. describe 'cache deletion hooks' do
  233. let(:media) { Fabricate(:media_attachment) }
  234. before do
  235. allow(Rails.configuration.x).to receive(:cache_buster_enabled).and_return(true)
  236. end
  237. it 'queues CacheBusterWorker jobs' do
  238. original_path = media.file.path(:original)
  239. small_path = media.file.path(:small)
  240. expect { media.destroy }
  241. .to enqueue_sidekiq_job(CacheBusterWorker).with(original_path)
  242. .and enqueue_sidekiq_job(CacheBusterWorker).with(small_path)
  243. end
  244. end
  245. private
  246. def media_metadata
  247. media.file.meta.deep_symbolize_keys
  248. end
  249. end