store_spec.rb 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. # NOTE: This class uses custom .add & .remove methods
  4. # to create and destroy records.
  5. # This pattern is a strong candidate for refactoring
  6. # to make use of Rails' native ActiveRecord + callbacks functionality.
  7. RSpec.describe Store, type: :model do
  8. subject(:store) { create(:store, **attributes) }
  9. let(:attributes) do
  10. {
  11. object: 'Test',
  12. o_id: 1,
  13. data: data,
  14. filename: filename,
  15. preferences: preferences,
  16. }
  17. end
  18. let(:data) { 'hello world' }
  19. let(:filename) { 'test.txt' }
  20. let(:preferences) { {} }
  21. describe 'Class methods:' do
  22. describe '.add' do
  23. it 'creates a new Store record' do
  24. expect { create(:store, **attributes) }.to change(described_class, :count).by(1)
  25. end
  26. it 'returns the newly created Store record' do
  27. expect(create(:store, **attributes)).to eq(described_class.last)
  28. end
  29. it 'saves data to #content attribute' do
  30. expect { create(:store, **attributes) }
  31. .to change { described_class.last&.content }.to('hello world')
  32. end
  33. it 'saves filename to #filename attribute' do
  34. expect { create(:store, **attributes) }
  35. .to change { described_class.last&.filename }.to('test.txt')
  36. end
  37. it 'sets #provider attribute to "DB"' do
  38. expect { create(:store, **attributes) }
  39. .to change { described_class.last&.provider }.to('DB')
  40. end
  41. context 'with UTF-8 (non-ASCII) characters in text' do
  42. let(:data) { 'hello world äöüß' }
  43. it 'stores data as binary string to #content attribute' do
  44. expect { create(:store, **attributes) }
  45. .to change { described_class.last&.content }.to('hello world äöüß'.force_encoding('ASCII-8BIT'))
  46. end
  47. end
  48. context 'with UTF-8 (non-ASCII) characters in filename' do
  49. let(:filename) { 'testäöüß.txt' }
  50. it 'stores filename verbatim to #filename attribute' do
  51. expect { create(:store, **attributes) }
  52. .to change { described_class.last&.filename }.to('testäöüß.txt')
  53. end
  54. end
  55. context 'with binary data' do
  56. let(:data) { Rails.root.join('test/data/pdf/test1.pdf').binread }
  57. it 'stores data as binary string to #content attribute' do
  58. expect { create(:store, **attributes) }
  59. .to change { described_class.last&.content&.class }.to(String)
  60. .and change { described_class.last&.content }.to(data)
  61. end
  62. it 'saves filename to #filename attribute' do
  63. expect { create(:store, **attributes) }
  64. .to change { described_class.last&.filename }.to('test.txt')
  65. end
  66. it 'sets #provider attribute to "DB"' do
  67. expect { create(:store, **attributes) }
  68. .to change { described_class.last&.provider }.to('DB')
  69. end
  70. context 'when an identical file has been stored before under a different name' do
  71. before { create(:store, **attributes) }
  72. it 'creates a new (duplicate) described_class record' do
  73. expect { create(:store, **attributes.merge(filename: 'test-again.pdf')) }
  74. .to change(described_class, :count).by(1)
  75. .and change { described_class.last&.filename }.to('test-again.pdf')
  76. .and not_change { described_class.last&.content&.class }
  77. .and not_change { described_class.last&.content }
  78. end
  79. end
  80. end
  81. context 'with an image (jpeg/jpg/png)' do
  82. let(:data) { Rails.root.join('test/data/upload/upload2.jpg').binread }
  83. let(:preferences) { { content_type: 'image/jpg' } }
  84. it 'generates previews' do
  85. create(:store, **attributes)
  86. expect(described_class.last.preferences)
  87. .to include(resizable: true, content_inline: true, content_preview: true)
  88. end
  89. context 'when system is in import mode' do
  90. before { Setting.set('import_mode', true) }
  91. it 'does not generate previews' do
  92. create(:store, **attributes)
  93. expect(described_class.last.preferences)
  94. .not_to include(resizable: true, content_inline: true, content_preview: true)
  95. end
  96. end
  97. end
  98. end
  99. describe '.remove' do
  100. before { create(:store, **attributes) }
  101. it 'destroys the specified Store record' do
  102. expect { described_class.remove(object: 'Test', o_id: 1) }
  103. .to change(described_class, :count).by(-1)
  104. end
  105. it 'destroys the associated Store::File record' do
  106. expect { described_class.remove(object: 'Test', o_id: 1) }
  107. .to change(described_class::File, :count).by(-1)
  108. end
  109. context 'with the same file stored under multiple o_ids' do
  110. before { create(:store, **attributes.merge(o_id: 2)) }
  111. it 'destroys only the specified Store record' do
  112. expect { described_class.remove(object: 'Test', o_id: 1) }
  113. .to change(described_class, :count).by(-1)
  114. end
  115. it 'does not destroy the associated Store::File record (because it is referenced by another Store)' do
  116. expect { described_class.remove(object: 'Test', o_id: 1) }
  117. .not_to change(Store::File, :count)
  118. end
  119. end
  120. context 'with multiple files stored under the same o_id' do
  121. before { create(:store, **attributes.merge(data: 'bar')) }
  122. it 'destroys all matching Store records' do
  123. expect { described_class.remove(object: 'Test', o_id: 1) }
  124. .to change(described_class, :count).by(-2)
  125. end
  126. it 'destroys all associated Store::File records' do
  127. expect { described_class.remove(object: 'Test', o_id: 1) }
  128. .to change(Store::File, :count).by(-2)
  129. end
  130. end
  131. end
  132. describe '.list' do
  133. let!(:store) do
  134. create(:store,
  135. object: 'Test',
  136. o_id: 1,
  137. data: 'hello world',
  138. filename: 'test.txt')
  139. end
  140. it 'runs a Store.where query for :object / :o_id parameters (:object is Store::Object association name)' do
  141. expect(described_class.list(object: 'Test', o_id: 1))
  142. .to eq([store])
  143. end
  144. context 'without a Store::Object name' do
  145. it 'returns an empty ActiveRecord::Relation' do
  146. expect(described_class.list(o_id: 1))
  147. .to be_an(ActiveRecord::Relation).and be_empty
  148. end
  149. end
  150. context 'without a #o_id' do
  151. it 'returns an empty ActiveRecord::Relation' do
  152. expect(described_class.list(object: 'Test'))
  153. .to be_an(ActiveRecord::Relation).and be_empty
  154. end
  155. end
  156. end
  157. end
  158. describe 'Instance methods:' do
  159. describe 'image previews (#content_inline / #content_preview)' do
  160. let(:attributes) do
  161. {
  162. object: 'Test',
  163. o_id: 1,
  164. data: data,
  165. filename: 'test1.pdf',
  166. preferences: {
  167. content_type: content_type,
  168. content_id: 234,
  169. }
  170. }
  171. end
  172. let(:resized_inline_image) do
  173. File.binwrite(temp_file, store.content_inline)
  174. Rszr::Image.load(temp_file)
  175. end
  176. let(:resized_preview_image) do
  177. File.binwrite(temp_file.next, store.content_preview)
  178. Rszr::Image.load(temp_file.next)
  179. end
  180. let(:temp_file) { Tempfile.new.path }
  181. context 'with content_type: "text/plain"' do
  182. let(:content_type) { 'text/plain' }
  183. context 'and text content' do
  184. let(:data) { 'foo' }
  185. it 'cannot be resized (neither inlined nor previewed)' do
  186. expect { store.content_inline }
  187. .to raise_error('Inline content could not be generated.')
  188. expect { store.content_preview }
  189. .to raise_error('Content preview could not be generated.')
  190. expect(store.preferences)
  191. .to not_include(resizable: true)
  192. .and not_include(content_inline: true)
  193. .and not_include(content_preview: true)
  194. end
  195. end
  196. end
  197. context 'with content_type: "image/*"' do
  198. context 'and text content' do
  199. let(:content_type) { 'image/jpeg' }
  200. let(:data) { 'foo' }
  201. it 'cannot be resized (neither inlined nor previewed)' do
  202. expect { store.content_inline }
  203. .to raise_error('Inline content could not be generated.')
  204. expect { store.content_preview }
  205. .to raise_error('Content preview could not be generated.')
  206. expect(store.preferences)
  207. .to not_include(resizable: true)
  208. .and not_include(content_inline: true)
  209. .and not_include(content_preview: true)
  210. end
  211. end
  212. context 'with image content (width > 1800px)' do
  213. context 'width <= 200px' do
  214. let(:content_type) { 'image/png' }
  215. let(:data) { Rails.root.join('test/data/image/1x1.png').binread }
  216. it 'cannot be resized (neither inlined nor previewed)' do
  217. expect { store.content_inline }
  218. .to raise_error('Inline content could not be generated.')
  219. expect { store.content_preview }
  220. .to raise_error('Content preview could not be generated.')
  221. expect(store.preferences)
  222. .to not_include(resizable: true)
  223. .and not_include(content_inline: true)
  224. .and not_include(content_preview: true)
  225. end
  226. end
  227. context '200px < width <= 1800px)' do
  228. let(:content_type) { 'image/png' }
  229. let(:data) { Rails.root.join('test/data/image/1000x1000.png').binread }
  230. it 'can be resized (previewed but not inlined)' do
  231. expect { store.content_inline }
  232. .to raise_error('Inline content could not be generated.')
  233. expect(resized_preview_image.width).to eq(200)
  234. expect(store.preferences)
  235. .to include(resizable: true)
  236. .and not_include(content_inline: true)
  237. .and include(content_preview: true)
  238. end
  239. end
  240. context '1800px < width' do
  241. let(:content_type) { 'image/jpeg' }
  242. let(:data) { Rails.root.join('test/data/upload/upload2.jpg').binread }
  243. it 'can be resized (inlined @ 1800px wide or previewed @ 200px wide)' do
  244. expect(resized_inline_image.width).to eq(1800)
  245. expect(resized_preview_image.width).to eq(200)
  246. expect(store.preferences)
  247. .to include(resizable: true)
  248. .and include(content_inline: true)
  249. .and include(content_preview: true)
  250. end
  251. context 'kind of wide/short: 8000x300' do
  252. let(:data) { Rails.root.join('test/data/image/8000x300.jpg').binread }
  253. it 'can be resized (inlined @ 1800px wide or previewed @ 200px wide)' do
  254. expect(resized_inline_image.width).to eq(1800)
  255. expect(resized_preview_image.width).to eq(200)
  256. expect(store.preferences)
  257. .to include(resizable: true)
  258. .and include(content_inline: true)
  259. .and include(content_preview: true)
  260. end
  261. end
  262. context 'very wide/short: 4000x1; i.e., <= 6px vertically per 200px (preview) or 1800px (inline) horizontally' do
  263. let(:data) { Rails.root.join('test/data/image/4000x1.jpg').binread }
  264. it 'cannot be resized (neither inlined nor previewed)' do
  265. expect { store.content_inline }
  266. .to raise_error('Inline content could not be generated.')
  267. expect { store.content_preview }
  268. .to raise_error('Content preview could not be generated.')
  269. expect(store.preferences)
  270. .to not_include(resizable: true)
  271. .and not_include(content_inline: true)
  272. .and not_include(content_preview: true)
  273. end
  274. end
  275. context 'very wide/short: 8000x25; i.e., <= 6px vertically per 200px (preview) or 1800px (inline) horizontally' do
  276. let(:data) { Rails.root.join('test/data/image/8000x25.jpg').binread }
  277. it 'cannot be resized (neither inlined nor previewed)' do
  278. expect { store.content_inline }
  279. .to raise_error('Inline content could not be generated.')
  280. expect { store.content_preview }
  281. .to raise_error('Content preview could not be generated.')
  282. expect(store.preferences)
  283. .to not_include(resizable: true)
  284. .and not_include(content_inline: true)
  285. .and not_include(content_preview: true)
  286. end
  287. end
  288. end
  289. end
  290. end
  291. end
  292. describe '#inline?' do
  293. context 'when Content-Disposition is inline' do
  294. let(:preferences) { { 'Content-Disposition' => 'inline' } }
  295. it { is_expected.to be_inline }
  296. end
  297. context 'when Content-Disposition not inline' do
  298. it { is_expected.not_to be_inline }
  299. end
  300. end
  301. end
  302. context 'when preferences exceed storage size' do
  303. let(:valid_entries) do
  304. {
  305. content_type: 'text/plain',
  306. content_id: 234,
  307. }
  308. end
  309. shared_examples 'keeps other entries' do
  310. context 'when other entries are present' do
  311. let(:preferences) do
  312. super().merge(valid_entries)
  313. end
  314. it 'keeps these entries' do
  315. expect(store.preferences).to include(valid_entries)
  316. end
  317. end
  318. end
  319. context 'when single content is oversized' do
  320. let(:preferences) do
  321. {
  322. oversized_content: '0' * 2500,
  323. }
  324. end
  325. it 'removes that entry' do
  326. expect(store.preferences).not_to have_key(:oversized_content)
  327. end
  328. include_examples 'keeps other entries'
  329. end
  330. context 'when the sum of multiple contents is oversized' do
  331. let(:preferences) do
  332. {
  333. oversized_content1: '0' * 2000,
  334. oversized_content2: '0' * 2000,
  335. }
  336. end
  337. it 'removes first entry' do
  338. expect(store.preferences).not_to have_key(:oversized_content1)
  339. end
  340. it 'keeps second entry' do
  341. expect(store.preferences).to have_key(:oversized_content2)
  342. end
  343. include_examples 'keeps other entries'
  344. end
  345. context 'when single key is oversized' do
  346. let(:oversized_key) { '0' * 2500 }
  347. let(:preferences) do
  348. {
  349. oversized_key => 'valid content',
  350. }
  351. end
  352. it 'removes that entry' do
  353. expect(store.preferences).not_to have_key(oversized_key)
  354. end
  355. include_examples 'keeps other entries'
  356. end
  357. context 'when the sum of multiple keys is oversized' do
  358. let(:oversized_key1) { '0' * 1500 }
  359. let(:oversized_key2) { '1' * 1500 }
  360. let(:preferences) do
  361. {
  362. oversized_key1 => 'valid content',
  363. oversized_key2 => 'valid content',
  364. }
  365. end
  366. it 'removes first entry' do
  367. expect(store.preferences).not_to have_key(oversized_key1)
  368. end
  369. it 'keeps second entry' do
  370. expect(store.preferences).to have_key(oversized_key2)
  371. end
  372. include_examples 'keeps other entries'
  373. end
  374. end
  375. end