otwarchive-symphonyarchive/spec/models/work_spec.rb
2026-03-11 22:22:11 +00:00

691 lines
24 KiB
Ruby

require "spec_helper"
describe Work do
# see lib/collectible_spec for collection-related tests
it "creates a minimal work" do
expect(create(:work)).to be_valid
end
context "when posted" do
it "posts the first chapter" do
work = create(:work)
expect(work.first_chapter.posted).to eq true
end
end
context "create_stat_counter" do
it "creates a stat counter for that work id" do
expect do
@work = build(:work)
@work.save!
end.to change { StatCounter.all.count }
.by(1)
expect(StatCounter.where(work_id: @work.id)).to exist
end
end
context "invalid title" do
it { expect(build(:work, title: nil)).to be_invalid }
it "cannot be shorter than ArchiveConfig.TITLE_MIN" do
expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.TITLE_MIN - 1))).to be_invalid
end
it "cannot be longer than ArchiveConfig.TITLE_MAX" do
expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.TITLE_MAX + 1))).to be_invalid
end
end
context "clean_and_validate_title" do
before do
ArchiveConfig.TITLE_MIN = 10
end
it "strips out leading spaces from the title" do
@work = create(:work, title: " Has Leading Spaces")
@work.reload
expect(@work.title).to eq("Has Leading Spaces")
end
let(:too_short) { ArchiveConfig.TITLE_MIN - 1 }
it "errors if the title without leading spaces is shorter than #{ArchiveConfig.TITLE_MIN}" do
expect do
@work = create(:work, title: " #{too_short}")
@work.reload
end.to raise_error(ActiveRecord::RecordInvalid, "Validation failed: Title must be at least #{ArchiveConfig.TITLE_MIN} characters long without leading spaces.")
end
# Reset the min characters in the title, so that the factory is valid
after do
ArchiveConfig.TITLE_MIN = 1
end
end
context "invalid summary" do
it "cannot be longer than ArchiveConfig.SUMMARY_MAX" do
expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.SUMMARY_MAX + 1))).to be_invalid
end
end
context "invalid notes" do
it "cannot be longer than ArchiveConfig.NOTES_MAX" do
expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.NOTES_MAX + 1))).to be_invalid
end
end
context "invalid endnotes" do
it "cannot be longer than ArchiveConfig.NOTES_MAX" do
expect(build(:work, title: Faker::Lorem.characters(number: ArchiveConfig.NOTES_MAX + 1))).to be_invalid
end
end
context "invalid language" do
let(:deleted_language_id) do
briefly_lived_language = create(:language)
deleted_language_id = briefly_lived_language.id
briefly_lived_language.destroy
deleted_language_id
end
it "is valid with a supported language" do
work = build(:work, language_id: Language.default.id)
expect(work).to be_valid
end
it "is not valid with a language we don't support" do
work = build(:work, language_id: deleted_language_id)
expect(work).not_to be_valid
expect(work.errors.messages[:base]).to include("Language cannot be blank.")
end
it "is not valid without a language" do
work = build(:work, language_id: "")
expect(work).not_to be_valid
expect(work.errors.messages[:base]).to include("Language cannot be blank.")
end
end
context "invalid rating" do
it "cannot have more than one rating" do
expect(build(:work, rating_string: "Not Rated, General Audiences")).to be_invalid
end
end
context "validate authors" do
let(:invalid_work) { build(:no_authors) }
it "does not save if author is blank" do
expect(invalid_work.save).to be_falsey
expect(invalid_work.errors[:base]).to include "Work must have at least one creator."
end
end
describe "reindexing" do
let!(:work) { create(:work) }
context "when draft status is changed" do
it "enqueues tags for reindex" do
expect do
work.update!(posted: false)
end.to add_to_reindex_queue(work.fandoms.last, :main)
end
end
context "when hidden by an admin" do
it "enqueues tags for reindex" do
expect do
work.update!(hidden_by_admin: true)
end.to add_to_reindex_queue(work.fandoms.last, :main)
end
end
context "when put in an unrevealed collection" do
it "enqueues tags for reindex" do
expect do
work.update!(in_unrevealed_collection: true)
end.to add_to_reindex_queue(work.fandoms.last, :main)
end
end
end
describe "#crossover" do
it "is not crossover with one fandom" do
fandom = create(:canonical_fandom, name: "nge")
work = create(:work, fandoms: [fandom])
expect(work.crossover).to be_falsy
end
it "is not crossover with one fandom and one of its synonyms" do
rel = create(:canonical_fandom, name: "evanescence")
syn = create(:fandom, name: "can't wake up (wake me up inside)", merger: rel)
work = create(:work, fandoms: [rel, syn])
expect(work.crossover).to be_falsy
end
it "is not crossover with multiple synonyms of one fandom" do
rel = create(:canonical_fandom, name: "nge")
syn1 = create(:fandom, name: "eva", merger: rel)
syn2 = create(:fandom, name: "end of eva", merger: rel)
work = create(:work, fandoms: [syn1, syn2])
expect(work.crossover).to be_falsy
end
it "is not crossover with fandoms sharing a direct meta tag" do
rel1 = create(:canonical_fandom, name: "rebuild")
rel2 = create(:canonical_fandom, name: "campus apocalypse")
meta_tag = create(:canonical_fandom, name: "nge")
meta_tag.update_attribute(:sub_tag_string, "#{rel1.name},#{rel2.name}")
rel1.reload
rel2.reload
work = create(:work, fandoms: [rel1, rel2])
expect(work.crossover).to be_falsy
end
it "is not a crossover between fandoms sharing an indirect meta tag" do
grand = create(:canonical_fandom)
parent1 = create(:canonical_fandom)
parent2 = create(:canonical_fandom)
child1 = create(:canonical_fandom)
child2 = create(:canonical_fandom)
grand.update_attribute(:sub_tag_string, "#{parent1.name},#{parent2.name}")
child1.update_attribute(:meta_tag_string, parent1.name)
child2.update_attribute(:meta_tag_string, parent2.name)
work = create(:work, fandom_string: "#{child1.name},#{child2.name}")
expect(work.crossover).to be_falsey
end
it "is crossover with fandoms in different meta tag trees" do
rel1 = create(:canonical_fandom, name: "rebuild again eventually")
rel2 = create(:canonical_fandom, name: "evanescence")
meta_tag = create(:canonical_fandom, name: "rebuild")
meta_tag.update_attribute(:sub_tag_string, rel1.name)
super_meta_tag = create(:canonical_fandom, name: "nge")
super_meta_tag.update_attribute(:sub_tag_string, meta_tag.name)
rel1.reload
rel2.reload
meta_tag.reload
super_meta_tag.reload
work = create(:work, fandoms: [rel1, rel2])
expect(work.crossover).to be_truthy
work = create(:work, fandoms: [meta_tag, super_meta_tag])
expect(work.crossover).to be_falsy
end
it "is crossover with unrelated fandoms" do
ships = [create(:canonical_fandom, name: "nge"), create(:canonical_fandom, name: "evanescence")]
work = create(:work, fandoms: ships)
expect(work.crossover).to be_truthy
end
it "is a crossover when missing meta-taggings" do
f1 = create(:canonical_fandom)
f2 = create(:canonical_fandom)
f3 = create(:canonical_fandom)
unrelated = create(:canonical_fandom)
f2.update_attribute(:meta_tag_string, f3.name)
f2.update_attribute(:sub_tag_string, f1.name)
f1.meta_tags.delete(f3)
work = create(:work, fandom_string: "#{f1.name}, #{unrelated.name}")
expect(work.crossover).to be_truthy
end
context "when one tagged fandom has two unrelated meta tags" do
let(:meta1) { create(:canonical_fandom) }
let(:meta2) { create(:canonical_fandom) }
let(:fandom) { create(:canonical_fandom) }
before do
fandom.update_attribute(:meta_tag_string, "#{meta1.name},#{meta2.name}")
end
it "is not a crossover with the fandom's synonym" do
syn = create(:fandom, merger: fandom)
work = create(:work, fandom_string: "#{fandom.name},#{syn.name}")
expect(work.crossover).to be_falsey
end
it "is not a crossover with the fandom's meta tag" do
work = create(:work, fandom_string: "#{fandom.name},#{meta1.name}")
expect(work.crossover).to be_falsey
end
it "is not a crossover with another subtag of the fandom's meta tag" do
sub = create(:canonical_fandom)
sub.update_attribute(:meta_tag_string, meta1.name)
work = create(:work, fandom_string: "#{fandom.name},#{sub.name}")
expect(work.crossover).to be_falsey
end
it "is not a crossover with another fandom sharing the same two meta tags" do
other = create(:canonical_fandom)
other.update_attribute(:meta_tag_string, "#{meta1.name},#{meta2.name}")
work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
expect(work.crossover).to be_falsey
end
it "is not a crossover with another fandom with two unrelated meta tags, only one of which is shared by both fandoms" do
# The tag fandom and the tag other share one meta tag (meta2), but
# fandom has a meta tag meta1 completely unrelated to other, and other
# has a meta tag meta3 completely unrelated to fandom. However, the
# shared meta tag means that they are related, and thus a work tagged
# with both is not a crossover.
meta3 = create(:canonical_fandom)
other = create(:canonical_fandom)
other.update_attribute(:meta_tag_string, "#{meta2.name},#{meta3.name}")
work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
expect(work.crossover).to be_falsey
end
it "is a crossover with another fandom with two unrelated meta tags, when none of the meta tags are shared" do
meta3 = create(:canonical_fandom)
meta4 = create(:canonical_fandom)
other = create(:canonical_fandom)
other.update_attribute(:meta_tag_string, "#{meta3.name},#{meta4.name}")
work = create(:work, fandom_string: "#{fandom.name},#{other.name}")
expect(work.crossover).to be_truthy
end
end
end
describe "#otp" do
it "is not otp with no relationship" do
work = create(:work)
expect(work.relationships).to be_empty
expect(work.otp).to be_falsy
end
it "is otp with only one relationship" do
rel = create(:relationship, name: "asushin")
work = create(:work, relationships: [rel])
expect(work.otp).to be_truthy
end
it "is otp with one canonical relationship and one of its synonyms" do
rel = create(:canonical_relationship, name: "kawoshin")
syn = create(:relationship, name: "shinkawo", merger: rel)
work = create(:work, relationships: [rel, syn])
expect(work.otp).to be_truthy
end
it "is otp with multiple synonyms of the same canonical relationship" do
rel = create(:canonical_relationship, name: "kawoshin")
syn1 = create(:relationship, name: "shinkawo", merger: rel)
syn2 = create(:relationship, name: "kaworu/shinji", merger: rel)
work = create(:work, relationships: [syn1, syn2])
expect(work.otp).to be_truthy
end
it "is not otp with unrelated relationships, one of which is canonical" do
ships = [create(:relationship, name: "shinrei"), create(:canonical_relationship, name: "asurei")]
work = create(:work, relationships: ships)
expect(work.otp).to be_falsy
end
it "is not otp with unrelated relationships" do
ships = [create(:relationship, name: "asushin"), create(:relationship, name: "asurei")]
work = create(:work, relationships: ships)
expect(work.otp).to be_falsy
end
it "is not otp with relationships sharing a meta tag" do
rel1 = create(:canonical_relationship, name: "shinrei")
rel2 = create(:canonical_relationship, name: "asurei")
meta_tag = create(:canonical_relationship)
meta_tag.update_attribute(:sub_tag_string, "#{rel1.name},#{rel2.name}")
rel1.reload
rel2.reload
work = create(:work, relationships: [rel1, rel2])
expect(work.otp).to be_falsy
end
end
describe "#authors_to_sort_on" do
let(:work) { build(:work) }
context "when the pseuds start with special characters" do
it "removes those characters" do
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "-jolyne")]
expect(work.authors_to_sort_on).to eq "jolyne"
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "_hermes")]
expect(work.authors_to_sort_on).to eq "hermes"
end
end
context "when the pseuds start with numbers" do
it "does not remove numbers" do
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "007james")]
expect(work.authors_to_sort_on).to eq "007james"
end
end
context "when the work is anonymous" do
it "returns Anonymous" do
work.in_anon_collection = true
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "stealthy")]
expect(work.authors_to_sort_on).to eq "Anonymous"
end
end
context "when the work has multiple pseuds" do
it "sorts them like the byline then joins them with commas" do
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "diavolo"), Pseud.new(name: "doppio")]
expect(work.authors_to_sort_on).to eq "diavolo, doppio"
allow(work).to receive(:pseuds).and_return [Pseud.new(name: "Tiziano"), Pseud.new(name: "squalo")]
expect(work.authors_to_sort_on).to eq "squalo, tiziano"
end
end
end
describe "work_skin_allowed" do
context "public skin"
context "private skin" do
before :each do
@skin_author = create(:user)
@second_author = create(:user)
@private_skin = create(:work_skin, :private, author_id: @skin_author.id)
end
let(:work_author) { @skin_author }
let(:work) { build(:custom_work_skin, authors: [work_author.pseuds.first], work_skin_id: @private_skin.id) }
it "can be used by the work skin author" do
expect(work.save).to be_truthy
end
let(:work) { build(:custom_work_skin, authors: [@second_author.pseuds.first], work_skin_id: @private_skin.id) }
it "cannot be used by another user" do
work.work_skin_allowed
expect(work.errors[:base]).to include("You do not have permission to use that custom work stylesheet.")
end
end
end
describe "new gifts virtual attribute" do
let(:recipient1) { create(:user).pseuds.first.name }
let(:recipient2) { create(:user).pseuds.first.name }
let(:recipient3) { create(:user).pseuds.first.name }
let(:work) { build(:work) }
before do
work.recipients = "#{recipient1},#{recipient2}"
end
it "contains gifts for the same recipients when they are first added" do
expect(work.new_gifts.collect(&:recipient)).to eq([recipient1, recipient2])
end
it "only contains a gift for the new recipient if replacing the previous recipients" do
work.recipients = recipient3
expect(work.new_gifts.collect(&:recipient)).to eq([recipient3])
end
it "simple assignment works" do
work.recipients = recipient2
expect(work.new_gifts.collect(&:recipient)).to eq([recipient2])
end
it "only contains one gift if the same recipient is entered twice" do
work.recipients = "#{recipient2},#{recipient2}"
expect(work.new_gifts.collect(&:recipient)).to eq([recipient2])
end
end
describe "#find_by_url" do
it "should find imported works with various URL formats" do
%w[http://foo.com/bar.html
http://foo.com/bar
http://lj-site.com/bar/foo?color=blue
https://www.lj-site.com/bar/foo?color=blue
http://www.foo.com/bar https://www.foo.com/bar].each do |url|
work = create(:work, imported_from_url: url)
expect(Work.find_by_url(url)).to eq(work)
work.destroy
end
end
it "should not mix up imported works with similar URLs or significant query parameters" do
{
"http://foo.com/12345" => "http://foo.com/123",
"http://efiction-site.com/viewstory.php?sid=123" => "http://efiction-site.com/viewstory.php?sid=456",
"http://www.foo.com/i-am-something" => "http://foo.com/i-am-something/else"
}.each do |import_url, find_url|
work = create(:work, imported_from_url: import_url)
expect(Work.find_by_url(find_url)).to_not eq(work)
work.destroy
end
end
it "should find works imported with irrelevant query parameters" do
work = create(:work, imported_from_url: "http://lj-site.com/thing1?style=mine")
expect(Work.find_by_url("http://lj-site.com/thing1?style=other")).to eq(work)
work.destroy
end
it "finds works imported with HTTP protocol and irrelevant query parameters" do
work = create(:work, imported_from_url: "http://lj-site.com/thing1?style=mine")
expect(Work.find_by_url("https://lj-site.com/thing1?style=other")).to eq(work)
work.destroy
end
it "finds works imported with HTTPS protocol and irrelevant query parameters" do
work = create(:work, imported_from_url: "https://lj-site.com/thing1?style=mine")
expect(Work.find_by_url("http://lj-site.com/thing1?style=other")).to eq(work)
work.destroy
end
it "gets the work from cache when searching for an imported work by URL" do
url = "http://lj-site.com/thing2"
work = create(:work, imported_from_url: url)
expect(Rails.cache.read(Work.find_by_url_cache_key(url))).to be_nil
expect(Work.find_by_url(url)).to eq(work)
expect(Rails.cache.read(Work.find_by_url_cache_key(url))).to eq(work)
work.destroy
end
end
describe "#update_complete_status" do
it "marks a work complete when it's been completed" do
work = create(:work, expected_number_of_chapters: 1)
expect(work.complete).to be_truthy
end
it "marks a work incomplete when it's no longer completed" do
work = create(:work, expected_number_of_chapters: 1)
work.update!(expected_number_of_chapters: nil)
expect(work.reload.complete).to be_falsey
end
end
describe "#wip_length" do
it "updating chapter count via wip_length sets a sensible expected_number_of_chapters value" do
work = create(:work)
create(:chapter, work: work)
work.reload
work.wip_length = 1
expect(work.expected_number_of_chapters).to be_nil
expect(work.wip_length).to eq("?")
work.wip_length = 2
expect(work.expected_number_of_chapters).to eq(2)
expect(work.wip_length).to eq(work.expected_number_of_chapters)
work.wip_length = 3
expect(work.expected_number_of_chapters).to eq(3)
expect(work.wip_length).to eq(work.expected_number_of_chapters)
end
end
describe "#hide_spam" do
before do
@admin_setting = AdminSetting.first || AdminSetting.create
@work = create(:work)
end
context "when the admin setting is enabled" do
before do
@admin_setting.update_attribute(:hide_spam, true)
end
it "automatically hides spam works and sends an email" do
expect { @work.update!(spam: true) }
.to change { ActionMailer::Base.deliveries.count }
.by(1)
expect(@work.reload.hidden_by_admin).to be_truthy
expect(ActionMailer::Base.deliveries.last.subject).to eq("[AO3] Your work was hidden as spam")
end
end
context "when the admin setting is disabled" do
before do
@admin_setting.update_attribute(:hide_spam, false)
end
it "does not automatically hide spam works and does not send an email" do
expect { @work.update!(spam: true) }
.not_to change { ActionMailer::Base.deliveries.count }
expect(@work.reload.hidden_by_admin).to be_falsey
end
end
end
describe "co-creator permissions" do
let(:creator) { create(:user) }
let(:co_creator) { create(:user) }
let(:no_co_creator) { create(:user) }
before do
# In order to enable co-creator checks (instead of just having everything
# be automatically approved), we need to make sure that User.current_user
# is not nil.
User.current_user = creator
co_creator.preference.update!(allow_cocreator: true)
no_co_creator.preference.update!(allow_cocreator: false)
end
it "allows normal users to invite others as chapter co-creators" do
work = create(:work, authors: creator.pseuds)
work.author_attributes = { byline: co_creator.login }
expect(work).to be_valid
expect(work.save).to be_truthy
expect(work.user_has_creator_invite?(co_creator)).to be_truthy
end
it "doesn't allow users to invite others who disallow co-creators" do
work = create(:work, authors: creator.pseuds)
work.author_attributes = { byline: no_co_creator.login }
expect(work).to be_invalid
expect(work.save).to be_falsey
expect(work.user_has_creator_invite?(no_co_creator)).to be_falsey
end
end
describe "#remove_author" do
let(:to_remove) { create(:user) }
let(:other) { create(:user) }
context "when all the pseuds on the work are owned by one user" do
let(:pseud1) { create(:pseud, user: to_remove) }
let(:pseud2) { create(:pseud, user: to_remove) }
let(:pseud3) { create(:pseud, user: to_remove) }
let!(:work) do
create(:work, authors: [pseud1, pseud2, pseud3])
end
let!(:solo1) { create(:chapter, work: work, authors: [pseud1]) }
let!(:solo2) { create(:chapter, work: work, authors: [pseud2]) }
let!(:solo3) { create(:chapter, work: work, authors: [pseud3]) }
before { work.reload }
it "raises an error" do
expect { work.remove_author(to_remove) }
.to raise_exception(
"Sorry, we can't remove all creators of a work."
)
end
end
context "when the work has a chapter whose sole creator is being removed" do
let!(:work) do
create(:work, authors: [to_remove.default_pseud, other.default_pseud])
end
let!(:solo_chapter) do
create(:chapter, work: work, authors: [to_remove.default_pseud])
end
# Make sure we see the newest chapter:
before { work.reload }
it "sets the chapter's creators equal to the work's" do
work.remove_author(to_remove)
expect(work.pseuds.reload).to contain_exactly(other.default_pseud)
expect(solo_chapter.pseuds.reload).to contain_exactly(other.default_pseud)
end
end
end
describe "#destroy" do
let(:work) { create(:work) }
it "does not save an original creator record" do
expect { work.destroy }
.not_to change { WorkOriginalCreator.count }
end
context "when an original creator exists" do
let!(:original_creator) { create(:work_original_creator, work: work) }
it "deletes the original creator" do
work.destroy
expect { original_creator.reload }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
describe "#allow_collection_invitation?" do
let(:creator1) { create(:user) }
let(:creator2) { create(:user) }
let(:work) { create(:work, authors: [creator1.default_pseud, creator2.default_pseud]) }
context "when all creators allow collection invitations" do
before do
creator1.preference.update!(allow_collection_invitation: true)
creator2.preference.update!(allow_collection_invitation: true)
end
it "returns true" do
expect(work.allow_collection_invitation?).to be true
end
end
context "when all creators disallow collection invitations" do
it "returns false" do
expect(work.allow_collection_invitation?).to be false
end
end
context "when creators have a mix of collection invitation preferences" do
before do
creator1.preference.update!(allow_collection_invitation: true)
end
it "returns true" do
expect(work.allow_collection_invitation?).to be true
end
end
end
end