tilde.news/spec/models/story_spec.rb

467 lines
16 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require "rails_helper"
describe Story do
it "should get a short id" do
s = create(:story, :title => "hello", :url => "http://example.com/")
expect(s.short_id).to match(/^\A[a-zA-Z0-9]{1,10}\z/)
end
it "has a limit on the markdown description field" do
s = build(:story)
s.markeddown_description = "Z" * 16_777_218
s.valid?
expect(s.errors[:markeddown_description]).to(
eq(['is too long (maximum is 16777215 characters)'])
)
end
it "has a limit on the twitter id field" do
s = build(:story)
s.twitter_id = "Z" * 25
s.valid?
expect(s.errors[:twitter_id]).to eq(['is too long (maximum is 20 characters)'])
end
it "requires a url or a description" do
expect { create(:story, :title => "hello", :url => "", :description => "") }.to raise_error
expect {
create(:story, :title => "hello", :description => "hi", :url => nil)
}.to_not raise_error
expect {
create(:story, :title => "hello", :url => "http://ex.com/", :description => nil)
}.to_not raise_error
end
it "does not allow too-short titles" do
expect { create(:story, :title => "") }.to raise_error
expect { create(:story, :title => "hi") }.to raise_error
expect { create(:story, :title => "hello") }.to_not raise_error
end
it "does not allow too-long titles" do
expect { create(:story, :title => ("hello" * 100)) }.to raise_error
end
it "must have at least one tag" do
expect { create(:story, :tags_a => nil) }.to raise_error
expect { create(:story, :tags_a => ["", " "]) }.to raise_error
expect { create(:story, :tags_a => ["", "tag1"]) }.to_not raise_error
end
it "removes redundant http port 80 and https port 443" do
expect(Story.new(url: 'http://example.com:80').url).to eq('http://example.com')
expect(Story.new(url: 'http://example.com:80/').url).to eq('http://example.com/')
expect(Story.new(url: 'https://example.com:443').url).to eq('https://example.com')
expect(Story.new(url: 'https://example.com:443/').url).to eq('https://example.com/')
end
it "removes utm_ tracking parameters" do
expect(Story.new(url: 'http://a.com?a=b').url).to eq('http://a.com?a=b')
expect(Story.new(url: 'http://a.com?utm_term=track&c=d').url).to eq('http://a.com?c=d')
expect(Story.new(url: 'http://a.com?a=b&utm_term=track&c=d').url).to eq('http://a.com?a=b&c=d')
end
it "checks for invalid urls" do
expect(Story.new(url: 'http://example.com').tap(&:valid?).errors[:url]).to be_empty
expect(Story.new(url: 'http://example/').tap(&:valid?).errors[:url]).to_not be_empty
expect(Story.new(url: 'ftp://example.com/').tap(&:valid?).errors[:url]).to_not be_empty
expect(Story.new(url: 'http://example.com:123/').tap(&:valid?).errors[:url]).to be_empty
end
it "checks for a previously posted story with same url" do
expect(Story.count).to eq(0)
create(:story, :title => "flim flam", :url => "http://example.com/")
expect(Story.count).to eq(1)
expect {
create(:story, :title => "flim flam 2", :url => "http://example.com/")
}.to raise_error
expect(Story.count).to eq(1)
expect {
create(:story, :title => "flim flam 2", :url => "http://www.example.com/")
}.to raise_error
expect(Story.count).to eq(1)
end
it "parses domain properly" do
story = Story.new
{
"http://example.com" => "example.com",
"https://example.com" => "example.com",
"http://example.com:8000" => "example.com",
"http://example.com:8000/" => "example.com",
"http://www3.example.com/goose" => "example.com",
"http://flub.example.com" => "flub.example.com",
}.each_pair do |url, domain|
story.url = url
expect(story.domain.domain).to eq(domain)
end
end
it "has domain straight out of the db, when Rails doesn't use setters" do
s = create(:story, url: 'https://example.com/foo.html')
s = Story.find(s.id)
expect(s.domain.domain).to eq('example.com')
s.url = 'http://example.org'
expect(s.domain.domain).to eq('example.org')
s.url = 'invalid'
expect(s.domain).to be_nil
end
it "converts a title to a url properly" do
s = create(:story, :title => "Hello there, this is a title")
expect(s.title_as_url).to eq("hello_there_this_is_title")
s = create(:story, :title => "Hello _ underscore")
expect(s.title_as_url).to eq("hello_underscore")
s = create(:story, :title => "Hello, underscore")
expect(s.title_as_url).to eq("hello_underscore")
s = build(:story, :title => "The One-second War (What Time Will You Die?) ")
expect(s.title_as_url).to eq("one_second_war_what_time_will_you_die")
end
it "is not editable by another non-admin user" do
s = create(:story)
expect(s.is_editable_by_user?(s.user)).to be true
u = create(:user)
expect(s.is_editable_by_user?(u)).to be false
end
context 'fetching titles' do
let(:story_directory) { Rails.root.join 'spec/fixtures/story_pages/' }
# this is more elaborate than the previous system, because now it needs to know the content type
def fake_response(content, type, code = '200')
res = Net::HTTPResponse.new(1.0, code, "OK")
res.add_field("content-type", type)
# we can't seemingly just set body, so...
allow(res).to receive(:body).and_return(content)
return res
end
it "can fetch PDF titles properly" do
content = File.read(story_directory + "titled.pdf")
res = fake_response(content, "application/pdf")
s = build(:story)
s.fetched_response = res
expect(s.fetched_attributes[:title]).to eq("Taking a Long Look at QUIC")
end
it "can fetch its title properly" do
content = File.read(story_directory + "title_ampersand.html")
res = fake_response(content, "text/html")
s = build(:story)
s.fetched_response = res
expect(s.fetched_attributes[:title]).to eq("B2G demo & quick hack // by Paul Rouget")
content = File.read(story_directory + "title_google.html")
res = fake_response(content, "text/html")
s = build(:story)
s.fetched_response = res
expect(s.fetched_attributes[:title]).to eq("Google")
end
it "does not fetch title with a port specified" do
expect(Sponge).to_not receive(:new)
story = Story.new url: 'https://example.com:123/'
expect(story.fetched_attributes[:title]).to eq('')
end
it "does not follow rel=canonical when this is to the main page" do
url = "https://www.mcsweeneys.net/articles/who-said-it-donald-trump-or-regina-george"
s = build(:story, url: url)
s.fetched_response = File.read(story_directory + "canonical_root.html")
expect(s.fetched_attributes[:url]).to eq(url)
end
it "does not assign canonical url when the response is non-200" do
url = "https://www.mcsweeneys.net/a/who-said-it-donald-trump-or-regina-george"
content = File.read(story_directory + "canonical_error.html")
res = fake_response(content, "text/html", '404')
expect_any_instance_of(Sponge)
.to receive(:fetch)
.and_return(Net::HTTPResponse.new(1.0, 404, "OK"))
s = build(:story, url: url)
s.fetched_response = res
expect(s.fetched_attributes[:url]).to eq(url)
end
it "assigns canonical when url when it resolves 200" do
url = "https://www.mcsweeneys.net/a/who-said-it-donald-trump-or-regina-george"
canonical = "https://www.mcsweeneys.net/articles/who-said-it-donald-trump-or-regina-george"
content = File.read(story_directory + "canonical_error.html")
res = fake_response(content, "text/html")
expect_any_instance_of(Sponge)
.to receive(:fetch)
.and_return(Net::HTTPResponse.new(1.0, 200, "OK"))
s = build(:story, url: url)
s.fetched_response = res
expect(s.fetched_attributes[:url]).to eq(canonical)
end
context "with unicode" do
it "can fetch unicode titles properly" do
content = "<!DOCTYPE html><html><title>你好世界! Heres a fancy apostrophe</title></html>"
.force_encoding('ASCII-8BIT') # This is the encoding returned by Sponge#fetch
res = fake_response(content, "text/html")
s = build(:story)
s.fetched_response = res
expect(s.fetched_attributes[:title]).to eq("你好世界! Heres a fancy apostrophe")
end
end
end
it "sets the url properly" do
s = build(:story, :title => "blah")
s.url = "https://factorable.net/"
s.valid?
expect(s.url).to eq("https://factorable.net/")
end
it "calculates tag changes properly" do
s = create(:story, :title => "blah", :tags_a => ["tag1", "tag2"])
s.tags_a = ["tag2"]
expect(s.tagging_changes).to eq("tags" => ["tag1 tag2", "tag2"])
end
it "assigning new tags_a should create new_record taggings" do
s = create(:story, tags_a: ['tag1'])
s.tags_a = ['tag1', 'tag2']
expect(s.taggings.map(&:new_record?)).to eq([false, true])
end
it "logs tag additions from user suggestions properly" do
s = create(:story, :title => "blah", :tags_a => ["tag1"], :description => "desc")
u1 = create(:user)
s.save_suggested_tags_a_for_user!(['tag1', 'tag2'], u1)
s.reload
u2 = create(:user)
s.save_suggested_tags_a_for_user!(['tag1', 'tag2'], u2)
mod_log = Moderation.last
expect(mod_log.moderator_user_id).to eq(nil)
expect(mod_log.story_id).to eq(s.id)
expect(mod_log.reason).to match(/Automatically changed/)
expect(mod_log.action).to match(/tags from "tag1" to "tag1 tag2"/)
end
it "logs moderations properly" do
mod = create(:user, :moderator)
s = create(:story, :title => "blah", :tags_a => ["tag1", "tag2"],
:description => "desc")
s.title = "changed title"
s.description = nil
s.tags_a = ["tag1"]
s.editor = mod
s.moderation_reason = "because i hate you"
s.save!
mod_log = Moderation.last
expect(mod_log.moderator_user_id).to eq(mod.id)
expect(mod_log.story_id).to eq(s.id)
expect(mod_log.reason).to eq("because i hate you")
expect(mod_log.action).to match(/title from "blah" to "changed title"/)
expect(mod_log.action).to match(/tags from "tag1 tag2" to "tag1"/)
end
describe "#similar_stories" do
it "finds stories with similar URLs" do
s1 = create(:story, url: 'https://example.com', created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://example.com/')
expect(s1.similar_stories).to eq([s2])
expect(s2.similar_stories).to eq([s1])
end
it "does not include merges" do
s1 = create(:story, url: 'https://example.com', created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://example.com/', merged_story_id: s1.id)
expect(s1.similar_stories).to eq([])
expect(s2.similar_stories).to eq([])
end
it "doesn't throw exceptions at brackets" do
s = create(:story, url: 'http://aaonline.fr/search.php?search&criteria[title-contains]=debian')
expect(s.similar_stories).to eq([])
end
it "finds arxiv html page and pdf URLs with the same arxiv identifier" do
s1 = create(:story,
url: 'https://arxiv.org/abs/2101.07554',
created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://arxiv.org/pdf/2101.07554')
expect(s1.similar_stories).to eq([s2])
expect(s2.similar_stories).to eq([s1])
end
it "finds similar arxiv html page and pdf URLs that contain a pdf extension" do
s1 = create(:story,
url: 'https://arxiv.org/abs/2101.09188',
created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://arxiv.org/pdf/2101.09188.pdf')
expect(s1.similar_stories).to eq([s2])
expect(s2.similar_stories).to eq([s1])
end
it "finds similar www.youtube and youtu.be URLs" do
s1 = create(:story,
url: 'https://www.youtube.com/watch?v=7Pq-S557XQU',
created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://youtu.be/7Pq-S557XQU')
expect(s1.similar_stories).to eq([s2])
expect(s2.similar_stories).to eq([s1])
end
it "finds similar www.youtube and m.youtube URLs" do
s1 = create(:story,
url: 'https://www.youtube.com/watch?v=7Pq-S557XQU',
created_at: (Story::RECENT_DAYS + 1).days.ago)
s2 = create(:story, url: 'https://m.youtube.com/watch?v=7Pq-S557XQU')
expect(s1.similar_stories).to eq([s2])
expect(s2.similar_stories).to eq([s1])
end
end
describe "#calculated_hotness" do
let(:story) do
create(:story, url: 'https://example.com', user_is_author: true)
end
before do
create(:comment, story: story, score: 1, flags: 5)
create(:comment, story: story, score: -9, flags: 10)
# stories stop accepting comments after a while, but this calculation is
# based on created_at, so set that to a known value after posting comments
story.update(created_at: Time.zone.at(0))
end
context "with positive base" do
it "return correct score" do
expect(story.calculated_hotness).to eq(-0.25)
end
end
context "with negative base" do
before do
tag = create(:tag, hotness_mod: -10)
story.update(tags: [tag])
end
it "return correct score" do
expect(story.calculated_hotness).to eq 9.75
end
end
end
describe "#update_comments_count!" do
context "with a merged_into_story" do
let(:merged_into_story) { create(:story) }
let(:story) { create(:story, merged_into_story: merged_into_story) }
it "should also update the merged_into_story's comment count" do
expect(story.comments_count).to eq 0
expect(merged_into_story.comments_count).to eq 0
create(:comment, story: story)
story.update_comments_count!
expect(story.comments_count).to eq 1
expect(merged_into_story.comments_count).to eq 1
end
end
end
describe "#already_posted_recently?" do
it "returns true when trying to submit a URL that's been submitted w/o an anchor in it" do
create(:story, url: "https://www.example.com/article.html")
story_has_url_with_anchor = build(:story, url: "https://www.example.com/article.html#main")
expect(story_has_url_with_anchor.already_posted_recently?).to be true
end
it "returns true when trying to submit a URL that's been submitted with an anchor in it" do
create(:story, url: "https://www.example.com/article.html#main")
story_has_url_without_anchor = build(:story, url: "https://www.example.com/article.html")
expect(story_has_url_without_anchor.already_posted_recently?).to be true
end
end
describe "scopes" do
context "recent" do
it "returns the newest stories that have not yet reached the front page" do
create(:story, title: "Front Page")
create(:story, title: "Front Page 2")
flagged = create(:story, title: "New Story", score: -2, flags: 3)
expect(Story.front_page).to_not include(flagged)
expect(Story.recent).to include(flagged)
expect(Story.recent).to_not include(Story.front_page)
end
end
end
describe "suggestions" do
it "does not auto-accept suggestion if quorum is not met" do
story = create(:story, :title => "hello", :url => "http://example.com/")
user = create(:user)
story.save_suggested_title_for_user!("new title", user)
expect(story.title).to eq("hello")
end
it "auto-accept suggestion once quorum is met" do
story = create(:story, :title => "hello", :url => "http://example.com/")
user1 = create(:user)
user2 = create(:user)
story.save_suggested_title_for_user!("new title", user1)
story.save_suggested_title_for_user!("new title", user2)
expect(story.title).to eq("new title")
end
it "notifies story creator upon auto-accepted suggestion" do
creator = create(:user)
story = create(:story, :user => creator, :title => "hello", :url => "http://example.com/")
user1 = create(:user)
user2 = create(:user)
expect(creator.received_messages.length).to eq(0)
story.save_suggested_title_for_user!("new title", user1)
story.save_suggested_title_for_user!("new title", user2)
expect(creator.reload.received_messages.length).to eq(1)
end
end
end