mirror of
https://github.com/manyfold3d/manyfold.git
synced 2026-10-21 19:22:18 -05:00
327 lines
12 KiB
Ruby
327 lines
12 KiB
Ruby
require "rails_helper"
|
|
require "support/mock_directory"
|
|
|
|
RSpec.describe ModelFile do
|
|
it_behaves_like "Listable"
|
|
|
|
it "is not valid without a filename" do
|
|
expect(build(:model_file, filename: nil)).not_to be_valid
|
|
end
|
|
|
|
it "is not valid if name is too long" do
|
|
expect(build(:model, name: SecureRandom.alphanumeric(256))).not_to be_valid
|
|
end
|
|
|
|
it "is not valid without being part of a model" do
|
|
expect(build(:model_file, model: nil)).not_to be_valid
|
|
end
|
|
|
|
it "is valid if it has a filename and model" do
|
|
expect(build(:model_file)).to be_valid
|
|
end
|
|
|
|
it "must have a unique filename within its model" do
|
|
model = create(:model, path: "model")
|
|
create(:model_file, model: model, filename: "part.stl")
|
|
expect(build(:model_file, model: model, filename: "part.stl")).not_to be_valid
|
|
end
|
|
|
|
it "can have the same filename as a file in a different model" do
|
|
library = create(:library)
|
|
model1 = create(:model, library: library, path: "model1")
|
|
create(:model_file, model: model1, filename: "part.stl")
|
|
model2 = create(:model, library: library, path: "model2")
|
|
expect(build(:model_file, model: model2, filename: "part.stl")).to be_valid
|
|
end
|
|
|
|
context "with a real model" do
|
|
let(:library) { create(:library, path: Rails.root.join("spec/fixtures")) }
|
|
let(:model) { create(:model, library: library, path: "model_file_spec") }
|
|
let(:part) {
|
|
create(
|
|
:model_file,
|
|
model: model,
|
|
filename: "example.obj",
|
|
attachment: ModelFileUploader.upload(File.open("spec/fixtures/model_file_spec/example.obj"), :cache)
|
|
)
|
|
}
|
|
|
|
it "calculates dimensions of model" do
|
|
expect(part.dimensions).to eq(Mittsu::Vector3.new(10, 15, 20))
|
|
end
|
|
|
|
it "calculates file size when attached" do
|
|
expect(part.size).to eq(284)
|
|
end
|
|
|
|
it "calculates digest for a file" do
|
|
expect(part.calculate_digest.first(16)).to eq("8a0f188378204b67")
|
|
end
|
|
|
|
it "updates model timestamp when changed" do
|
|
expect { part.update!(presupported: true) }.to change(model, :updated_at)
|
|
end
|
|
end
|
|
|
|
context "when serializing JSON fields" do
|
|
let(:file) { create(:model_file) }
|
|
|
|
[
|
|
:attachment_data
|
|
].each do |field|
|
|
it "deserializes #{field.to_s.humanize} correctly" do
|
|
expect(file.reload.send(field)).to be_a Hash
|
|
end
|
|
end
|
|
end
|
|
|
|
it "finds duplicate files using digest" do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
|
|
library = create(:library, path: Rails.root.join("storage"))
|
|
model = create(:model, library: library, path: "model")
|
|
part1 = create(:model_file, model: model, filename: "same.obj", digest: "1234")
|
|
part2 = create(:model_file, model: model, filename: "same.stl", digest: "1234")
|
|
create(:model_file, model: model, filename: "different.stl", digest: "4321")
|
|
allow(part1).to receive(:size).and_return(123)
|
|
expect(part1.duplicate?).to be true
|
|
expect(part1.duplicates).to contain_exactly(part2)
|
|
end
|
|
|
|
it "does not flag duplicates for nil digests" do # rubocop:todo RSpec/ExampleLength
|
|
library = create(:library, path: Rails.root.join("storage"))
|
|
model = create(:model, library: library, path: "model1")
|
|
part1 = create(:model_file, model: model, filename: "nil.obj", digest: nil)
|
|
create(:model_file, model: model, filename: "nil.stl", digest: nil)
|
|
expect(part1.duplicate?).to be false
|
|
end
|
|
|
|
it "does not flag duplicates for zero-length files" do # rubocop:todo RSpec/ExampleLength
|
|
library = create(:library, path: Rails.root.join("storage"))
|
|
model = create(:model, library: library, path: "model1")
|
|
part1 = create(:model_file, model: model, filename: "same.obj", digest: "1234")
|
|
create(:model_file, model: model, filename: "same.stl", digest: "1234")
|
|
allow(part1).to receive(:size).and_return(0)
|
|
expect(part1.duplicate?).to be false
|
|
end
|
|
|
|
it "mtime reports attachment modified time" do
|
|
file = create(:model_file)
|
|
expect(file.mtime).to be_present
|
|
end
|
|
|
|
it "mtime reports updated time if attachment has no mtime metadata" do
|
|
file = create(:model_file)
|
|
allow(file).to receive(:attachment).and_return(double(mtime: nil)) # rubocop:disable RSpec/VerifiedDoubles
|
|
expect(file.mtime).to be_present
|
|
end
|
|
|
|
it "mtime reports updated time if there is no attachment" do
|
|
file = create(:model_file)
|
|
allow(file).to receive(:attachment).and_return(nil)
|
|
expect(file.mtime).to be_present
|
|
end
|
|
|
|
it "ctime reports attachment created time" do
|
|
file = create(:model_file)
|
|
expect(file.ctime).to be_present
|
|
end
|
|
|
|
it "mtime reports created time if attachment has no ctime metadata" do
|
|
file = create(:model_file)
|
|
allow(file).to receive(:attachment).and_return(double(ctime: nil)) # rubocop:disable RSpec/VerifiedDoubles
|
|
expect(file.ctime).to be_present
|
|
end
|
|
|
|
it "ctime reports created time if there is no attachment" do
|
|
file = create(:model_file)
|
|
allow(file).to receive(:attachment).and_return(nil)
|
|
expect(file.ctime).to be_present
|
|
end
|
|
|
|
context "with actual files on disk" do
|
|
around do |ex|
|
|
MockDirectory.create([
|
|
"model_one/part_1.3mf"
|
|
]) do |path|
|
|
@library_path = path
|
|
ex.run
|
|
end
|
|
end
|
|
|
|
let(:library) { create(:library, path: @library_path) } # rubocop:todo RSpec/InstanceVariable
|
|
let(:model) { create(:model, library: library, path: "model_one") }
|
|
let(:file) { create(:model_file, model: model, filename: "part_1.3mf", digest: "1234") }
|
|
|
|
it "renames file on disk" do # rubocop:disable RSpec/MultipleExpectations
|
|
file.update!(filename: "newname.3mf")
|
|
expect(File.exist?(File.join(library.path, "model_one/part_1.3mf"))).to be false
|
|
expect(File.exist?(File.join(library.path, "model_one/newname.3mf"))).to be true
|
|
end
|
|
|
|
it "rejects filename change if MIME type would change" do # rubocop:disable RSpec/MultipleExpectations
|
|
file.update(filename: "part_1.stl")
|
|
expect(file).not_to be_valid
|
|
expect(file.errors[:filename].first).to eq "is not the same file type"
|
|
end
|
|
|
|
it "rejects case-only filename change" do # rubocop:disable RSpec/MultipleExpectations
|
|
file.update(filename: "part_1.3MF")
|
|
expect(file).not_to be_valid
|
|
expect(file.errors[:filename].first).to eq "cannot be a case-only change"
|
|
end
|
|
|
|
it "removes original file from disk when explicitly told to" do
|
|
expect { file.delete_from_disk_and_destroy }.to(
|
|
change { File.exist?(File.join(library.path, file.path_within_library)) }.from(true).to(false)
|
|
)
|
|
end
|
|
|
|
it "does not remove original file from disk when destroyed" do
|
|
expect { file.destroy }.not_to(
|
|
change { File.exist?(File.join(library.path, file.path_within_library)) }
|
|
)
|
|
end
|
|
|
|
it "ignores missing files on deletion" do
|
|
file.update_attribute :filename, "gone.3mf" # rubocop:disable Rails/SkipsModelValidations
|
|
expect { file.destroy }.not_to raise_exception
|
|
end
|
|
|
|
it "queues up rescans for duplicates on destroy" do
|
|
dupe = create(:model_file, model: model, filename: "duplicate.3mf", digest: "1234")
|
|
expect { file.destroy }.to(
|
|
have_enqueued_job(Analysis::AnalyseModelFileJob).with(dupe.id)
|
|
)
|
|
end
|
|
end
|
|
|
|
context "with different versions of the same file" do
|
|
let!(:model) { create(:model) }
|
|
let!(:presupported) { create(:model_file, model: model, presupported: true) }
|
|
let!(:unsupported) { create(:model_file, model: model, presupported: false, presupported_version: presupported) }
|
|
|
|
it "can access supported part from unsupported part" do
|
|
expect(unsupported.presupported_version).to eq presupported
|
|
end
|
|
|
|
it "can access unsupported part from presupported part" do
|
|
expect(presupported.unsupported_version).to eq unsupported
|
|
end
|
|
|
|
it "only let presupported files be set as the presupported_version" do # rubocop:todo RSpec/MultipleExpectations
|
|
another_unsupported = create(:model_file, model: model, presupported: false)
|
|
unsupported.presupported_version = another_unsupported
|
|
expect(unsupported).not_to be_valid
|
|
expect(unsupported.errors[:presupported_version].first).to eq "is not a presupported file"
|
|
end
|
|
|
|
it "does not allow a presupported_version to be set for presupported files" do # rubocop:todo RSpec/MultipleExpectations
|
|
another_presupported = create(:model_file, model: model, presupported: true)
|
|
presupported.presupported_version = another_presupported
|
|
expect(presupported).not_to be_valid
|
|
expect(presupported.errors[:presupported_version].first).to eq "cannot be set on a presupported file"
|
|
end
|
|
|
|
it "clears presupported version if presupported file is set to unsupported" do
|
|
presupported.update!(presupported: false)
|
|
expect(unsupported.reload.presupported_version).to be_nil
|
|
end
|
|
end
|
|
|
|
{
|
|
stl: true,
|
|
png: false,
|
|
pdf: false,
|
|
lys: false
|
|
}.each_pair do |extension, result|
|
|
it "shows that #{extension} files are#{"n't" if result == false} renderable" do
|
|
file = create(:model_file, filename: "test.#{extension}")
|
|
expect(file.is_renderable?).to be result
|
|
end
|
|
end
|
|
|
|
[true, false].each do |state|
|
|
before do
|
|
allow(SiteSettings).to receive_messages(default_indexable: state, default_ai_indexable: state)
|
|
end
|
|
|
|
let(:model) { create(:model) }
|
|
let(:file) { create(:model_file, model: model) }
|
|
|
|
it "delegates indexable to model (#{state})" do
|
|
expect(file.indexable?).to eq model.indexable?
|
|
end
|
|
|
|
it "delegates AI indexable to model (#{state})" do
|
|
expect(file.ai_indexable?).to eq model.ai_indexable?
|
|
end
|
|
end
|
|
|
|
context "when creating derivatives for an image file" do
|
|
let(:model) { create(:model) }
|
|
let(:file) {
|
|
create(:model_file, model: model, filename: "logo.png",
|
|
attachment: ModelFileUploader.upload(
|
|
File.open(Rails.root.join("logo.png")),
|
|
:cache
|
|
))
|
|
}
|
|
|
|
before do
|
|
allow(SiteSettings).to receive_messages(generate_image_derivatives: true)
|
|
file.reload
|
|
end
|
|
|
|
it "automatically generates a preview derivative" do
|
|
expect(file.attachment_derivatives).to have_key(:preview)
|
|
end
|
|
|
|
it "automatically generates a carousel derivative" do
|
|
expect(file.attachment_derivatives).to have_key(:carousel)
|
|
end
|
|
end
|
|
|
|
context "with missing derivatives" do
|
|
let(:model) { create(:model) }
|
|
let(:file) {
|
|
create(:model_file, model: model, filename: "logo.png",
|
|
attachment: ModelFileUploader.upload(
|
|
File.open(Rails.root.join("logo.png")),
|
|
:cache
|
|
))
|
|
}
|
|
|
|
before do
|
|
allow(SiteSettings).to receive_messages(generate_image_derivatives: true)
|
|
file.attachment_derivatives.keys.each do |derivative|
|
|
file.attachment_attacher.remove_derivative(derivative, delete: true)
|
|
end
|
|
file.save!
|
|
file.reload
|
|
end
|
|
|
|
it "regenerates derivatives on demand" do
|
|
expect { file.create_derivatives! }.to change { file.attachment_derivatives.count }.from(0).to(2)
|
|
end
|
|
|
|
it "does not change the updated_at time of the file" do
|
|
expect { file.create_derivatives! }.not_to change(file.reload, :updated_at)
|
|
end
|
|
|
|
it "does not change the updated_at time of the owning model" do
|
|
expect { file.create_derivatives! }.not_to change(model.reload, :updated_at)
|
|
end
|
|
|
|
it "sends UI update for owning model" do
|
|
expect { file.create_derivatives! }.to have_enqueued_job(Turbo::Streams::BroadcastStreamJob).once
|
|
end
|
|
|
|
it "does not create followup jobs for owning model" do
|
|
expect { file.create_derivatives! }.not_to have_enqueued_job(Scan::Model::CheckForProblemsJob).once
|
|
end
|
|
|
|
it "does not create federated activites for change to owning model", :federation do
|
|
expect { file.create_derivatives! }.not_to change(Federails::Activity, :count)
|
|
end
|
|
end
|
|
end
|