require "spec_helper" describe AbuseReport do it { is_expected.to validate_presence_of(:url) } context "when report is not spam" do context "valid reports" do it "is valid" do expect(build(:abuse_report)).to be_valid end end context "comment missing" do let(:report_without_comment) { build(:abuse_report, comment: nil) } it "is invalid" do expect(report_without_comment.save).to be_falsey expect(report_without_comment.errors[:comment]).not_to be_empty end end context "comment with weird characters" do it "is valid with slash and dot" do expect(build(:abuse_report, comment: "/.")).to be_valid end it "is valid in other languages" do expect(build(:abuse_report, comment: "café")).to be_valid end it "is valid in other alphabets" do expect(build(:abuse_report, comment: "γεια")).to be_valid end end context "provided email is invalid" do BAD_EMAILS.each do |email| let(:bad_email) { build(:abuse_report, email: email) } it "fails email format check and cannot be created" do expect(bad_email.save).to be_falsey expect(bad_email.errors[:email]).to include("should look like an email address.") end end end context "with a chapter URL that's missing the work id" do context "when the chapter exists" do let(:work) { create(:work) } let(:chapter) { work.chapters.first } let(:missing_work_id) { build(:abuse_report, url: "http://archiveofourown.org/chapters/#{chapter.id}/") } it "saves and adds the correct work id to the URL" do expect(missing_work_id.save).to be_truthy expect(missing_work_id.url).to eq("http://archiveofourown.org/works/#{work.id}/chapters/#{chapter.id}/") end context "when the URL does not include the scheme" do let(:missing_work_id) { build(:abuse_report, url: "archiveofourown.org/chapters/#{chapter.id}/") } it "saves and adds a scheme and correct work id to the URL" do expect(missing_work_id.save).to be_truthy expect(missing_work_id.url).to eq("https://archiveofourown.org/works/#{work.id}/chapters/#{chapter.id}/") end end end context "when the chapter does not exist" do let(:chapter_url) { "http://archiveofourown.org/chapters/000" } let(:missing_work_id) { build(:abuse_report, url: chapter_url) } it "saves without adding a work id to the URL" do expect(missing_work_id.save).to be_truthy expect(missing_work_id.url).to eq("#{chapter_url}/") end context "when the URL does not include the scheme" do let(:chapter_url) { "archiveofourown.org/chapters/000" } let(:missing_work_id) { build(:abuse_report, url: chapter_url) } it "saves and adds a scheme but no work id to the URL" do expect(missing_work_id.save).to be_truthy expect(missing_work_id.url).to eq("https://#{chapter_url}/") end end end end context "with a very long URL" do let(:long_url) { "https://archiveofourown.org/#{'a' * 2080}" } let(:abuse_report) { build(:abuse_report, url: long_url) } it "truncates the url to the maximum length" do expect(abuse_report.save).to be_truthy expect(abuse_report.url).to eq(long_url[0..2079]) end end context "invalid url" do let(:invalid_url) { build(:abuse_report, url: "nothing before #{ArchiveConfig.APP_URL}") } it "text before url" do expect(invalid_url.save).to be_falsey expect(invalid_url.errors[:url]).not_to be_empty end let(:not_from_site) { build(:abuse_report, url: "http://www.google.com/not/our/site") } it "url not from our site" do expect(not_from_site.save).to be_falsey expect(not_from_site.errors[:url]).not_to be_empty end let(:no_url) { build(:abuse_report, url: "") } it "no url" do expect(no_url.save).to be_falsey expect(no_url.errors[:url]).not_to be_empty end end let(:no_email_provided) { build(:abuse_report, email: nil) } it "is invalid if an email is not provided" do expect(no_email_provided.save).to be_falsey expect(no_email_provided.errors[:email]).not_to be_empty end let(:email_provided) { build(:abuse_report) } it "is valid if an email is provided" do expect(email_provided.save).to be_truthy expect(email_provided.errors[:email]).to be_empty end context "for an already-reported work" do work_url = "http://archiveofourown.org/works/1234" let(:common_report) { build(:abuse_report, url: work_url) } it "can be submitted up to a set number of times" do (ArchiveConfig.ABUSE_REPORTS_PER_WORK_MAX - 1).times do create(:abuse_report, url: work_url) end expect(common_report.save).to be_truthy expect(common_report.errors[:base]).to be_empty end end shared_examples "enough already" do |url| let(:report) { build(:abuse_report, url: url) } it "can't be submitted" do expect(report.save).to be_falsey expect(report.errors[:base].first).to include("This page has already been reported.") end end shared_examples "alright" do |url| let(:report) { build(:abuse_report, url: url) } it "can be submitted" do expect(report.save).to be_truthy expect(report.errors[:base]).to be_empty end end context "for a work reported the maximum number of times" do work_url = "http://archiveofourown.org/works/789" before do ArchiveConfig.ABUSE_REPORTS_PER_WORK_MAX.times do create(:abuse_report, url: work_url) end expect(AbuseReport.count).to eq(ArchiveConfig.ABUSE_REPORTS_PER_WORK_MAX) end # obviously it_behaves_like "enough already", work_url # the same work, different protocol it_behaves_like "enough already", "https://archiveofourown.org/works/789" # the same work, with parameters/anchors it_behaves_like "enough already", "http://archiveofourown.org/works/789?smut=yes" it_behaves_like "enough already", "http://archiveofourown.org/works/789?smut=yes#timeline" it_behaves_like "enough already", "http://archiveofourown.org/works/789#timeline" it_behaves_like "enough already", "http://archiveofourown.org/works/789/?smut=yes" it_behaves_like "enough already", "http://archiveofourown.org/works/789/#timeline" # the same work, in a collection it_behaves_like "enough already", "http://archiveofourown.org/collections/rarepair/works/789" # the same work, under users it_behaves_like "enough already", "http://archiveofourown.org/users/author/works/789" it_behaves_like "enough already", "http://archiveofourown.org/users/coauthor/works/789" # the same work, subpages it_behaves_like "enough already", "http://archiveofourown.org/works/789/bookmarks" it_behaves_like "enough already", "http://archiveofourown.org/works/789/collections" it_behaves_like "enough already", "http://archiveofourown.org/works/789/comments" it_behaves_like "enough already", "http://archiveofourown.org/works/789/kudos" # a specific chapter on the work it_behaves_like "enough already", "http://archiveofourown.org/works/789/chapters/123" it_behaves_like "enough already", "http://archiveofourown.org/works/789/chapters/123#major-character-death" it_behaves_like "enough already", "http://archiveofourown.org/works/789/chapters/123?ending=1" it_behaves_like "enough already", "http://archiveofourown.org/works/789/chapters/123?ending=2#major-character-death" # the same work: variations we don't cover it_behaves_like "alright", "http://archiveofourown.org/chapters/123" it_behaves_like "alright", "http://archiveofourown.org/comments/show_comments?work_id=789" # not the same work it_behaves_like "alright", "http://archiveofourown.org/works/9009" it_behaves_like "alright", "http://archiveofourown.org/works/78" it_behaves_like "alright", "http://archiveofourown.org/works/7890" it_behaves_like "alright", "http://archiveofourown.org/external_works/789" # unrelated it_behaves_like "alright", "http://archiveofourown.org/users/someone" context "a month later" do before { travel(32.days) } it_behaves_like "alright", work_url end end context "for a user profile reported the maximum number of times" do user_url = "http://archiveofourown.org/users/someone" before do ArchiveConfig.ABUSE_REPORTS_PER_USER_MAX.times do create(:abuse_report, url: user_url) end expect(AbuseReport.count).to eq(ArchiveConfig.ABUSE_REPORTS_PER_USER_MAX) end # obviously it_behaves_like "enough already", user_url # the same user, different protocol it_behaves_like "enough already", "https://archiveofourown.org/users/someone" # the same user, with parameters/anchors it_behaves_like "enough already", "http://archiveofourown.org/users/someone?sfw=yes" it_behaves_like "enough already", "http://archiveofourown.org/users/someone?sfw=yes#timeline" it_behaves_like "enough already", "http://archiveofourown.org/users/someone#timeline" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/?sfw=yes" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/#timeline" # the same user, as admin (why admin?) it_behaves_like "enough already", "http://archiveofourown.org/admin/users/someone" # the same user, subpages it_behaves_like "enough already", "http://archiveofourown.org/users/someone/bookmarks" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/claims" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/pseuds/" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/pseuds/ghostwriter" it_behaves_like "enough already", "http://archiveofourown.org/users/someone/pseuds/g h o s t w r i t e r" # the same user, Unicode in parameters it_behaves_like "enough already", "http://archiveofourown.org/users/someone/inbox?utf8=✓&filters[read]=false" # not the same user it_behaves_like "alright", "http://archiveofourown.org/users/some" it_behaves_like "alright", "http://archiveofourown.org/users/someoneelse" it_behaves_like "alright", "http://archiveofourown.org/users/somebody" # unrelated it_behaves_like "alright", "http://archiveofourown.org/works/789" context "a month later" do before { travel(32.days) } it_behaves_like "alright", user_url end end context "for a URL that is not a work" do page_url = "http://archiveofourown.org/tags/Testing/works" let(:common_report) { build(:abuse_report, url: page_url) } it "can be submitted an unrestricted number of times" do ArchiveConfig.ABUSE_REPORTS_PER_WORK_MAX.times do create(:abuse_report, url: page_url) end expect(common_report.save).to be_truthy expect(common_report.errors[:base]).to be_empty end end context "for alternate URL format" do let(:report) { build(:abuse_report) } it "no protocol" do report.url = "archiveofourown.org" expect(report.valid?).to be_truthy end it "dot com" do report.url = "http://archiveofourown.com" expect(report.valid?).to be_truthy end it "acronym" do report.url = "http://ao3.org" expect(report.valid?).to be_truthy end end context "when email is valid" do let(:report) { build(:abuse_report, email: "email@example.com") } context "when email has submitted less than the maximum daily number of reports" do before do (ArchiveConfig.ABUSE_REPORTS_PER_EMAIL_MAX - 1).times do create(:abuse_report, email: "email@example.com") end end it "can be submitted" do expect(report.save).to be_truthy expect(report.errors[:base]).to be_empty end end context "when email has submitted the maximum daily number of reports" do before do ArchiveConfig.ABUSE_REPORTS_PER_EMAIL_MAX.times do create(:abuse_report, email: "email@example.com") end end it "can't be submitted" do expect(report.save).to be_falsey expect(report.errors[:base].first).to include("daily reporting limit") end context "when it's a day later" do before { travel(1.day) } it "can be submitted" do expect(report.save).to be_truthy expect(report.errors[:base]).to be_empty end end end end end context "when report is spam" do let(:legit_user) { create(:user) } let(:spam_report) { build(:abuse_report, username: "viagra-test-123") } let!(:safe_report) { build(:abuse_report, username: "viagra-test-123", email: legit_user.email) } before do allow(Akismetor).to receive(:spam?).and_return(true) end it "is not valid if Akismet flags it as spam" do expect(spam_report.save).to be_falsey expect(spam_report.errors[:base]).to include("This report looks like spam to our system!") end it "is valid even if the email casing is different" do legit_user.email = legit_user.email.upcase legit_user.save User.current_user = legit_user expect(safe_report.save).to be_truthy end it "is valid even with spam if logged in and providing correct email" do User.current_user = legit_user expect(safe_report.save).to be_truthy end end context "when report is submitted to Akismet" do let(:report) { build(:abuse_report) } it "has comment_type \"contact-form\"" do expect(report.akismet_attributes[:comment_type]).to eq("contact-form") end it "has user_role \"user-with-nonmatching-email\" when reporter is logged in" do User.current_user = create(:user) expect(report.akismet_attributes[:user_role]).to eq("user-with-nonmatching-email") end it "has user_role \"guest\" when reporter is logged out" do expect(report.akismet_attributes[:user_role]).to eq("guest") end end describe "#attach_work_download" do include ActiveJob::TestHelper def queue_adapter_for_test ActiveJob::QueueAdapters::TestAdapter.new end let(:ticket_id) { "123" } let(:work) { create(:work) } it "does not attach a download for non-work URLs asynchronously" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/users/someone/") expect { subject.attach_work_download(ticket_id) } .not_to have_enqueued_job end it "does not attach a download for comment sub-URLs asynchronously" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/comments/") expect { subject.attach_work_download(ticket_id) } .not_to have_enqueued_job end it "attaches a download for work URLs asynchronously" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect { subject.attach_work_download(ticket_id) } .to have_enqueued_job end end describe "#creator_ids" do it "returns no creator ids for non-work URLs" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/users/someone/") expect(subject.creator_ids).to be_nil end it "returns no creator ids for comment sub-URLs" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/123/comments/") expect(subject.creator_ids).to be_nil end context "for work URLs" do it "returns deletedwork for a work that doesn't exist" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/000/") expect(subject.creator_ids).to eq("deletedwork") end context "for a single creator" do let(:work) { create(:work) } it "returns a single creator id" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq(work.users.first.id.to_s) end end context "for an anonymous work" do let(:anonymous_collection) { create(:anonymous_collection) } let(:work) { create(:work, collections: [anonymous_collection]) } it "returns a single creator id" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq(work.users.first.id.to_s) end end context "for an unrevealed work" do let(:unrevealed_collection) { create(:unrevealed_collection) } let(:work) { create(:work, collections: [unrevealed_collection]) } it "returns a single creator id" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq(work.users.first.id.to_s) end end context "for multiple pseuds of one creator" do let(:user) { create(:user) } let(:pseud) { create(:pseud, user: user) } let(:work) { create(:work, authors: [pseud, user.default_pseud]) } it "returns a single creator id" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq(user.id.to_s) end end context "for multiple creators" do let(:user1) { create(:user, id: 10) } let(:user2) { create(:user, id: 11) } let(:work) { create(:work, authors: [user2.default_pseud, user1.default_pseud]) } it "returns a sorted list of creator ids" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq("#{user1.id}, #{user2.id}") end end context "for an invited co-creator that hasn't accepted yet" do let(:user) { create(:user) } let(:invited) { create(:user) } let(:work) { create(:work, authors: [user.default_pseud, invited.default_pseud]) } let(:creatorship) { work.creatorships.last } before do creatorship.approved = false creatorship.save!(validate: false) end it "returns only the creator" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq(user.id.to_s) end end end context "for an orphaned work" do let!(:orphan_account) { create(:user, login: "orphan_account") } let(:orphaneer) { create(:user, id: 40) } let(:work) { create(:work, authors: [orphaneer.default_pseud]) } context "recently orphaned" do before do Creatorship.orphan([orphaneer.default_pseud], [work], false) end it "returns orphanedwork and the original creator" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq("orphanedwork, #{orphaneer.id}") end end context "orphaned a long time ago" do before do Creatorship.orphan([orphaneer.default_pseud], [work], false) work.original_creators.destroy_all end it "returns orphanedwork" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq("orphanedwork") end end context "partially orphaned" do let(:cocreator) { create(:user, id: 41) } let(:work) { create(:work, authors: [cocreator.default_pseud, orphaneer.default_pseud]) } before do Creatorship.orphan([orphaneer.default_pseud], [work], false) end it "returns a sorted list of orphanedwork, the co-creator and the original creator" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") expect(subject.creator_ids).to eq("orphanedwork, #{orphaneer.id}, #{cocreator.id}") end end end end end