Files
manyfold/spec/requests/models_spec.rb

724 lines
26 KiB
Ruby

require "rails_helper"
# edit_model GET /models/:id/edit(.:format) models#edit
# model GET /models/:id(.:format) models#show
# PATCH /models/:id(.:format) models#update
# PUT /models/:id(.:format) models#update
# DELETE /models/:id(.:format) models#destroy
# edit_models GET /models/edit(.:format) models#bulk_edit
# update_models PATCH /models/update(.:format) models#bulk_update
# models GET /models(.:format) models#index
# new_model GET /models/new(.:format) models#new
# POST /models(.:format) models#create
# merge_models POST /models/merge(.:format) models#merge
# scan_model POST /models/:id/scan(.:format) models#scan
RSpec.describe "Models" do
it_behaves_like "Permittable", Model
context "when signed out in multiuser mode", :after_first_run, :multiuser do
context "with public model" do
let!(:model) { create(:model, :public) }
describe "GET /models" do
it "includes indexing directive header" do
allow(SiteSettings).to receive_messages(default_indexable: true, default_ai_indexable: false)
get "/models"
expect(response.headers["X-Robots-Tag"]).to eq "noai noimageai"
end
it "includes indexing directive meta tag" do
allow(SiteSettings).to receive_messages(default_indexable: true, default_ai_indexable: false)
get "/models"
expect(response.body).to include %(<meta name="robots" content="noai noimageai">)
end
end
describe "GET /models/:id" do
it "returns http success" do
get "/models/#{model.to_param}"
expect(response).to have_http_status(:success)
end
it "includes indexing directive header" do
allow(SiteSettings).to receive_messages(default_indexable: true, default_ai_indexable: false)
get "/models/#{model.to_param}"
expect(response.headers["X-Robots-Tag"]).to eq "noai noimageai"
end
it "includes indexing directive meta tag" do
allow(SiteSettings).to receive_messages(default_indexable: true, default_ai_indexable: false)
get "/models/#{model.to_param}"
expect(response.body).to include %(<meta name="robots" content="noai noimageai">)
end
end
end
context "with non-public model" do
let!(:model) { create(:model) }
describe "GET /models/:id" do
it "returns not found" do
get "/models/#{model.to_param}"
expect(response).to be_not_found
end
end
end
end
[:multiuser, :singleuser].each do |mode|
context "when signed in in #{mode} mode", mode do
let!(:creator) { create(:creator) }
let!(:collection) { create(:collection) }
let!(:library) do
l = create(:library)
build_list(:model, 5, library: l) { |it| it.save! }
build_list(:model, 5, library: l, creator: creator) { |it| it.save! }
build_list(:model, 5, library: l, collection: collection) { |it| it.save! }
build_list(:model, 5, library: l, creator: creator, collection: collection) { |it| it.save! }
l
end
describe "GET /models/:id", :as_member do
it "returns http success" do
get "/models/#{library.models.first.to_param}"
expect(response).to have_http_status(:success)
end
context "with zip file prepared" do
let(:model) { create(:model, library: library) }
before do
PrepareDownloadJob.perform_now(model_id: model.id, selection: nil)
end
it "gets ZIP file" do
get "/models/#{model.to_param}.zip"
expect(response).to have_http_status(:success)
end
it "doesn't get zipfile if only preview access available" do
model.revoke_all_permissions(Role.find_by!(name: :member))
model.grant_permission_to("preview", User.last)
get "/models/#{model.to_param}.zip"
expect(response).to have_http_status(:forbidden)
end
end
end
describe "GET /models/:id/edit" do
before { get "/models/#{library.models.first.to_param}/edit" }
it "shows edit page for file", :as_moderator do
expect(response).to have_http_status(:success)
end
it "sets returnable session param", :as_moderator do
expect(session[:return_after_new]).to eq "/models/#{library.models.first.to_param}/edit"
end
it "is denied to non-moderators", :as_contributor do
expect(response).to have_http_status(:forbidden)
end
end
describe "PUT /models/:id" do
it "adds tags to a model", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
put "/models/#{library.models.first.to_param}", params: {model: {tag_list: ["a", "b", "c"]}}
expect(response).to have_http_status(:redirect)
expect(library.models.first.tag_list).to include("a", "b", "c")
end
it "clears returnable session param", :as_moderator do
put "/models/#{library.models.first.to_param}", params: {model: {tag_list: ["a", "b", "c"]}}
expect(session[:return_after_new]).to be_nil
end
it "removes tags from a model", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
first = library.models.first
first.tag_list = "a, b, c"
first.save
put "/models/#{library.models.first.to_param}", params: {model: {tag_list: ["a", "b"]}}
expect(response).to have_http_status(:redirect)
first.reload
tags = first.tag_list
expect(tags.length).to eq 2
expect(tags).to contain_exactly("a", "b")
end
it "both adds and removes tags from a model", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
first = library.models.first
first.tag_list = "a, b, c"
first.save
put "/models/#{library.models.first.to_param}", params: {model: {tag_list: ["a", "b", "d"]}}
expect(response).to have_http_status(:redirect)
first.reload
tags = first.tag_list
expect(tags.length).to eq 3
expect(tags).to contain_exactly("a", "b", "d")
end
it "auto-publishes creator if being made public", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
private_creator = create(:creator)
private_model = create(:model, creator: private_creator)
put "/models/#{private_model.to_param}", params: {
model: {
creator_id: private_creator.id,
caber_relations_attributes: {"0" => {subject: "role::public", permission: "view"}}
}
}
expect(response).to have_http_status(:redirect)
expect(private_model.reload).to be_public
expect(private_creator.reload).to be_public
end
it "adds links", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
model = library.models.first
put "/models/#{model.to_param}", params: {
model: {
links_attributes: {"0" => {url: "https://manyfold.app"}}
}
}
expect(response).to have_http_status(:redirect)
expect(model.reload.links.find_by(url: "https://manyfold.app")).to be_present
end
it "is denied to non-moderators", :as_contributor do
put "/models/#{library.models.first.to_param}"
expect(response).to have_http_status(:forbidden)
end
end
describe "DELETE /models/:id" do # rubocop:todo RSpec/RepeatedExampleGroupBody
before { delete "/models/#{library.models.first.to_param}" }
it "redirects to model list after deletion", :as_moderator do
expect(response).to redirect_to("/")
end
it "is denied to non-moderators", :as_contributor do
expect(response).to have_http_status(:forbidden)
end
end
describe "GET /models/edit" do # rubocop:todo RSpec/RepeatedExampleGroupBody
before { get edit_models_path }
it "shows bulk edit page", :as_moderator do
expect(response).to have_http_status(:success)
end
it "sets returnable session param", :as_moderator do
expect(session[:return_after_new]).to eq "/models/edit"
end
it "is denied to non-moderators", :as_contributor do
expect(response).to have_http_status(:forbidden)
end
context "with filters", :as_moderator do
let(:tag) { create(:tag) }
let!(:tagged_model) { create(:model, library: library, tag_list: [tag.name]) }
it "shows filtered models" do
get edit_models_path(tag: [tag.name])
expect(response.body).to include(tagged_model.name)
end
it "doesn't show other models" do
get edit_models_path(tag: [tag.name])
library.models.each do |model|
next if model == tagged_model
expect(response.body).not_to include(model.name)
end
end
end
end
describe "PATCH /models/update" do
let(:model_params) {
model_params = {}
library.models.each do |model|
model_params[model.to_param] = 1
end
model_params
}
it "updates models creator", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
models = library.models.take(2)
update = {}
update[models[0].to_param] = 1
update[models[1].to_param] = 1
patch update_models_path, params: {models: update, creator_id: creator.id}
expect(response).to have_http_status(:redirect)
models.each { |model| model.reload }
expect(models[0].creator_id).to eq creator.id
expect(models[1].creator_id).to eq creator.id
end
it "adds tags to models", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
patch update_models_path, params: {models: model_params, add_tags: ["a", "b", "c"]}
expect(response).to have_http_status(:redirect)
library.models.take(2).each do |model|
expect(model.tag_list).to include("a", "b", "c")
end
end
it "removes tags from models", :as_moderator do # rubocop:todo RSpec/ExampleLength, RSpec/MultipleExpectations
library.models.take(2).each do |model|
model.tag_list = "a, b, c"
model.save
end
patch update_models_path, params: {models: model_params, remove_tags: ["a", "b"]}
expect(response).to have_http_status(:redirect)
expect(library.models.tagged_with("c").count).to eq 2
expect(library.models.tagged_with("a").count).to eq 0
end
it "clears returnable session param", :as_moderator do
patch update_models_path, params: {models: model_params, remove_tags: ["a", "b"]}
expect(session[:return_after_new]).to be_nil
end
it "is denied to non-moderators", :as_contributor do
update = {}
library.models.take(2).each { |it| update[it.to_param] = 1 }
patch update_models_path, params: {models: model_params, remove_tags: ["a", "b"]}
expect(response).to have_http_status(:forbidden)
end
context "when updating all filtered models", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:tag) { create(:tag) }
let!(:tagged_model) { create(:model, library: library, tag_list: [tag.name]) }
let(:new_library) { create(:library) }
let(:params) do
{
update_all: I18n.t("models.bulk_edit.update_all"),
new_library_id: new_library.id,
tag: [tag.name]
}
end
it "updates all models matching the filter" do
patch update_models_path, params: params
library.models.each do |model|
next if model == tagged_model
expect(model.reload.library_id).to eq(library.id)
end
end
end
end
describe "GET /models", :as_member do
it "allows search queries" do
get "/models?q=#{library.models.first.name}"
expect(response).to have_http_status(:success)
end
it "allows tag filters" do
m = library.models.first
m.tag_list << "test"
m.save
get "/models?tag[]=test"
expect(response).to have_http_status(:success)
end
it "allows link filters" do
get "/models?link="
expect(response).to have_http_status(:success)
end
it "returns paginated models" do # rubocop:todo RSpec/MultipleExpectations
get "/models?library=#{library.to_param}&page=2"
expect(response).to have_http_status(:success)
expect(response.body).to match(/pagination/)
end
end
describe "POST /models/merge" do
context "with a target and models to merge into it", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
target: model_one.to_param,
models: [model_two.to_param]
}
}
it "is denied if the user doesn't have update permission on the models", :as_contributor do
merge_post
expect(response).to have_http_status(:forbidden)
end
it "is denied if the user doesn't have update permission on the target"
end
context "without any models", :as_moderator do
let(:model) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
target: model.to_param
}
}
it "gives a bad request response if no models are provided" do
merge_post
expect(response).to have_http_status(:bad_request)
end
end
context "without a target", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
models: [model_one.to_param, model_two.to_param]
}
}
before { merge_post }
it "redirects to the merge options page" do
expect(response).to redirect_to("/models/merge?models%5B%5D=#{model_one.to_param}&models%5B%5D=#{model_two.to_param}")
end
end
context "when merging to target model", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
models: [model_one.to_param, model_two.to_param],
target: model_one.to_param
}
}
before { merge_post }
it "redirects to the target model" do
expect(response).to redirect_to("/models/#{model_one.to_param}")
end
end
context "when merging to a completely new model", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
models: [model_one.to_param, model_two.to_param],
target: "==new=="
}
}
before { merge_post }
it "redirects to the new model" do
new_model = Model.last
expect(response).to redirect_to("/models/#{new_model.to_param}")
end
it "Uses the name of the first merged model as the new name" do
new_model = Model.last
expect(new_model.name).to eq model_one.name
end
end
context "when merging to a common root", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model, path: "common/model_one") }
let(:model_two) { create(:model, path: "common/model_two", library: model_one.library) }
let(:merge_post) {
post "/models/merge", params: {
models: [model_one.to_param, model_two.to_param],
target: "==common_root=="
}
}
before { merge_post }
it "redirects to the new model" do
new_model = Model.last
expect(response).to redirect_to("/models/#{new_model.to_param}")
end
it "uses the common root folder as the new path" do
new_model = Model.last
expect(new_model.path).to eq "common"
end
it "uses the name of the common root folder as the new name" do
new_model = Model.last
expect(new_model.name).to eq "Common"
end
end
context "with form-encoded data", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:merge_post) {
post "/models/merge", params: {
models: {
model_one.to_param => "0",
model_two.to_param => "1"
}
}
}
before { merge_post }
it "extracts selected models from form" do
expect(assigns(:models)).to include model_two
end
it "ignores unselected models" do
expect(assigns(:models)).not_to include model_one
end
end
end
describe "GET /models/merge", :as_moderator do # rubocop:todo RSpec/MultipleMemoizedHelpers
let(:model_one) { create(:model) }
let(:model_two) { create(:model) }
let(:configure_merge) {
get "/models/merge", params: {
models: [model_one.to_param, model_two.to_param]
}
}
before { configure_merge }
it "merge options page includes a form for choosing the target" do
expect(response.body).to include(%(<form action="/models/merge" accept-charset="UTF-8" method="post">))
end
end
describe "POST /models/:id/scan" do
it "schedules a scan job", :as_moderator do
expect { post "/models/#{library.models.first.to_param}/scan" }.to(
have_enqueued_job(Scan::CheckModelJob).with(library.models.first.id).once
)
end
it "redirects back to model page", :as_contributor do
post "/models/#{library.models.first.to_param}/scan"
expect(response).to redirect_to("/models/#{library.models.first.public_id}")
end
it "is denied to non-contributors", :as_member do
post "/models/#{library.models.first.to_param}/scan"
expect(response).to have_http_status(:forbidden)
end
end
describe "GET /models/new" do
before { get "/models/new" }
it "shows upload form", :as_contributor do
expect(response).to have_http_status(:success)
end
it "sets returnable session param", :as_contributor do
expect(session[:return_after_new]).to eq "/models/new"
end
it "denies member permission", :as_member do
expect(response).to have_http_status(:forbidden)
end
end
describe "POST /models" do
let(:creator) { create(:creator) }
let(:collection) { create(:collection) }
let(:post_models) {
post "/models", params: {
model: {
library: library.to_param,
scan: "1",
file: files,
creator_id: creator.id,
collection_id: collection.id,
license: "MIT",
sensitive: "1",
permission_preset: "public",
tag_list: ["tag1", "tag2"],
name: "Only used for single model upload"
}
}
}
context "with a single uncompressed file", :as_contributor do
let(:files) {
{
"0" => {
id: "upload_key",
name: "test.stl"
}
}
}
it "enqueues processing job with all parameters" do # rubocop:disable RSpec/ExampleLength
expect { post_models }
.to have_enqueued_job(ProcessUploadedFileJob)
.with(Library.first.id,
[{
id: "upload_key",
storage: "cache",
metadata: {
filename: "test.stl"
}
}],
owner: User.last,
creator_id: creator.id.to_s,
collection_id: collection.id.to_s,
license: "MIT",
sensitive: true,
permission_preset: "public",
tag_list: ["tag1", "tag2"],
name: "Only used for single model upload").once
end
it "redirect back to index after upload" do
post_models
expect(response).to redirect_to("/models")
end
it "clears returnable session param" do
post_models
expect(session[:return_after_new]).to be_nil
end
it "rate limits model uploads" do
Rails.cache.increment("rate-limit:models:127.0.0.1", 10, expires_in: 1.minute)
post_models
expect(response).to have_http_status :too_many_requests
end
end
context "with a filename including path traversal", :as_contributor do
let(:files) {
{
"0" => {
id: "upload_key",
name: "../test.stl"
}
}
}
it "sanitizes filename" do
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id,
array_including(hash_including({metadata: {filename: "test.stl"}})),
hash_including({})).once
end
end
context "with multiple compressed files", :as_contributor do
let(:files) {
{
"0" => {
id: "upload_1",
name: "test.zip"
},
"1" => {
id: "upload_2",
name: "example.zip"
}
}
}
it "enqueues separate archive processing jobs" do # rubocop:disable RSpec/ExampleLength
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id, hash_including({metadata: {filename: "test.zip"}}), hash_including({})).once
.and have_been_enqueued
.with(Library.first.id, hash_including({metadata: {filename: "example.zip"}}), hash_including({})).once
end
it "does not provide model name field" do # rubocop:disable RSpec/ExampleLength
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id, hash_including({}), hash_not_including({name: "Only for single model upload"})).twice
end
end
context "with multiple uncompressed files", :as_contributor do
let(:files) {
{
"0" => {
id: "upload_1",
name: "test.stl"
},
"1" => {
id: "upload_2",
name: "readme.txt"
}
}
}
it "enqueues a single processing job for all files combined" do # rubocop:disable RSpec/ExampleLength
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id,
array_including(
hash_including({metadata: {filename: "test.stl"}}),
hash_including({metadata: {filename: "readme.txt"}})
),
hash_including({})).once
end
it "provides model name field" do # rubocop:disable RSpec/ExampleLength
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id, array_including, hash_including({name: "Only used for single model upload"}))
end
end
context "with an uncompressed file and a compressed one", :as_contributor do
let(:files) {
{
"0" => {
id: "upload_1",
name: "test.zip"
},
"1" => {
id: "upload_2",
name: "model.stl"
}
}
}
it "enqueues one processing job for all files combined" do # rubocop:disable RSpec/ExampleLength
post_models
expect(ProcessUploadedFileJob).to have_been_enqueued
.with(Library.first.id,
array_including(
hash_including({metadata: {filename: "test.zip"}}),
hash_including({metadata: {filename: "model.stl"}})
),
hash_including({})).once
end
end
context "without upload permission", :as_member do
let(:files) { {} }
it "denies members" do
post_models
expect(response).to have_http_status(:forbidden)
end
end
end
end
end
end