store_spec.rb 15 KB

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