Refactoring: Migrate store_test to RSpec.

This commit is contained in:
Ryan Lue 2021-07-09 16:38:23 +00:00 committed by Martin Gruner
parent 7e1732e6b9
commit a8f028164d
8 changed files with 734 additions and 555 deletions

View File

@ -122,6 +122,9 @@ RSpec/ContextWording:
- 'spec/models/role_spec.rb'
- 'spec/models/scheduler_spec.rb'
- 'spec/models/smime_certificate_spec.rb'
- 'spec/models/store/file_spec.rb'
- 'spec/models/store/provider/file_spec.rb'
- 'spec/models/store_spec.rb'
- 'spec/models/tag/item_spec.rb'
- 'spec/models/tag_spec.rb'
- 'spec/models/taskbar_spec.rb'
@ -256,6 +259,9 @@ RSpec/ExampleLength:
- 'spec/models/role_spec.rb'
- 'spec/models/scheduler_spec.rb'
- 'spec/models/sla/has_escalation_calculation_impact_examples.rb'
- 'spec/models/store/file_spec.rb'
- 'spec/models/store/provider/file_spec.rb'
- 'spec/models/store_spec.rb'
- 'spec/models/taskbar_spec.rb'
- 'spec/models/ticket/article_spec.rb'
- 'spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb'
@ -537,6 +543,9 @@ RSpec/MultipleExpectations:
- 'spec/models/session_spec.rb'
- 'spec/models/sla/has_escalation_calculation_impact_examples.rb'
- 'spec/models/smime_certificate_spec.rb'
- 'spec/models/store/file_spec.rb'
- 'spec/models/store/provider/file_spec.rb'
- 'spec/models/store_spec.rb'
- 'spec/models/taskbar_spec.rb'
- 'spec/models/ticket/article_spec.rb'
- 'spec/models/ticket/article/has_ticket_contact_attributes_impact_examples.rb'
@ -617,6 +626,9 @@ RSpec/NestedGroups:
- 'spec/models/channel/driver/twitter_spec.rb'
- 'spec/models/channel/email_parser_spec.rb'
- 'spec/models/job_spec.rb'
- 'spec/models/store/file_spec.rb'
- 'spec/models/store/provider/file_spec.rb'
- 'spec/models/store_spec.rb'
- 'spec/models/token_spec.rb'
- 'spec/models/trigger_spec.rb'
- 'spec/models/user/has_ticket_create_screen_impact_examples.rb'

View File

@ -5,6 +5,8 @@ class Store < ApplicationModel
belongs_to :store_object, class_name: 'Store::Object', optional: true
belongs_to :store_file, class_name: 'Store::File', optional: true
delegate :content, to: :store_file
delegate :provider, to: :store_file
validates :filename, presence: true
@ -142,28 +144,6 @@ remove one attachment from storage
=begin
get content of file
store = Store.find(store_id)
content_as_string = store.content
returns
content_as_string
=end
def content
file = Store::File.find_by(id: store_file_id)
if !file
raise "No such file #{store_file_id}!"
end
file.content
end
=begin
get content of file in preview size
store = Store.find(store_id)
@ -241,15 +221,6 @@ returns
slice :id, :filename, :size, :preferences
end
def provider
file = Store::File.find_by(id: store_file_id)
if !file
raise "No such file #{store_file_id}!"
end
file.provider
end
RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i.freeze
def self.resizable_mime?(input)

View File

@ -78,11 +78,8 @@ in case of fixing sha hash use:
def self.verify(fix_it = nil)
success = true
file_ids = Store::File.all.pluck(:id)
file_ids.each do |item_id|
item = Store::File.find(item_id)
content = item.content
sha = Digest::SHA256.hexdigest(content)
Store::File.find_each(batch_size: 10) do |item|
sha = Digest::SHA256.hexdigest(item.content)
logger.info "CHECK: Store::File.find(#{item.id})"
next if sha == item.sha
@ -90,9 +87,7 @@ in case of fixing sha hash use:
logger.error "DIFF: sha diff of Store::File.find(#{item.id}) current:#{sha}/db:#{item.sha}/provider:#{item.provider}"
store = Store.find_by(store_file_id: item.id)
logger.error "STORE: #{store.inspect}"
if fix_it
item.update_attribute(:sha, sha) # rubocop:disable Rails/SkipsModelValidations
end
item.update_attribute(:sha, sha) if fix_it # rubocop:disable Rails/SkipsModelValidations
end
success
end
@ -119,26 +114,16 @@ nice move to keep system responsive
adapter_source = "Store::Provider::#{source}".constantize
adapter_target = "Store::Provider::#{target}".constantize
file_ids = Store::File.all.pluck(:id)
file_ids.each do |item_id|
item = Store::File.find(item_id)
next if item.provider == target
content = item.content
# add to new provider
adapter_target.add(content, item.sha)
# update meta data
Store::File.where(provider: source).find_each(batch_size: 10) do |item|
adapter_target.add(item.content, item.sha)
item.update_attribute(:provider, target) # rubocop:disable Rails/SkipsModelValidations
# remove from old provider
adapter_source.delete(item.sha)
logger.info "Moved file #{item.sha} from #{source} to #{target}"
sleep delay if delay
end
true
end

View File

@ -4,117 +4,65 @@ class Store::Provider::File
# write file to fs
def self.add(data, sha)
# install file
location = get_location(sha)
permission = '600'
# verify if file already is in file system and if it's not corrupt
if File.exist?(location)
begin
get(sha)
rescue
delete(sha)
end
end
# write file to file system
if !File.exist?(location)
Rails.logger.debug { "storage write '#{location}' (#{permission})" }
file = File.new(location, 'wb')
file.write(data)
file.close
end
File.chmod(permission.to_i(8), location)
# check sha
local_sha = Digest::SHA256.hexdigest(get(sha))
if sha != local_sha
raise "Corrupt file in fs #{location}, sha should be #{sha} but is #{local_sha}"
Rails.logger.debug { "storge write '#{location}' (600)" }
File.binwrite(location, data)
end
true
File.chmod(0o600, location)
validate_file(sha)
rescue # .validate_file will raise an error if contents do not match SHA
delete(sha)
fail_count ||= 0
fail_count.zero? ? (fail_count += 1) && retry : raise
end
# read file from fs
def self.get(sha)
location = get_location(sha)
Rails.logger.debug { "read from fs #{location}" }
if !File.exist?(location)
raise "No such file #{location}"
end
data = File.open(location, 'rb')
content = data.read
Rails.logger.debug { "read from fs #{location}" }
content = File.binread(location)
local_sha = Digest::SHA256.hexdigest(content)
# check sha
local_sha = Digest::SHA256.hexdigest(content)
if local_sha != sha
raise "Corrupt file in fs #{location}, sha should be #{sha} but is #{local_sha}"
end
raise "File corrupted: path #{location} does not match SHA digest (#{local_sha})" if local_sha != sha
content
end
class << self
alias validate_file get
end
# unlink file from fs
def self.delete(sha)
location = get_location(sha)
if File.exist?(location)
Rails.logger.info "storage remove '#{location}'"
File.delete(location)
end
# check if dir need to be removed
locations = location.split('/')
(0..locations.count).reverse_each do |count|
local_location = locations[0, count].join('/')
break if local_location.match?(%r{storage/fs/{0,4}$})
break if Dir["#{local_location}/*"].present?
next if !Dir.exist?(local_location)
# remove empty ancestor directories
storage_fs_path = Rails.root.join('storage/fs')
location.parent.ascend do |path|
break if !Dir.empty?(path)
break if path == storage_fs_path
FileUtils.rmdir(local_location)
Dir.rmdir(path)
end
end
# generate file location
def self.get_location(sha)
# generate directory
base = Rails.root.join('storage/fs').to_s
parts = []
length1 = 4
length2 = 5
length3 = 7
last_position = 0
# rubocop:disable Style/CombinableLoops
(0..1).each do |_count|
end_position = last_position + length1
parts.push sha[last_position, length1]
last_position = end_position
end
(0..1).each do |_count|
end_position = last_position + length2
parts.push sha[last_position, length2]
last_position = end_position
end
(0..1).each do |_count|
end_position = last_position + length3
parts.push sha[last_position, length3]
last_position = end_position
end
# rubocop:enable Style/CombinableLoops
path = "#{parts[ 0..6 ].join('/')}/"
file = sha[last_position, sha.length]
location = "#{base}/#{path}"
# create directory if not exists
if !File.exist?(location)
FileUtils.mkdir_p(location)
end
full_path = location + file
full_path.gsub('//', '/')
parts = sha.scan(%r{^(.{4})(.{4})(.{5})(.{5})(.{7})(.{7})(.*)}).first
Rails.root.join('storage/fs', *parts).tap { |path| FileUtils.mkdir_p(path.parent) }
end
end

View File

@ -0,0 +1,95 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'rails_helper'
RSpec.describe Store::File, type: :model do
subject(:file) { described_class.add('foo') }
describe '.add' do
context 'with no preconfigured storage provider' do
before { Setting.set('storage_provider', nil) }
it 'defaults to the "DB" provider' do
expect(file.provider).to eq('DB')
end
end
context 'with a preconfigured storage provider' do
before { Setting.set('storage_provider', 'File') }
after { Store::Provider::File.delete(Digest::SHA256.hexdigest('foo')) }
it 'defaults to the "DB" provider' do
expect(file.provider).to eq('File')
end
end
end
describe '.verify' do
context 'when no Store::File records exist' do
it 'returns true' do
expect(described_class.verify).to be(true)
end
end
context 'when all Store::File records have matching #content / #sha attributes' do
before do
file # create Store::File record
end
it 'returns true' do
expect(described_class.verify).to be(true)
end
end
context 'when at least one Store::File records #content / #sha attributes do not match' do
before do
file # create Store::File record
Store::Provider::DB.last.update(data: 'bar')
end
it 'returns false' do
expect(described_class.verify).to be(false)
end
end
end
describe '.move' do
before { Setting.set('storage_provider', nil) }
after { Store::Provider::File.delete(Digest::SHA256.hexdigest('foo')) }
let(:storage_path) { Rails.root.join('storage/fs') }
it 'replaces all Store::Provider::{source} records with Store::Provider::{target} ones' do
file # create Store::File record
expect { described_class.move('DB', 'File') }
.to change { file.reload.provider }.to('File')
.and change { Store::Provider::DB.count }.by(-1)
.and change { Dir[storage_path.join('**', '*')].select { |entry| File.file?(entry) }.count }.by(1)
end
context 'when no Store::File records of the source type exist' do
it 'makes no changes and returns true' do
file # create Store::File record
expect { described_class.move('File', 'DB') }
.not_to change { file.reload.provider }
end
end
context 'when moving from "File" adapter to "DB"' do
before { Setting.set('storage_provider', 'File') }
it 'removes stored files from filesystem' do
file # create Store::File record
expect { described_class.move('File', 'DB') }
.to change { file.reload.provider }.to('DB')
.and change { Store::Provider::DB.count }.by(1)
.and change { Dir[storage_path.join('*')].count }.by(-1)
end
end
end
end

View File

@ -0,0 +1,124 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'rails_helper'
RSpec.describe Store::Provider::File do
before { FileUtils.rm_rf(Rails.root.join('storage/fs', sha[0, 4])) }
after { FileUtils.rm_rf(Rails.root.join('storage/fs', sha[0, 4])) }
let(:data) { 'foo' }
let(:sha) { Digest::SHA256.hexdigest(data) }
let(:filepath) { Rails.root.join('storage/fs/2c26/b46b/68ffc/68ff9/9b453c1/d304134/13422d706483bfa0f98a5e886266e7ae') }
describe '.get_location' do
context 'with a valid SHA256 digest' do
let(:sha) { '0000111122222333334444444555555566666666666666666666666666666666' }
it 'returns a Pathname matching the SHA digest (split into chunks of 4, 4, 5, 5, 7, 7, & 32 chars)' do
expect(described_class.get_location(sha))
.to eq(Rails.root.join('storage/fs/0000/1111/22222/33333/4444444/5555555/66666666666666666666666666666666'))
end
end
end
describe '.add' do
context 'when no matching file exists' do
it 'writes the file to disk' do
expect { described_class.add(data, sha) }
.to change { File.exist?(filepath) }.to(true)
expect(File.read(filepath)).to eq(data)
end
it 'sets permissions on the new file to 600' do
described_class.add(data, sha)
expect(File.stat(filepath).mode & 0o777).to eq(0o600)
end
end
context 'when a matching file exists' do
before { FileUtils.mkdir_p(filepath.parent) }
context 'and its contents match the SHA digest of its filepath' do
before do
File.write(filepath, 'foo')
File.chmod(0o755, filepath)
end
it 'sets file permissions to 600' do
expect { described_class.add(data, sha) }
.to change { File.stat(filepath).mode & 0o777 }.to(0o600)
end
end
context 'and its contents do NOT match the SHA digest of its filepath' do
before { File.write(filepath, 'bar') }
it 'replaces the corrupt file with the specified contents' do
expect { described_class.add(data, sha) }
.to change { File.read(filepath) }.to('foo')
end
end
end
end
describe '.get' do
context 'when a file exists for the given SHA digest' do
before { FileUtils.mkdir_p(filepath.parent) }
context 'and its contents match the digest' do
before { File.write(filepath, data) }
it 'returns the contents of the file' do
expect(described_class.get(sha)).to eq('foo')
end
end
context 'and its contents do NOT match the digest' do
before { File.write(filepath, 'bar') }
it 'raises an error' do
expect { described_class.get(sha) }
.to raise_error(StandardError)
end
end
end
context 'when NO file exists for the given SHA digest' do
it 'raises an error' do
expect { described_class.get(sha) }
.to raise_error(Errno::ENOENT)
end
end
end
describe '.delete' do
before do
FileUtils.mkdir_p(filepath.parent)
File.write(filepath, data)
end
it 'deletes the file' do
expect { described_class.delete(sha) }
.to change { File.exist?(filepath) }.to(false)
end
context 'when the files parent directories contain other files' do
before { FileUtils.touch(filepath.parent.join('baz')) }
it 'leaves non-empty subdirectories in place' do
expect { described_class.delete(sha) }
.not_to change { Dir.exist?(filepath.parent) }
end
end
context 'when the files parent directories contain no other files' do
it 'deletes empty parent subdirectories, up to /storage/fs' do
expect { described_class.delete(sha) }
.to change { Dir.empty?(Rails.root.join('storage/fs')) }.to(true)
end
end
end
end

468
spec/models/store_spec.rb Normal file
View File

@ -0,0 +1,468 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'rails_helper'
# NOTE: This class uses custom .add & .remove methods
# to create and destroy records.
# This pattern is a strong candidate for refactoring
# to make use of Rails' native ActiveRecord + callbacks functionality.
RSpec.describe Store, type: :model do
subject(:store) { described_class.add(**attributes) }
let(:attributes) do
{
object: 'Test',
o_id: 1,
data: data,
filename: filename,
preferences: preferences,
created_by_id: 1,
}
end
let(:data) { 'hello world' }
let(:filename) { 'test.txt' }
let(:preferences) { {} }
describe 'Class methods:' do
describe '.add' do
it 'creates a new Store record' do
expect { described_class.add(**attributes) }.to change(described_class, :count).by(1)
end
it 'returns the newly created Store record' do
expect(described_class.add(**attributes)).to eq(described_class.last)
end
it 'saves data to #content attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.content }.to('hello world')
end
it 'saves filename to #filename attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.filename }.to('test.txt')
end
it 'sets #provider attribute to "DB"' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.provider }.to('DB')
end
context 'with UTF-8 (non-ASCII) characters in text' do
let(:data) { 'hello world äöüß' }
it 'stores data as binary string to #content attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.content }.to('hello world äöüß'.force_encoding('ASCII-8BIT'))
end
end
context 'with UTF-8 (non-ASCII) characters in filename' do
let(:filename) { 'testäöüß.txt' }
it 'stores filename verbatim to #filename attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.filename }.to('testäöüß.txt')
end
end
context 'with binary data' do
let(:data) { File.binread(Rails.root.join('test/data/pdf/test1.pdf')) }
it 'stores data as binary string to #content attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.content&.class }.to(String)
.and change { described_class.last&.content }.to(data)
end
it 'saves filename to #filename attribute' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.filename }.to('test.txt')
end
it 'sets #provider attribute to "DB"' do
expect { described_class.add(**attributes) }
.to change { described_class.last&.provider }.to('DB')
end
context 'when an identical file has been stored before under a different name' do
before { described_class.add(**attributes) }
it 'creates a new (duplicate) described_class record' do
expect { described_class.add(**attributes.merge(filename: 'test-again.pdf')) }
.to change(described_class, :count).by(1)
.and change { described_class.last&.filename }.to('test-again.pdf')
.and not_change { described_class.last&.content&.class }
.and not_change { described_class.last&.content }
end
end
end
context 'with an image (jpeg/jpg/png)' do
let(:data) { File.binread(Rails.root.join('test/data/upload/upload2.jpg')) }
let(:preferences) { { content_type: 'image/jpg' } }
it 'generates previews' do
described_class.add(**attributes)
expect(described_class.last.preferences)
.to include(resizable: true, content_inline: true, content_preview: true)
end
context 'when system is in import mode' do
before { Setting.set('import_mode', true) }
it 'does not generate previews' do
described_class.add(**attributes)
expect(described_class.last.preferences)
.not_to include(resizable: true, content_inline: true, content_preview: true)
end
end
end
end
describe '.remove' do
before { described_class.add(**attributes) }
it 'destroys the specified Store record' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.to change(described_class, :count).by(-1)
end
it 'destroys the associated Store::File record' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.to change { described_class::File.count }.by(-1)
end
context 'with the same file stored under multiple o_ids' do
before { described_class.add(**attributes.merge(o_id: 2)) }
it 'destroys only the specified Store record' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.to change(described_class, :count).by(-1)
end
it 'does not destroy the associated Store::File record (because it is referenced by another Store)' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.not_to change { Store::File.count }
end
end
context 'with multiple files stored under the same o_id' do
before { described_class.add(**attributes.merge(data: 'bar')) }
it 'destroys all matching Store records' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.to change(described_class, :count).by(-2)
end
it 'destroys all associated Store::File records' do
expect { described_class.remove(object: 'Test', o_id: 1) }
.to change { Store::File.count }.by(-2)
end
end
end
describe '.list' do
let!(:store) do
described_class.add(
object: 'Test',
o_id: 1,
data: 'hello world',
filename: 'test.txt',
preferences: {},
created_by_id: 1,
)
end
it 'runs a Store.where query for :object / :o_id parameters (:object is Store::Object association name)' do
expect(described_class.list(object: 'Test', o_id: 1))
.to eq([store])
end
context 'without a Store::Object name' do
it 'returns an empty ActiveRecord::Relation' do
expect(described_class.list(o_id: 1))
.to be_an(ActiveRecord::Relation).and be_empty
end
end
context 'without a #o_id' do
it 'returns an empty ActiveRecord::Relation' do
expect(described_class.list(object: 'Test'))
.to be_an(ActiveRecord::Relation).and be_empty
end
end
end
end
describe 'Instance methods:' do
describe 'image previews (#content_inline / #content_preview)' do
let(:attributes) do
{
object: 'Test',
o_id: 1,
data: data,
filename: 'test1.pdf',
preferences: {
content_type: content_type,
content_id: 234,
},
created_by_id: 1,
}
end
let(:resized_inline_image) do
File.binwrite(temp_file, store.content_inline)
Rszr::Image.load(temp_file)
end
let(:resized_preview_image) do
File.binwrite(temp_file.next, store.content_preview)
Rszr::Image.load(temp_file.next)
end
let(:temp_file) { Tempfile.new.path }
context 'with content_type: "text/plain"' do
let(:content_type) { 'text/plain' }
context 'and text content' do
let(:data) { 'foo' }
it 'cannot be resized (neither inlined nor previewed)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect { store.content_preview }
.to raise_error('Unable to generate preview')
expect(store.preferences)
.to not_include(resizable: true)
.and not_include(content_inline: true)
.and not_include(content_preview: true)
end
end
end
context 'with content_type: "image/*"' do
context 'and text content' do
let(:content_type) { 'image/jpeg' }
let(:data) { 'foo' }
it 'cannot be resized (neither inlined nor previewed)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect { store.content_preview }
.to raise_error('Unable to generate preview')
expect(store.preferences)
.to not_include(resizable: true)
.and not_include(content_inline: true)
.and not_include(content_preview: true)
end
end
context 'with image content (width > 1800px)' do
context 'width <= 200px' do
let(:content_type) { 'image/png' }
let(:data) { File.binread(Rails.root.join('test/data/image/1x1.png')) }
it 'cannot be resized (neither inlined nor previewed)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect { store.content_preview }
.to raise_error('Unable to generate preview')
expect(store.preferences)
.to not_include(resizable: true)
.and not_include(content_inline: true)
.and not_include(content_preview: true)
end
end
context '200px < width <= 1800px)' do
let(:content_type) { 'image/png' }
let(:data) { File.binread(Rails.root.join('test/data/image/1000x1000.png')) }
it 'can be resized (previewed but not inlined)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect(resized_preview_image.width).to eq(200)
expect(store.preferences)
.to include(resizable: true)
.and not_include(content_inline: true)
.and include(content_preview: true)
end
end
context '1800px < width' do
let(:content_type) { 'image/jpeg' }
let(:data) { File.binread(Rails.root.join('test/data/upload/upload2.jpg')) }
it 'can be resized (inlined @ 1800px wide or previewed @ 200px wide)' do
expect(resized_inline_image.width).to eq(1800)
expect(resized_preview_image.width).to eq(200)
expect(store.preferences)
.to include(resizable: true)
.and include(content_inline: true)
.and include(content_preview: true)
end
context 'kind of wide/short: 8000x300' do
let(:data) { File.binread(Rails.root.join('test/data/image/8000x300.jpg')) }
it 'can be resized (inlined @ 1800px wide or previewed @ 200px wide)' do
expect(resized_inline_image.width).to eq(1800)
expect(resized_preview_image.width).to eq(200)
expect(store.preferences)
.to include(resizable: true)
.and include(content_inline: true)
.and include(content_preview: true)
end
end
context 'very wide/short: 4000x1; i.e., <= 6px vertically per 200px (preview) or 1800px (inline) horizontally' do
let(:data) { File.binread(Rails.root.join('test/data/image/4000x1.jpg')) }
it 'cannot be resized (neither inlined nor previewed)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect { store.content_preview }
.to raise_error('Unable to generate preview')
expect(store.preferences)
.to not_include(resizable: true)
.and not_include(content_inline: true)
.and not_include(content_preview: true)
end
end
context 'very wide/short: 8000x25; i.e., <= 6px vertically per 200px (preview) or 1800px (inline) horizontally' do
let(:data) { File.binread(Rails.root.join('test/data/image/8000x25.jpg')) }
it 'cannot be resized (neither inlined nor previewed)' do
expect { store.content_inline }
.to raise_error('Unable to generate inline')
expect { store.content_preview }
.to raise_error('Unable to generate preview')
expect(store.preferences)
.to not_include(resizable: true)
.and not_include(content_inline: true)
.and not_include(content_preview: true)
end
end
end
end
end
end
end
context 'when preferences exceed storage size' do
let(:valid_entries) do
{
content_type: 'text/plain',
content_id: 234,
}
end
shared_examples 'keeps other entries' do
context 'when other entries are present' do
let(:preferences) do
super().merge(valid_entries)
end
it 'keeps these entries' do
expect(store.preferences).to include(valid_entries)
end
end
end
context 'when single content is oversized' do
let(:preferences) do
{
oversized_content: '0' * 2500,
}
end
it 'removes that entry' do
expect(store.preferences).not_to have_key(:oversized_content)
end
include_examples 'keeps other entries'
end
context 'when the sum of multiple contents is oversized' do
let(:preferences) do
{
oversized_content1: '0' * 2000,
oversized_content2: '0' * 2000,
}
end
it 'removes first entry' do
expect(store.preferences).not_to have_key(:oversized_content1)
end
it 'keeps second entry' do
expect(store.preferences).to have_key(:oversized_content2)
end
include_examples 'keeps other entries'
end
context 'when single key is oversized' do
let(:oversized_key) { '0' * 2500 }
let(:preferences) do
{
oversized_key => 'valid content',
}
end
it 'removes that entry' do
expect(store.preferences).not_to have_key(oversized_key)
end
include_examples 'keeps other entries'
end
context 'when the sum of multiple keys is oversized' do
let(:oversized_key1) { '0' * 1500 }
let(:oversized_key2) { '1' * 1500 }
let(:preferences) do
{
oversized_key1 => 'valid content',
oversized_key2 => 'valid content',
}
end
it 'removes first entry' do
expect(store.preferences).not_to have_key(oversized_key1)
end
it 'keeps second entry' do
expect(store.preferences).to have_key(oversized_key2)
end
include_examples 'keeps other entries'
end
end
end

View File

@ -1,424 +0,0 @@
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
require 'test_helper'
class StoreTest < ActiveSupport::TestCase
test 'store fs - get_location' do
sha = 'ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73'
location = Store::Provider::File.get_location(sha)
assert_equal(Rails.root.join('storage/fs/ed70/02b4/39e9a/c845f/22357d8/22bac14/44730fbdb6016d3ec9432297b9ec9f73').to_s, location)
end
test 'store fs - empty dir remove' do
sha = 'ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73'
content = 'content'
location = Store::Provider::File.get_location(sha)
result = Store::Provider::File.add(content, sha)
assert(result)
exists = File.exist?(location)
assert(exists)
Store::Provider::File.delete(sha)
exists = File.exist?(location)
assert_not(exists)
exists = File.exist?(Rails.root.join('storage/fs/ed70/02b4'))
assert_not(exists)
exists = File.exist?(Rails.root.join('storage/fs/ed70'))
assert_not(exists)
exists = File.exist?(Rails.root.join('storage/fs'))
assert(exists)
exists = File.exist?(Rails.root.join('storage'))
assert(exists)
end
test 'store attachment and move it between backends' do
files = [
{
data: 'hello world',
filename: 'test.txt',
o_id: 1,
},
{
data: 'hello world äöüß',
filename: 'testäöüß.txt',
o_id: 2,
},
{
data: File.binread(Rails.root.join('test/data/pdf/test1.pdf')),
filename: 'test.pdf',
o_id: 3,
},
{
data: File.binread(Rails.root.join('test/data/pdf/test1.pdf')),
filename: 'test-again.pdf',
o_id: 4,
},
]
files.each do |file|
sha = Digest::SHA256.hexdigest(file[:data])
# add attachments
store = Store.add(
object: 'Test',
o_id: file[:o_id],
data: file[:data],
filename: file[:filename],
preferences: {},
created_by_id: 1,
)
assert store
# get list of attachments
attachments = Store.list(
object: 'Test',
o_id: file[:o_id],
)
assert attachments
# sha check
sha_new = Digest::SHA256.hexdigest(attachments[0].content)
assert_equal(sha, sha_new, "check file #{file[:filename]}")
# filename check
assert_equal(file[:filename], attachments[0].filename)
# provider check
assert_equal('DB', attachments[0].provider)
end
success = Store::File.verify
assert success, 'verify ok'
Store::File.move('DB', 'File')
files.each do |file|
sha = Digest::SHA256.hexdigest(file[:data])
# get list of attachments
attachments = Store.list(
object: 'Test',
o_id: file[:o_id],
)
assert attachments
# sha check
sha_new = Digest::SHA256.hexdigest(attachments[0].content)
assert_equal(sha, sha_new, "check file #{file[:filename]}")
# filename check
assert_equal(file[:filename], attachments[0].filename)
# provider check
assert_equal('File', attachments[0].provider)
end
success = Store::File.verify
assert success, 'verify ok'
Store::File.move('File', 'DB')
files.each do |file|
sha = Digest::SHA256.hexdigest(file[:data])
# get list of attachments
attachments = Store.list(
object: 'Test',
o_id: file[:o_id],
)
assert(attachments)
assert_equal(attachments.count, 1)
# sha check
sha_new = Digest::SHA256.hexdigest(attachments[0].content)
assert_equal(sha, sha_new, "check file #{file[:filename]}")
# filename check
assert_equal(file[:filename], attachments[0].filename)
# provider check
assert_equal('DB', attachments[0].provider)
# delete attachments
success = Store.remove(
object: 'Test',
o_id: file[:o_id],
)
assert(success)
# check attachments again
attachments = Store.list(
object: 'Test',
o_id: file[:o_id],
)
assert_not(attachments[0])
end
end
test 'test resizable' do
# not possible
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'text/plain',
content_id: 234,
},
created_by_id: 1,
)
assert_not(store.preferences.key?(:resizable))
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# not possible
store = Store.add(
object: 'SomeObject2',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], false)
assert_not(store.preferences.key?(:content_inline))
assert_not(store.preferences.key?(:content_preview))
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# possible (preview and inline)
store = Store.add(
object: 'SomeObject3',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload2.jpg')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_equal(store.preferences[:content_inline], true)
assert_equal(store.preferences[:content_preview], true)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_inline)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 1800)
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# possible (preview only)
store = Store.add(
object: 'SomeObject4',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/image/1000x1000.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_nil(store.preferences[:content_inline])
assert_equal(store.preferences[:content_preview], true)
assert_raises(RuntimeError) do
store.content_inline
end
temp_file = ::Tempfile.new.path
File.binwrite(temp_file, store.content_preview)
image = Rszr::Image.load(temp_file)
assert_equal(image.width, 200)
# no preview or inline needed
store = Store.add(
object: 'SomeObject5',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/image/1x1.png')),
filename: 'test1.png',
preferences: {
content_type: 'image/png',
content_id: 234,
},
created_by_id: 1,
)
assert_nil(store.preferences[:resizable])
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
assert_raises(RuntimeError) do
store.content_inline
end
assert_raises(RuntimeError) do
store.content_preview
end
# no preview or inline needed
store = Store.add(
object: 'SomeObject6',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/image/4000x1.jpg')),
filename: 'test1.jpg',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_nil(store.preferences[:resizable])
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
# possible (no preview or inline needed)
store = Store.add(
object: 'SomeObject7',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/image/8000x25.jpg')),
filename: 'test1.jpg',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_nil(store.preferences[:resizable])
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
# possible (preview and inline)
store = Store.add(
object: 'SomeObject8',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/image/8000x300.jpg')),
filename: 'test1.jpg',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_equal(store.preferences[:resizable], true)
assert_equal(store.preferences[:content_inline], true)
assert_equal(store.preferences[:content_preview], true)
# possible, but skipped (preview and inline)
Setting.set('import_mode', true)
store = Store.add(
object: 'SomeObject3',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload2.jpg')),
filename: 'test1.pdf',
preferences: {
content_type: 'image/jpg',
content_id: 234,
},
created_by_id: 1,
)
assert_nil(store.preferences[:resizable])
assert_nil(store.preferences[:content_inline])
assert_nil(store.preferences[:content_preview])
end
test 'test maximal preferences size check with one oversized content' do
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'text/plain',
content_id: 234,
some_key: '0' * 2500,
},
created_by_id: 1,
)
assert_not(store.preferences.key?(:some_key))
assert(store.preferences.key?(:content_id))
assert(store.preferences.key?(:content_type))
end
test 'test maximal preferences size check with two oversized content' do
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: {
content_type: 'text/plain',
content_id: 234,
some_key1: '0' * 2000,
some_key2: '0' * 2000,
},
created_by_id: 1,
)
assert_not(store.preferences.key?(:some_key1))
assert(store.preferences.key?(:some_key2))
assert(store.preferences.key?(:content_id))
assert(store.preferences.key?(:content_type))
end
test 'test maximal preferences size check with one oversized key' do
preferences = {
content_type: 'text/plain',
content_id: 234,
}
some_key1 = '0' * 2500
preferences[some_key1] = 'some content'
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: preferences,
created_by_id: 1,
)
assert_not(store.preferences.key?(some_key1))
assert(store.preferences.key?(:content_id))
assert(store.preferences.key?(:content_type))
end
test 'test maximal preferences size check with two oversized key' do
preferences = {
content_type: 'text/plain',
content_id: 234,
}
some_key1 = '0' * 1500
preferences[some_key1] = 'some content'
some_key2 = '1' * 1500
preferences[some_key2] = 'some content'
store = Store.add(
object: 'SomeObject1',
o_id: rand(1_234_567_890),
data: File.binread(Rails.root.join('test/data/upload/upload1.txt')),
filename: 'test1.pdf',
preferences: preferences,
created_by_id: 1,
)
assert_not(store.preferences.key?(some_key1))
assert(store.preferences.key?(some_key2))
assert(store.preferences.key?(:content_id))
assert(store.preferences.key?(:content_type))
end
end