Skip to content

Commit 4fce97d

Browse files
tests
1 parent 0be073d commit 4fce97d

8 files changed

Lines changed: 306 additions & 1 deletion

File tree

app/models/public_feed.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def get(limit, max_id = nil, since_id = nil, min_id = nil)
4646
attr_reader :account, :options
4747

4848
def prepend_stickies(results)
49-
return results unless allow_local_only?
49+
return results unless local_account?
5050

5151
stickies = Sticky.stickied_statuses.to_a
5252
sticky_ids = stickies.to_set(&:id)

app/models/sticky.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Sticky < ApplicationRecord
1616

1717
scope :stickied_statuses, lambda {
1818
Status.local
19+
.distributable_visibility
1920
.joins(:sticky)
2021
.reorder('stickies.created_at DESC')
2122
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# frozen_string_literal: true
2+
3+
Fabricator(:sticky) do
4+
status
5+
end

spec/models/home_feed_spec.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,39 @@
122122
expect(redis.hget("account:#{account.id}:regeneration", 'status')).to eq 'finished'
123123
end
124124
end
125+
126+
describe '#get with stickies' do
127+
let!(:older_sticky_status) { Fabricate(:status, visibility: :public) }
128+
let!(:newer_sticky_status) { Fabricate(:status, visibility: :public) }
129+
let!(:normal_status_in_feed) { Fabricate(:status, account: account) }
130+
131+
before do
132+
Sticky.create!(status: older_sticky_status, created_at: 2.hours.ago)
133+
Sticky.create!(status: newer_sticky_status, created_at: 1.hour.ago)
134+
redis.zadd(
135+
FeedManager.instance.key(:home, account.id),
136+
[[normal_status_in_feed.id, normal_status_in_feed.id]]
137+
)
138+
end
139+
140+
it 'prepends stickies at the top of the first page, newest first' do
141+
ids = subject.get(20).map(&:id)
142+
expect(ids.first(2)).to eq [newer_sticky_status.id, older_sticky_status.id]
143+
expect(ids).to include(normal_status_in_feed.id)
144+
end
145+
146+
it 'does not prepend stickies on paginated requests with max_id' do
147+
ids = subject.get(20, normal_status_in_feed.id).map(&:id)
148+
expect(ids).to_not include(newer_sticky_status.id, older_sticky_status.id)
149+
end
150+
151+
it 'deduplicates a sticky that would appear naturally in the feed' do
152+
redis.zadd(
153+
FeedManager.instance.key(:home, account.id),
154+
[[newer_sticky_status.id, newer_sticky_status.id]]
155+
)
156+
ids = subject.get(20).map(&:id)
157+
expect(ids.count(newer_sticky_status.id)).to eq 1
158+
end
159+
end
125160
end

spec/models/public_feed_spec.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,4 +719,34 @@
719719
end
720720
end
721721
end
722+
723+
describe '#get with stickies' do
724+
subject { described_class.new(local_account) }
725+
726+
let!(:local_account) { Fabricate(:account, domain: nil) }
727+
let!(:older_sticky_status) { Fabricate(:status, id: 1, created_at: 3.hours.ago, visibility: :public, account: local_account) }
728+
let!(:newer_sticky_status) { Fabricate(:status, id: 3, created_at: 1.hour.ago, visibility: :public, account: local_account) }
729+
let!(:normal_status) { Fabricate(:status, id: 2, created_at: 2.hours.ago, visibility: :public, account: local_account) }
730+
731+
before do
732+
Sticky.create!(status: older_sticky_status, created_at: 2.hours.ago)
733+
Sticky.create!(status: newer_sticky_status, created_at: 1.hour.ago)
734+
end
735+
736+
it 'prepends stickies at the top of the first page, newest first' do
737+
ids = subject.get(20).map(&:id)
738+
expect(ids.first(2)).to eq [newer_sticky_status.id, older_sticky_status.id]
739+
expect(ids).to include(normal_status.id)
740+
end
741+
742+
it 'deduplicates a sticky that would also appear naturally in the feed' do
743+
ids = subject.get(20).map(&:id)
744+
expect(ids.count(newer_sticky_status.id)).to eq 1
745+
end
746+
747+
it 'does not pin stickies on paginated requests with max_id' do
748+
ids = subject.get(20, normal_status.id).map(&:id)
749+
expect(ids).to eq [older_sticky_status.id]
750+
end
751+
end
722752
end

spec/models/sticky_spec.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe Sticky do
6+
describe 'Validations' do
7+
it 'requires a status' do
8+
sticky = described_class.new
9+
expect(sticky).to_not be_valid
10+
expect(sticky.errors[:status]).to be_present
11+
end
12+
end
13+
14+
describe 'Association' do
15+
it 'is destroyed when the parent status is destroyed' do
16+
status = Fabricate(:status)
17+
described_class.create!(status: status)
18+
19+
expect { status.destroy }.to change(described_class, :count).by(-1)
20+
end
21+
end
22+
23+
describe '.stickied_statuses' do
24+
let!(:older_sticky_status) { Fabricate(:status, visibility: :public) }
25+
let!(:newer_sticky_status) { Fabricate(:status, visibility: :public) }
26+
27+
before do
28+
described_class.create!(status: older_sticky_status, created_at: 2.hours.ago)
29+
described_class.create!(status: newer_sticky_status, created_at: 1.hour.ago)
30+
end
31+
32+
it 'returns sticky statuses ordered by sticky creation desc' do
33+
expect(described_class.stickied_statuses.to_a).to eq [newer_sticky_status, older_sticky_status]
34+
end
35+
36+
it 'excludes private and direct statuses' do
37+
private_status = Fabricate(:status, visibility: :private)
38+
direct_status = Fabricate(:status, visibility: :direct)
39+
described_class.create!(status: private_status)
40+
described_class.create!(status: direct_status)
41+
42+
ids = described_class.stickied_statuses.pluck(:id)
43+
expect(ids).to_not include(private_status.id, direct_status.id)
44+
end
45+
end
46+
end
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Stickies' do
6+
let(:user) { Fabricate(:moderator_user) }
7+
let(:scopes) { 'write:statuses' }
8+
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
9+
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
10+
11+
describe 'POST /api/v1/statuses/:status_id/sticky' do
12+
subject do
13+
post "/api/v1/statuses/#{status.id}/sticky", headers: headers
14+
end
15+
16+
let(:status) { Fabricate(:status) }
17+
18+
it_behaves_like 'forbidden for wrong scope', 'read'
19+
20+
context 'when a moderator' do
21+
it 'creates a sticky and returns updated json with sticky: true', :aggregate_failures do
22+
subject
23+
24+
expect(response).to have_http_status(200)
25+
expect(Sticky.exists?(status_id: status.id)).to be true
26+
expect(response.parsed_body).to match(
27+
a_hash_including(id: status.id.to_s, sticky: true)
28+
)
29+
expect(response.parsed_body.keys.map(&:to_s)).to include('sticky')
30+
end
31+
32+
it 'is idempotent when called twice' do
33+
subject
34+
expect { post "/api/v1/statuses/#{status.id}/sticky", headers: headers }
35+
.to_not change(Sticky, :count)
36+
expect(response).to have_http_status(200)
37+
end
38+
end
39+
40+
context 'when a regular user' do
41+
let(:user) { Fabricate(:user) }
42+
43+
it 'returns http forbidden' do
44+
subject
45+
expect(response).to have_http_status(403)
46+
end
47+
end
48+
49+
context 'without an authorization header' do
50+
let(:headers) { {} }
51+
52+
it 'returns http unauthorized' do
53+
subject
54+
expect(response).to have_http_status(401)
55+
end
56+
end
57+
58+
context 'when the status does not exist' do
59+
it 'returns http not found' do
60+
post '/api/v1/statuses/-1/sticky', headers: headers
61+
expect(response).to have_http_status(404)
62+
end
63+
end
64+
end
65+
66+
describe 'POST /api/v1/statuses/:status_id/unsticky' do
67+
subject do
68+
post "/api/v1/statuses/#{status.id}/unsticky", headers: headers
69+
end
70+
71+
let(:status) { Fabricate(:status) }
72+
73+
it_behaves_like 'forbidden for wrong scope', 'read'
74+
75+
context 'when a moderator' do
76+
context 'when the status is currently sticky' do
77+
before { Sticky.create!(status: status) }
78+
79+
it 'removes the sticky and returns updated json with sticky: false', :aggregate_failures do
80+
subject
81+
82+
expect(response).to have_http_status(200)
83+
expect(Sticky.exists?(status_id: status.id)).to be false
84+
expect(response.parsed_body).to match(
85+
a_hash_including(id: status.id.to_s, sticky: false)
86+
)
87+
end
88+
end
89+
90+
context 'when the status is not sticky' do
91+
it 'returns http success without error' do
92+
subject
93+
expect(response).to have_http_status(200)
94+
expect(response.parsed_body).to match(
95+
a_hash_including(id: status.id.to_s, sticky: false)
96+
)
97+
end
98+
end
99+
end
100+
101+
context 'when a regular user' do
102+
let(:user) { Fabricate(:user) }
103+
104+
before { Sticky.create!(status: status) }
105+
106+
it 'returns http forbidden and does not remove the sticky', :aggregate_failures do
107+
subject
108+
expect(response).to have_http_status(403)
109+
expect(Sticky.exists?(status_id: status.id)).to be true
110+
end
111+
end
112+
end
113+
end

spec/system/stickies_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Sticky posts', :inline_jobs, :js do
6+
include ProfileStories
7+
8+
let(:email) { 'test@example.com' }
9+
let(:password) { 'password' }
10+
let(:confirmed_at) { Time.zone.now }
11+
let(:finished_onboarding) { true }
12+
let!(:other_account) { Fabricate(:account, username: 'alice') }
13+
let!(:target_status) { Fabricate(:status, account: other_account, text: 'Hello from Alice', visibility: :public) }
14+
15+
before do
16+
Setting.local_live_feed_access = 'public'
17+
as_a_logged_in_user
18+
bob.update!(role: UserRole.find_by!(name: 'Moderator'))
19+
page.refresh
20+
end
21+
22+
it 'toggles stickiness from off to on' do
23+
ignore_js_error(/Failed to load resource/)
24+
25+
visit '/public/local'
26+
27+
expect(page).to have_css('.status', text: 'Hello from Alice')
28+
29+
within(first('.status')) do
30+
find('button[title="More"]').click_button
31+
end
32+
33+
within('.dropdown-menu') do
34+
expect(page).to have_text('Make this post globally sticky')
35+
click_on 'Make this post globally sticky'
36+
end
37+
38+
within(first('.status')) do
39+
find('button[title="More"]').click_button
40+
end
41+
42+
within('.dropdown-menu') do
43+
expect(page).to have_text('Unsticky this post')
44+
end
45+
46+
expect(Sticky.exists?(status_id: target_status.id)).to be true
47+
end
48+
49+
it 'toggles stickiness from on to off' do
50+
Sticky.create!(status: target_status)
51+
52+
ignore_js_error(/Failed to load resource/)
53+
54+
visit '/public/local'
55+
56+
within(first('.status')) do
57+
find('button[title="More"]').click_button
58+
end
59+
60+
within('.dropdown-menu') do
61+
click_on 'Unsticky this post'
62+
end
63+
64+
# Reopen and confirm we're back to "Make sticky"
65+
within(first('.status')) do
66+
find('button[title="More"]').click_button
67+
end
68+
69+
within('.dropdown-menu') do
70+
expect(page).to have_text('Make this post globally sticky')
71+
end
72+
73+
expect(Sticky.exists?(status_id: target_status.id)).to be false
74+
end
75+
end

0 commit comments

Comments
 (0)