diff options
Diffstat (limited to 'spec')
60 files changed, 2000 insertions, 246 deletions
diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 92f888590..a8ade790c 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -4,6 +4,7 @@ RSpec.describe AccountsController, type: :controller do render_views let(:alice) { Fabricate(:account, username: 'alice') } + let(:eve) { Fabricate(:user) } describe 'GET #show' do let!(:status1) { Status.create!(account: alice, text: 'Hello world') } @@ -19,93 +20,123 @@ RSpec.describe AccountsController, type: :controller do let!(:status_pin3) { StatusPin.create!(account: alice, status: status7, created_at: 10.minutes.ago) } before do + alice.block!(eve.account) status3.media_attachments.create!(account: alice, file: fixture_file_upload('files/attachment.jpg', 'image/jpeg')) end - context 'atom' do + shared_examples 'responses' do before do - get :show, params: { username: alice.username, max_id: status4.stream_entry.id, since_id: status1.stream_entry.id }, format: 'atom' + sign_in(current_user) if defined? current_user + get :show, params: { + username: alice.username, + max_id: (max_id if defined? max_id), + since_id: (since_id if defined? since_id), + current_user: (current_user if defined? current_user), + }, format: format end it 'assigns @account' do expect(assigns(:account)).to eq alice end - it 'assigns @entries' do - entries = assigns(:entries).to_a - expect(entries.size).to eq 2 - expect(entries[0].status).to eq status3 - expect(entries[1].status).to eq status2 + it 'returns http success' do + expect(response).to have_http_status(:success) end - it 'returns http success with Atom' do - expect(response).to have_http_status(:success) + it 'returns correct format' do + expect(response.content_type).to eq content_type end end - context 'activitystreams2' do - before do - get :show, params: { username: alice.username }, format: 'json' - end + context 'atom' do + let(:format) { 'atom' } + let(:content_type) { 'application/atom+xml' } - it 'assigns @account' do - expect(assigns(:account)).to eq alice + shared_examples 'responsed streams' do + it 'assigns @entries' do + entries = assigns(:entries).to_a + expect(entries.size).to eq expected_statuses.size + entries.each.zip(expected_statuses.each) do |entry, expected_status| + expect(entry.status).to eq expected_status + end + end end - it 'returns http success with Activity Streams 2.0' do - expect(response).to have_http_status(:success) - end + include_examples 'responses' - it 'returns application/activity+json' do - expect(response.content_type).to eq 'application/activity+json' - end - end + context 'without max_id nor since_id' do + let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] } - context 'html without since_id nor max_id' do - before do - get :show, params: { username: alice.username } + include_examples 'responsed streams' end - it 'assigns @account' do - expect(assigns(:account)).to eq alice - end + context 'with max_id and since_id' do + let(:max_id) { status4.stream_entry.id } + let(:since_id) { status1.stream_entry.id } + let(:expected_statuses) { [status3, status2] } - it 'assigns @pinned_statuses' do - pinned_statuses = assigns(:pinned_statuses).to_a - expect(pinned_statuses.size).to eq 3 - expect(pinned_statuses[0]).to eq status7 - expect(pinned_statuses[1]).to eq status5 - expect(pinned_statuses[2]).to eq status6 + include_examples 'responsed streams' end + end - it 'returns http success' do - expect(response).to have_http_status(:success) - end + context 'activitystreams2' do + let(:format) { 'json' } + let(:content_type) { 'application/activity+json' } + + include_examples 'responses' end - context 'html with since_id and max_id' do - before do - get :show, params: { username: alice.username, max_id: status4.id, since_id: status1.id } - end + context 'html' do + let(:format) { nil } + let(:content_type) { 'text/html' } - it 'assigns @account' do - expect(assigns(:account)).to eq alice - end + shared_examples 'responsed statuses' do + it 'assigns @pinned_statuses' do + pinned_statuses = assigns(:pinned_statuses).to_a + expect(pinned_statuses.size).to eq expected_pinned_statuses.size + pinned_statuses.each.zip(expected_pinned_statuses.each) do |pinned_status, expected_pinned_status| + expect(pinned_status).to eq expected_pinned_status + end + end - it 'assigns @statuses' do - statuses = assigns(:statuses).to_a - expect(statuses.size).to eq 2 - expect(statuses[0]).to eq status3 - expect(statuses[1]).to eq status2 + it 'assigns @statuses' do + statuses = assigns(:statuses).to_a + expect(statuses.size).to eq expected_statuses.size + statuses.each.zip(expected_statuses.each) do |status, expected_status| + expect(status).to eq expected_status + end + end end - it 'assigns an empty array to @pinned_statuses' do - pinned_statuses = assigns(:pinned_statuses).to_a - expect(pinned_statuses.size).to eq 0 + include_examples 'responses' + + context 'with anonymous visitor' do + context 'without since_id nor max_id' do + let(:expected_statuses) { [status7, status6, status5, status4, status3, status2, status1] } + let(:expected_pinned_statuses) { [status7, status5, status6] } + + include_examples 'responsed statuses' + end + + context 'with since_id nor max_id' do + let(:max_id) { status4.id } + let(:since_id) { status1.id } + let(:expected_statuses) { [status3, status2] } + let(:expected_pinned_statuses) { [] } + + include_examples 'responsed statuses' + end end - it 'returns http success' do - expect(response).to have_http_status(:success) + context 'with blocked visitor' do + let(:current_user) { eve } + + context 'without since_id nor max_id' do + let(:expected_statuses) { [] } + let(:expected_pinned_statuses) { [] } + + include_examples 'responsed statuses' + end end end end diff --git a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb index 461b8b34b..247420d08 100644 --- a/spec/controllers/api/v1/accounts/credentials_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/credentials_controller_spec.rb @@ -51,7 +51,9 @@ describe Api::V1::Accounts::CredentialsController do describe 'with invalid data' do before do - patch :update, params: { note: 'This is too long. ' * 10 } + note = 'This is too long. ' + note = note + 'a' * (Account::MAX_NOTE_LENGTH - note.length + 1) + patch :update, params: { note: note } end it 'returns http unprocessable entity' do diff --git a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb index 431fc2194..f25b86ac1 100644 --- a/spec/controllers/api/v1/accounts/relationships_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/relationships_controller_spec.rb @@ -32,7 +32,7 @@ describe Api::V1::Accounts::RelationshipsController do json = body_as_json expect(json).to be_a Enumerable - expect(json.first[:following]).to be true + expect(json.first[:following]).to be_truthy expect(json.first[:followed_by]).to be false end end @@ -51,7 +51,7 @@ describe Api::V1::Accounts::RelationshipsController do expect(json).to be_a Enumerable expect(json.first[:id]).to eq simon.id.to_s - expect(json.first[:following]).to be true + expect(json.first[:following]).to be_truthy expect(json.first[:followed_by]).to be false expect(json.first[:muting]).to be false expect(json.first[:requested]).to be false diff --git a/spec/controllers/api/v1/accounts_controller_spec.rb b/spec/controllers/api/v1/accounts_controller_spec.rb index c770649ec..f3b879421 100644 --- a/spec/controllers/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/api/v1/accounts_controller_spec.rb @@ -31,10 +31,10 @@ RSpec.describe Api::V1::AccountsController, type: :controller do expect(response).to have_http_status(:success) end - it 'returns JSON with following=true and requested=false' do + it 'returns JSON with following=truthy and requested=false' do json = body_as_json - expect(json[:following]).to be true + expect(json[:following]).to be_truthy expect(json[:requested]).to be false end @@ -50,11 +50,11 @@ RSpec.describe Api::V1::AccountsController, type: :controller do expect(response).to have_http_status(:success) end - it 'returns JSON with following=false and requested=true' do + it 'returns JSON with following=false and requested=truthy' do json = body_as_json expect(json[:following]).to be false - expect(json[:requested]).to be true + expect(json[:requested]).to be_truthy end it 'creates a follow request relation between user and target user' do @@ -137,6 +137,35 @@ RSpec.describe Api::V1::AccountsController, type: :controller do it 'creates a muting relation' do expect(user.account.muting?(other_account)).to be true end + + it 'mutes notifications' do + expect(user.account.muting_notifications?(other_account)).to be true + end + end + + describe 'POST #mute with notifications set to false' do + let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + user.account.follow!(other_account) + post :mute, params: {id: other_account.id, notifications: false } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'does not remove the following relation between user and target user' do + expect(user.account.following?(other_account)).to be true + end + + it 'creates a muting relation' do + expect(user.account.muting?(other_account)).to be true + end + + it 'does not mute notifications' do + expect(user.account.muting_notifications?(other_account)).to be false + end end describe 'POST #unmute' do diff --git a/spec/controllers/api/v1/lists/accounts_controller_spec.rb b/spec/controllers/api/v1/lists/accounts_controller_spec.rb new file mode 100644 index 000000000..953e5909d --- /dev/null +++ b/spec/controllers/api/v1/lists/accounts_controller_spec.rb @@ -0,0 +1,54 @@ +require 'rails_helper' + +describe Api::V1::Lists::AccountsController do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let(:list) { Fabricate(:list, account: user.account) } + + before do + follow = Fabricate(:follow, account: user.account) + list.accounts << follow.target_account + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #index' do + it 'returns http success' do + get :show, params: { list_id: list.id } + + expect(response).to have_http_status(:success) + end + end + + describe 'POST #create' do + let(:bob) { Fabricate(:account, username: 'bob') } + + before do + user.account.follow!(bob) + post :create, params: { list_id: list.id, account_ids: [bob.id] } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'adds account to the list' do + expect(list.accounts.include?(bob)).to be true + end + end + + describe 'DELETE #destroy' do + before do + delete :destroy, params: { list_id: list.id, account_ids: [list.accounts.first.id] } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'removes account from the list' do + expect(list.accounts.count).to eq 0 + end + end +end diff --git a/spec/controllers/api/v1/lists_controller_spec.rb b/spec/controllers/api/v1/lists_controller_spec.rb new file mode 100644 index 000000000..be08c221f --- /dev/null +++ b/spec/controllers/api/v1/lists_controller_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +RSpec.describe Api::V1::ListsController, type: :controller do + render_views + + let!(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read write') } + let!(:list) { Fabricate(:list, account: user.account) } + + before { allow(controller).to receive(:doorkeeper_token) { token } } + + describe 'GET #index' do + it 'returns http success' do + get :index + expect(response).to have_http_status(:success) + end + end + + describe 'GET #show' do + it 'returns http success' do + get :show, params: { id: list.id } + expect(response).to have_http_status(:success) + end + end + + describe 'POST #create' do + before do + post :create, params: { title: 'Foo bar' } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'creates list' do + expect(List.where(account: user.account).count).to eq 2 + expect(List.last.title).to eq 'Foo bar' + end + end + + describe 'PUT #update' do + before do + put :update, params: { id: list.id, title: 'Updated title' } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'updates the list' do + expect(list.reload.title).to eq 'Updated title' + end + end + + describe 'DELETE #destroy' do + before do + delete :destroy, params: { id: list.id } + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'deletes the list' do + expect(List.find_by(id: list.id)).to be_nil + end + end +end diff --git a/spec/controllers/api/v1/mutes_controller_spec.rb b/spec/controllers/api/v1/mutes_controller_spec.rb index 3e6fa887b..7387b9d2d 100644 --- a/spec/controllers/api/v1/mutes_controller_spec.rb +++ b/spec/controllers/api/v1/mutes_controller_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } before do - Fabricate(:mute, account: user.account) + Fabricate(:mute, account: user.account, hide_notifications: false) allow(controller).to receive(:doorkeeper_token) { token } end @@ -18,4 +18,24 @@ RSpec.describe Api::V1::MutesController, type: :controller do expect(response).to have_http_status(:success) end end + + describe 'GET #details' do + before do + get :details, params: { limit: 1 } + end + + let(:mutes) { JSON.parse(response.body) } + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'returns one mute' do + expect(mutes.size).to be(1) + end + + it 'returns whether the mute hides notifications' do + expect(mutes.first["hide_notifications"]).to be(false) + end + end end diff --git a/spec/controllers/api/v1/timelines/list_controller_spec.rb b/spec/controllers/api/v1/timelines/list_controller_spec.rb new file mode 100644 index 000000000..07eba955a --- /dev/null +++ b/spec/controllers/api/v1/timelines/list_controller_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Api::V1::Timelines::ListController do + render_views + + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:list) { Fabricate(:list, account: user.account) } + + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + context 'with a user context' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') } + + describe 'GET #show' do + before do + follow = Fabricate(:follow, account: user.account) + list.accounts << follow.target_account + PostStatusService.new.call(follow.target_account, 'New status for user home timeline.') + end + + it 'returns http success' do + get :show, params: { id: list.id } + expect(response).to have_http_status(:success) + end + end + end + + context 'with the wrong user context' do + let(:other_user) { Fabricate(:user, account: Fabricate(:account, username: 'bob')) } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: other_user.id, scopes: 'read') } + + describe 'GET #show' do + it 'returns http not found' do + get :show, params: { id: list.id } + expect(response).to have_http_status(:not_found) + end + end + end + + context 'without a user context' do + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: nil, scopes: 'read') } + + describe 'GET #show' do + it 'returns http unprocessable entity' do + get :show, params: { id: list.id } + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.headers['Link']).to be_nil + end + end + end +end diff --git a/spec/controllers/api/v1/timelines/tag_controller_spec.rb b/spec/controllers/api/v1/timelines/tag_controller_spec.rb index 74de1e81f..6c66ee58e 100644 --- a/spec/controllers/api/v1/timelines/tag_controller_spec.rb +++ b/spec/controllers/api/v1/timelines/tag_controller_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' describe Api::V1::Timelines::TagController do render_views - let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } + let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } before do allow(controller).to receive(:doorkeeper_token) { token } diff --git a/spec/controllers/settings/applications_controller_spec.rb b/spec/controllers/settings/applications_controller_spec.rb index ca66f8d23..90e6a63d5 100644 --- a/spec/controllers/settings/applications_controller_spec.rb +++ b/spec/controllers/settings/applications_controller_spec.rb @@ -2,10 +2,10 @@ require 'rails_helper' describe Settings::ApplicationsController do render_views - + let!(:user) { Fabricate(:user) } let!(:app) { Fabricate(:application, owner: user) } - + before do sign_in user, scope: :user end @@ -21,7 +21,7 @@ describe Settings::ApplicationsController do end end - + describe 'GET #show' do it 'returns http success' do get :show, params: { id: app.id } @@ -110,7 +110,7 @@ describe Settings::ApplicationsController do end end end - + describe 'PATCH #update' do context 'success' do let(:opts) { @@ -131,7 +131,7 @@ describe Settings::ApplicationsController do call_update expect(app.reload.website).to eql(opts[:website]) end - + it 'redirects back to applications page' do expect(call_update).to redirect_to(settings_applications_path) end diff --git a/spec/controllers/settings/keyword_mutes_controller_spec.rb b/spec/controllers/settings/keyword_mutes_controller_spec.rb new file mode 100644 index 000000000..a8c37a072 --- /dev/null +++ b/spec/controllers/settings/keyword_mutes_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Settings::KeywordMutesController, type: :controller do + +end diff --git a/spec/fabricators/glitch_keyword_mute_fabricator.rb b/spec/fabricators/glitch_keyword_mute_fabricator.rb new file mode 100644 index 000000000..20d393320 --- /dev/null +++ b/spec/fabricators/glitch_keyword_mute_fabricator.rb @@ -0,0 +1,2 @@ +Fabricator('Glitch::KeywordMute') do +end diff --git a/spec/fabricators/list_account_fabricator.rb b/spec/fabricators/list_account_fabricator.rb new file mode 100644 index 000000000..30e4004aa --- /dev/null +++ b/spec/fabricators/list_account_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:list_account) do + list nil + account nil + follow nil +end diff --git a/spec/fabricators/list_fabricator.rb b/spec/fabricators/list_fabricator.rb new file mode 100644 index 000000000..d249c2029 --- /dev/null +++ b/spec/fabricators/list_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:list) do + account nil + title "MyString" +end diff --git a/spec/fabricators/session_activation_fabricator.rb b/spec/fabricators/session_activation_fabricator.rb index 46050bdab..526faaec2 100644 --- a/spec/fabricators/session_activation_fabricator.rb +++ b/spec/fabricators/session_activation_fabricator.rb @@ -1,4 +1,4 @@ Fabricator(:session_activation) do - user_id 1 + user session_id "MyString" end diff --git a/spec/fabricators/setting_fabricator.rb b/spec/fabricators/setting_fabricator.rb new file mode 100644 index 000000000..336d7c355 --- /dev/null +++ b/spec/fabricators/setting_fabricator.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +Fabricator(:setting) do +end diff --git a/spec/fixtures/files/mini-static.gif b/spec/fixtures/files/mini-static.gif new file mode 100644 index 000000000..fe597b215 --- /dev/null +++ b/spec/fixtures/files/mini-static.gif Binary files differdiff --git a/spec/fixtures/requests/attachment1.txt b/spec/fixtures/requests/attachment1.txt index 77fd9c836..30bd456be 100644 --- a/spec/fixtures/requests/attachment1.txt +++ b/spec/fixtures/requests/attachment1.txt Binary files differdiff --git a/spec/fixtures/requests/attachment2.txt b/spec/fixtures/requests/attachment2.txt index 917a1d398..2a252d2de 100644 --- a/spec/fixtures/requests/attachment2.txt +++ b/spec/fixtures/requests/attachment2.txt Binary files differdiff --git a/spec/fixtures/requests/avatar.txt b/spec/fixtures/requests/avatar.txt index d57b0984f..d771f5dda 100644 --- a/spec/fixtures/requests/avatar.txt +++ b/spec/fixtures/requests/avatar.txt Binary files differdiff --git a/spec/fixtures/requests/idn.txt b/spec/fixtures/requests/idn.txt index 3c76c59c0..5d07f2b79 100644 --- a/spec/fixtures/requests/idn.txt +++ b/spec/fixtures/requests/idn.txt @@ -6,7 +6,7 @@ Content-Length: 38111 Last-Modified: Wed, 20 Jul 2016 02:50:52 GMT Connection: keep-alive Accept-Ranges: bytes - + <!DOCTYPE html> <html> <head> @@ -21,16 +21,16 @@ Accept-Ranges: bytes var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); - + </script> - - + + <link rel="stylesheet" type="text/css" href="css/common.css"/> <script src="js/jquery-1.11.1.min.js" type="text/javascript" charset="utf-8"></script> <script src="js/common.js" type="text/javascript" charset="utf-8"></script> <script src="js/carousel.js" type="text/javascript" charset="utf-8"></script> <title>中国域名网站</title> - + </head> <body> <div class="head-tips" id="headTip"> @@ -453,7 +453,7 @@ Accept-Ranges: bytes <li><a href="http://新疆农业大学.中国" target="_blank">新疆农业大学.中国</a></li> <li><a href="http://浙江万里学院.中国" target="_blank">浙江万里学院.中国</a></li> <li><a href="http://重庆大学.中国" target="_blank">重庆大学.中国</a></li> - + </ul> </div> </div> @@ -472,7 +472,7 @@ Accept-Ranges: bytes <script> $("#headTip").hide() var hostname = window.location.hostname || ""; - + var tips = "您所访问的域名 <font size='' color='#ff0000'>" + hostname +"</font> 无法到达,您可以尝试重新访问,或使用搜索相关信息" if (hostname != "导航.中国") { $("#headTip").html(tips); diff --git a/spec/helpers/settings/keyword_mutes_helper_spec.rb b/spec/helpers/settings/keyword_mutes_helper_spec.rb new file mode 100644 index 000000000..a19d518dd --- /dev/null +++ b/spec/helpers/settings/keyword_mutes_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Settings::KeywordMutesHelper. For example: +# +# describe Settings::KeywordMutesHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Settings::KeywordMutesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/stream_entries_helper_spec.rb b/spec/helpers/stream_entries_helper_spec.rb index 2c0d7b239..1de6691ba 100644 --- a/spec/helpers/stream_entries_helper_spec.rb +++ b/spec/helpers/stream_entries_helper_spec.rb @@ -77,7 +77,7 @@ RSpec.describe StreamEntriesHelper, type: :helper do params[:controller] = StreamEntriesHelper::EMBEDDED_CONTROLLER params[:action] = StreamEntriesHelper::EMBEDDED_ACTION end - + describe '#style_classes' do it do status = double(reblog?: false) @@ -202,7 +202,7 @@ RSpec.describe StreamEntriesHelper, type: :helper do expect(css_class).to eq 'h-cite' end end - + describe '#rtl?' do it 'is false if text is empty' do expect(helper).not_to be_rtl '' diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 0f97a579e..ba96b6e7e 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -56,6 +56,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true end + it 'returns true for reblog from account with reblogs disabled' do + status = Fabricate(:status, text: 'Hello world', account: jeff) + reblog = Fabricate(:status, reblog: status, account: alice) + bob.follow!(alice, reblogs: false) + expect(FeedManager.instance.filter?(:home, reblog, bob.id)).to be true + end + it 'returns false for reply by followee to another followee' do status = Fabricate(:status, text: 'Hello world', account: jeff) reply = Fabricate(:status, text: 'Nay', thread: status, account: alice) @@ -105,6 +112,13 @@ RSpec.describe FeedManager do expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true end + it 'returns true for status by followee mentioning muted account' do + bob.mute!(jeff) + bob.follow!(alice) + status = PostStatusService.new.call(alice, 'Hey @jeff') + expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true + end + it 'returns true for reblog of a personally blocked domain' do alice.block_domain!('example.com') alice.follow!(jeff) @@ -112,6 +126,44 @@ RSpec.describe FeedManager do reblog = Fabricate(:status, reblog: status, account: jeff) expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true end + + it 'returns true for a status containing a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + status = Fabricate(:status, text: 'This is a hot take', account: bob) + + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end + + it 'returns true for a reply containing a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + s1 = Fabricate(:status, text: 'Something', account: alice) + s2 = Fabricate(:status, text: 'This is a hot take', thread: s1, account: bob) + + expect(FeedManager.instance.filter?(:home, s2, alice.id)).to be true + end + + it 'returns true for a status whose spoiler text contains a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob) + + expect(FeedManager.instance.filter?(:home, status, alice.id)).to be true + end + + it 'returns true for a reblog containing a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + status = Fabricate(:status, text: 'This is a hot take', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + + expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true + end + + it 'returns true for a reblog whose spoiler text contains a muted keyword' do + Fabricate('Glitch::KeywordMute', account: alice, keyword: 'take') + status = Fabricate(:status, spoiler_text: 'This is a hot take', account: bob) + reblog = Fabricate(:status, reblog: status, account: jeff) + + expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true + end end context 'for mentions feed' do @@ -140,6 +192,13 @@ RSpec.describe FeedManager do bob.follow!(alice) expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false end + + it 'returns true for status that contains a muted keyword' do + Fabricate('Glitch::KeywordMute', account: bob, keyword: 'take') + status = Fabricate(:status, text: 'This is a hot take', account: alice) + bob.follow!(alice) + expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true + end end end @@ -148,21 +207,11 @@ RSpec.describe FeedManager do account = Fabricate(:account) status = Fabricate(:status) members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } - Redis.current.zadd("feed:type:#{account.id}", members) - - FeedManager.instance.push('type', account, status) - - expect(Redis.current.zcard("feed:type:#{account.id}")).to eq FeedManager::MAX_ITEMS - end - - it 'sends push updates for non-home timelines' do - account = Fabricate(:account) - status = Fabricate(:status) - allow(Redis.current).to receive_messages(publish: nil) + Redis.current.zadd("feed:home:#{account.id}", members) - FeedManager.instance.push('type', account, status) + FeedManager.instance.push_to_home(account, status) - expect(Redis.current).to have_received(:publish).with("timeline:#{account.id}", any_args).at_least(:once) + expect(Redis.current.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS end context 'reblogs' do @@ -171,7 +220,7 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - expect(FeedManager.instance.push('type', account, reblog)).to be true + expect(FeedManager.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recent status' do @@ -179,9 +228,9 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push('type', account, reblogged) + FeedManager.instance.push_to_home(account, reblogged) - expect(FeedManager.instance.push('type', account, reblog)).to be false + expect(FeedManager.instance.push_to_home(account, reblog)).to be false end it 'saves a new reblog of an old status' do @@ -189,14 +238,14 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) reblog = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push('type', account, reblogged) + FeedManager.instance.push_to_home(account, reblogged) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do - FeedManager.instance.push('type', account, Fabricate(:status)) + FeedManager.instance.push_to_home(account, Fabricate(:status)) end - expect(FeedManager.instance.push('type', account, reblog)).to be true + expect(FeedManager.instance.push_to_home(account, reblog)).to be true end it 'does not save a new reblog of a recently-reblogged status' do @@ -205,10 +254,10 @@ RSpec.describe FeedManager do reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted - FeedManager.instance.push('type', account, reblogs.first) + FeedManager.instance.push_to_home(account, reblogs.first) # The second reblog should be ignored - expect(FeedManager.instance.push('type', account, reblogs.last)).to be false + expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false end it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do @@ -217,14 +266,14 @@ RSpec.describe FeedManager do reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } # Accept the reblogs - FeedManager.instance.push('type', account, reblogs[0]) - FeedManager.instance.push('type', account, reblogs[1]) + FeedManager.instance.push_to_home(account, reblogs[0]) + FeedManager.instance.push_to_home(account, reblogs[1]) # Unreblog the first one - FeedManager.instance.unpush('type', account, reblogs[0]) + FeedManager.instance.unpush_from_home(account, reblogs[0]) # The last reblog should still be ignored - expect(FeedManager.instance.push('type', account, reblogs.last)).to be false + expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false end it 'saves a new reblog of a long-ago-reblogged status' do @@ -233,15 +282,15 @@ RSpec.describe FeedManager do reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } # The first reblog will be accepted - FeedManager.instance.push('type', account, reblogs.first) + FeedManager.instance.push_to_home(account, reblogs.first) # Fill the feed with intervening statuses FeedManager::REBLOG_FALLOFF.times do - FeedManager.instance.push('type', account, Fabricate(:status)) + FeedManager.instance.push_to_home(account, Fabricate(:status)) end # The second reblog should also be accepted - expect(FeedManager.instance.push('type', account, reblogs.last)).to be true + expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be true end end end @@ -253,11 +302,11 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) another_status = Fabricate(:status, reblog: reblogged) - reblogs_key = FeedManager.instance.key('type', receiver.id, 'reblogs') - reblog_set_key = FeedManager.instance.key('type', receiver.id, "reblogs:#{reblogged.id}") + reblogs_key = FeedManager.instance.key('home', receiver.id, 'reblogs') + reblog_set_key = FeedManager.instance.key('home', receiver.id, "reblogs:#{reblogged.id}") - FeedManager.instance.push('type', receiver, status) - FeedManager.instance.push('type', receiver, another_status) + FeedManager.instance.push_to_home(receiver, status) + FeedManager.instance.push_to_home(receiver, another_status) # We should have a tracking set and an entry in reblogs. expect(Redis.current.exists(reblog_set_key)).to be true @@ -265,12 +314,12 @@ RSpec.describe FeedManager do # Push everything off the end of the feed. FeedManager::MAX_ITEMS.times do - FeedManager.instance.push('type', receiver, Fabricate(:status)) + FeedManager.instance.push_to_home(receiver, Fabricate(:status)) end # `trim` should be called automatically, but do it anyway, as # we're testing `trim`, not side effects of `push`. - FeedManager.instance.trim('type', receiver.id) + FeedManager.instance.trim('home', receiver.id) # We should not have any reblog tracking data. expect(Redis.current.exists(reblog_set_key)).to be false @@ -285,32 +334,32 @@ RSpec.describe FeedManager do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push('type', receiver, reblogged) - FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push('type', receiver, Fabricate(:status)) } - FeedManager.instance.push('type', receiver, status) + FeedManager.instance.push_to_home(receiver, reblogged) + FeedManager::REBLOG_FALLOFF.times { FeedManager.instance.push_to_home(receiver, Fabricate(:status)) } + FeedManager.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(status.id.to_s) + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) - FeedManager.instance.unpush('type', receiver, status) + FeedManager.instance.unpush_from_home(receiver, status) # Restore original status - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to include(reblogged.id.to_s) end it 'removes a reblogged status if it was only reblogged once' do reblogged = Fabricate(:status) status = Fabricate(:status, reblog: reblogged) - FeedManager.instance.push('type', receiver, status) + FeedManager.instance.push_to_home(receiver, status) # The reblogging status should show up under normal conditions. - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [status.id.to_s] + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [status.id.to_s] - FeedManager.instance.unpush('type', receiver, status) + FeedManager.instance.unpush_from_home(receiver, status) - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to be_empty + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to be_empty end it 'leaves a multiply-reblogged status if another reblog was in feed' do @@ -318,26 +367,26 @@ RSpec.describe FeedManager do reblogs = 3.times.map { Fabricate(:status, reblog: reblogged) } reblogs.each do |reblog| - FeedManager.instance.push('type', receiver, reblog) + FeedManager.instance.push_to_home(receiver, reblog) end # The reblogging status should show up under normal conditions. - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.first.id.to_s] reblogs[0...-1].each do |reblog| - FeedManager.instance.unpush('type', receiver, reblog) + FeedManager.instance.unpush_from_home(receiver, reblog) end - expect(Redis.current.zrange("feed:type:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] + expect(Redis.current.zrange("feed:home:#{receiver.id}", 0, -1)).to eq [reblogs.last.id.to_s] end it 'sends push updates' do status = Fabricate(:status) - FeedManager.instance.push('type', receiver, status) + FeedManager.instance.push_to_home(receiver, status) allow(Redis.current).to receive_messages(publish: nil) - FeedManager.instance.unpush('type', receiver, status) + FeedManager.instance.unpush_from_home(receiver, status) deletion = Oj.dump(event: :delete, payload: status.id.to_s) expect(Redis.current).to have_received(:publish).with("timeline:#{receiver.id}", deletion) diff --git a/spec/lib/settings/scoped_settings_spec.rb b/spec/lib/settings/scoped_settings_spec.rb new file mode 100644 index 000000000..7566685b4 --- /dev/null +++ b/spec/lib/settings/scoped_settings_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Settings::ScopedSettings do + let(:object) { Fabricate(:user) } + let(:scoped_setting) { described_class.new(object) } + let(:val) { 'whatever' } + let(:methods) { %i(auto_play_gif default_sensitive unfollow_modal boost_modal delete_modal reduce_motion system_font_ui noindex theme) } + + describe '.initialize' do + it 'sets @object' do + scoped_setting = described_class.new(object) + expect(scoped_setting.instance_variable_get(:@object)).to be object + end + end + + describe '#method_missing' do + it 'sets scoped_setting.method_name = val' do + methods.each do |key| + scoped_setting.send("#{key}=", val) + expect(scoped_setting.send(key)).to eq val + end + end + end + + describe '#[]= and #[]' do + it 'sets [key] = val' do + methods.each do |key| + scoped_setting[key] = val + expect(scoped_setting[key]).to eq val + end + end + end +end diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index 6fbf6536b..fee875373 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -62,7 +62,7 @@ describe UserSettingsDecorator do settings.update(values) expect(user.settings['auto_play_gif']).to eq false end - + it 'updates the user settings value for system font in UI' do values = { 'setting_system_font_ui' => '0' } diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb index c4be8c4af..16983b2e3 100644 --- a/spec/models/account_moderation_note_spec.rb +++ b/spec/models/account_moderation_note_spec.rb @@ -1,5 +1,5 @@ require 'rails_helper' RSpec.describe AccountModerationNote, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index aef0c3082..7501c498c 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -93,21 +93,44 @@ RSpec.describe Account, type: :model do end describe '#save_with_optional_media!' do - it 'sets default avatar, header, avatar_remote_url, and header_remote_url if some of them are invalid' do + before do stub_request(:get, 'https://remote/valid_avatar').to_return(request_fixture('avatar.txt')) stub_request(:get, 'https://remote/invalid_avatar').to_return(request_fixture('feed.txt')) - account = Fabricate(:account, - avatar_remote_url: 'https://remote/valid_avatar', - header_remote_url: 'https://remote/valid_avatar') + end + + let(:account) do + Fabricate(:account, + avatar_remote_url: 'https://remote/valid_avatar', + header_remote_url: 'https://remote/valid_avatar') + end + + let!(:expectation) { account.dup } + + context 'with valid properties' do + before do + account.save_with_optional_media! + end + + it 'unchanges avatar, header, avatar_remote_url, and header_remote_url' do + expect(account.avatar_remote_url).to eq expectation.avatar_remote_url + expect(account.header_remote_url).to eq expectation.header_remote_url + expect(account.avatar_file_name).to eq expectation.avatar_file_name + expect(account.header_file_name).to eq expectation.header_file_name + end + end - account.avatar_remote_url = 'https://remote/invalid_avatar' - account.save_with_optional_media! + context 'with invalid properties' do + before do + account.avatar_remote_url = 'https://remote/invalid_avatar' + account.save_with_optional_media! + end - account.reload - expect(account.avatar_remote_url).to eq '' - expect(account.header_remote_url).to eq '' - expect(account.avatar_file_name).to eq nil - expect(account.header_file_name).to eq nil + it 'sets default avatar, header, avatar_remote_url, and header_remote_url' do + expect(account.avatar_remote_url).to eq '' + expect(account.header_remote_url).to eq '' + expect(account.avatar_file_name).to eq nil + expect(account.header_file_name).to eq nil + end end end @@ -123,6 +146,61 @@ RSpec.describe Account, type: :model do end end + describe '#possibly_stale?' do + let(:account) { Fabricate(:account, last_webfingered_at: last_webfingered_at) } + + context 'last_webfingered_at is nil' do + let(:last_webfingered_at) { nil } + + it 'returns true' do + expect(account.possibly_stale?).to be true + end + end + + context 'last_webfingered_at is more than 24 hours before' do + let(:last_webfingered_at) { 25.hours.ago } + + it 'returns true' do + expect(account.possibly_stale?).to be true + end + end + + context 'last_webfingered_at is less than 24 hours before' do + let(:last_webfingered_at) { 23.hours.ago } + + it 'returns false' do + expect(account.possibly_stale?).to be false + end + end + end + + describe '#refresh!' do + let(:account) { Fabricate(:account, domain: domain) } + let(:acct) { account.acct } + + context 'domain is nil' do + let(:domain) { nil } + + it 'returns nil' do + expect(account.refresh!).to be_nil + end + + it 'calls not ResolveRemoteAccountService#call' do + expect_any_instance_of(ResolveRemoteAccountService).not_to receive(:call).with(acct) + account.refresh! + end + end + + context 'domain is present' do + let(:domain) { 'example.com' } + + it 'calls ResolveRemoteAccountService#call' do + expect_any_instance_of(ResolveRemoteAccountService).to receive(:call).with(acct).once + account.refresh! + end + end + end + describe '#to_param' do it 'returns username' do account = Fabricate(:account, username: 'alice') @@ -558,8 +636,8 @@ RSpec.describe Account, type: :model do expect(account).to model_have_error_on_field(:display_name) end - it 'is invalid if the note is longer than 160 characters' do - account = Fabricate.build(:account, note: Faker::Lorem.characters(161)) + it 'is invalid if the note is longer than 500 characters' do + account = Fabricate.build(:account, note: Faker::Lorem.characters(501)) account.valid? expect(account).to model_have_error_on_field(:note) end @@ -598,8 +676,8 @@ RSpec.describe Account, type: :model do expect(account).not_to model_have_error_on_field(:display_name) end - it 'is valid even if the note is longer than 160 characters' do - account = Fabricate.build(:account, domain: 'domain', note: Faker::Lorem.characters(161)) + it 'is valid even if the note is longer than 500 characters' do + account = Fabricate.build(:account, domain: 'domain', note: Faker::Lorem.characters(501)) account.valid? expect(account).not_to model_have_error_on_field(:note) end diff --git a/spec/models/concerns/account_interactions_spec.rb b/spec/models/concerns/account_interactions_spec.rb new file mode 100644 index 000000000..1e238e27c --- /dev/null +++ b/spec/models/concerns/account_interactions_spec.rb @@ -0,0 +1,75 @@ +require 'rails_helper' + +describe AccountInteractions do + describe 'muting an account' do + let(:me) { Fabricate(:account, username: 'Me') } + let(:you) { Fabricate(:account, username: 'You') } + + context 'with the notifications option unspecified' do + before do + me.mute!(you) + end + + it 'defaults to muting notifications' do + expect(me.muting_notifications?(you)).to be true + end + end + + context 'with the notifications option set to false' do + before do + me.mute!(you, notifications: false) + end + + it 'does not mute notifications' do + expect(me.muting_notifications?(you)).to be false + end + end + + context 'with the notifications option set to true' do + before do + me.mute!(you, notifications: true) + end + + it 'does mute notifications' do + expect(me.muting_notifications?(you)).to be true + end + end + end + + describe 'ignoring reblogs from an account' do + before do + @me = Fabricate(:account, username: 'Me') + @you = Fabricate(:account, username: 'You') + end + + context 'with the reblogs option unspecified' do + before do + @me.follow!(@you) + end + + it 'defaults to showing reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + + context 'with the reblogs option set to false' do + before do + @me.follow!(@you, reblogs: false) + end + + it 'does mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(true) + end + end + + context 'with the reblogs option set to true' do + before do + @me.follow!(@you, reblogs: true) + end + + it 'does not mute reblogs' do + expect(@me.muting_reblogs?(@you)).to be(false) + end + end + end +end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index cb51e9519..bb150b837 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -1,6 +1,35 @@ require 'rails_helper' RSpec.describe CustomEmoji, type: :model do + describe '#local?' do + let(:custom_emoji) { Fabricate(:custom_emoji, domain: domain) } + + subject { custom_emoji.local? } + + context 'domain is nil' do + let(:domain) { nil } + + it 'returns true' do + is_expected.to be true + end + end + + context 'domain is present' do + let(:domain) { 'example.com' } + + it 'returns false' do + is_expected.to be false + end + end + end + + describe '#object_type' do + it 'returns :emoji' do + custom_emoji = Fabricate(:custom_emoji) + expect(custom_emoji.object_type).to be :emoji + end + end + describe '.from_text' do let!(:emojo) { Fabricate(:custom_emoji) } diff --git a/spec/models/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb index 5f5d189d9..efd2853a9 100644 --- a/spec/models/email_domain_block_spec.rb +++ b/spec/models/email_domain_block_spec.rb @@ -13,9 +13,10 @@ RSpec.describe EmailDomainBlock, type: :model do Fabricate(:email_domain_block, domain: 'example.com') expect(EmailDomainBlock.block?('nyarn@example.com')).to eq true end + it 'returns true if the domain is not registed' do - Fabricate(:email_domain_block, domain: 'domain') - expect(EmailDomainBlock.block?('example')).to eq false + Fabricate(:email_domain_block, domain: 'example.com') + expect(EmailDomainBlock.block?('nyarn@example.net')).to eq false end end end diff --git a/spec/models/follow_request_spec.rb b/spec/models/follow_request_spec.rb index cc6f8ee62..7bc93a2aa 100644 --- a/spec/models/follow_request_spec.rb +++ b/spec/models/follow_request_spec.rb @@ -1,25 +1,37 @@ require 'rails_helper' RSpec.describe FollowRequest, type: :model do - describe '#authorize!' - describe '#reject!' + describe '#authorize!' do + let(:follow_request) { Fabricate(:follow_request, account: account, target_account: target_account) } + let(:account) { Fabricate(:account) } + let(:target_account) { Fabricate(:account) } - describe 'validations' do - it 'has a valid fabricator' do - follow_request = Fabricate.build(:follow_request) - expect(follow_request).to be_valid + it 'calls Account#follow!, MergeWorker.perform_async, and #destroy!' do + expect(account).to receive(:follow!).with(target_account, reblogs: true) + expect(MergeWorker).to receive(:perform_async).with(target_account.id, account.id) + expect(follow_request).to receive(:destroy!) + follow_request.authorize! end - it 'is invalid without an account' do - follow_request = Fabricate.build(:follow_request, account: nil) - follow_request.valid? - expect(follow_request).to model_have_error_on_field(:account) + it 'generates a Follow' do + follow_request = Fabricate.create(:follow_request) + follow_request.authorize! + target = follow_request.target_account + expect(follow_request.account.following?(target)).to be true end - it 'is invalid without a target account' do - follow_request = Fabricate.build(:follow_request, target_account: nil) - follow_request.valid? - expect(follow_request).to model_have_error_on_field(:target_account) + it 'correctly passes show_reblogs when true' do + follow_request = Fabricate.create(:follow_request, show_reblogs: true) + follow_request.authorize! + target = follow_request.target_account + expect(follow_request.account.muting_reblogs?(target)).to be false + end + + it 'correctly passes show_reblogs when false' do + follow_request = Fabricate.create(:follow_request, show_reblogs: false) + follow_request.authorize! + target = follow_request.target_account + expect(follow_request.account.muting_reblogs?(target)).to be true end end end diff --git a/spec/models/glitch/keyword_mute_spec.rb b/spec/models/glitch/keyword_mute_spec.rb new file mode 100644 index 000000000..9685c6493 --- /dev/null +++ b/spec/models/glitch/keyword_mute_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +RSpec.describe Glitch::KeywordMute, type: :model do + let(:alice) { Fabricate(:account, username: 'alice').tap(&:save!) } + let(:bob) { Fabricate(:account, username: 'bob').tap(&:save!) } + + describe '.matcher_for' do + let(:matcher) { Glitch::KeywordMute.matcher_for(alice) } + + describe 'with no mutes' do + before do + Glitch::KeywordMute.delete_all + end + + it 'does not match' do + expect(matcher =~ 'This is a hot take').to be_falsy + end + end + + describe 'with mutes' do + it 'does not match keywords set by a different account' do + Glitch::KeywordMute.create!(account: bob, keyword: 'take') + + expect(matcher =~ 'This is a hot take').to be_falsy + end + + it 'does not match if no keywords match the status text' do + Glitch::KeywordMute.create!(account: alice, keyword: 'cold') + + expect(matcher =~ 'This is a hot take').to be_falsy + end + + it 'considers word boundaries when matching' do + Glitch::KeywordMute.create!(account: alice, keyword: 'bob', whole_word: true) + + expect(matcher =~ 'bobcats').to be_falsy + end + + it 'matches substrings if whole_word is false' do + Glitch::KeywordMute.create!(account: alice, keyword: 'take', whole_word: false) + + expect(matcher =~ 'This is a shiitake mushroom').to be_truthy + end + + it 'matches keywords at the beginning of the text' do + Glitch::KeywordMute.create!(account: alice, keyword: 'take') + + expect(matcher =~ 'Take this').to be_truthy + end + + it 'matches keywords at the end of the text' do + Glitch::KeywordMute.create!(account: alice, keyword: 'take') + + expect(matcher =~ 'This is a hot take').to be_truthy + end + + it 'matches if at least one keyword case-insensitively matches the text' do + Glitch::KeywordMute.create!(account: alice, keyword: 'hot') + + expect(matcher =~ 'This is a HOT take').to be_truthy + end + + it 'maintains case-insensitivity when combining keywords into a single matcher' do + Glitch::KeywordMute.create!(account: alice, keyword: 'hot') + Glitch::KeywordMute.create!(account: alice, keyword: 'cold') + + expect(matcher =~ 'This is a HOT take').to be_truthy + end + + it 'matches keywords surrounded by non-alphanumeric ornamentation' do + Glitch::KeywordMute.create!(account: alice, keyword: 'hot') + + expect(matcher =~ '(hot take)').to be_truthy + end + + it 'escapes metacharacters in keywords' do + Glitch::KeywordMute.create!(account: alice, keyword: '(hot take)') + + expect(matcher =~ '(hot take)').to be_truthy + end + + it 'uses case-folding rules appropriate for more than just English' do + Glitch::KeywordMute.create!(account: alice, keyword: 'großeltern') + + expect(matcher =~ 'besuch der grosseltern').to be_truthy + end + + it 'matches keywords that are composed of multiple words' do + Glitch::KeywordMute.create!(account: alice, keyword: 'a shiitake') + + expect(matcher =~ 'This is a shiitake').to be_truthy + expect(matcher =~ 'This is shiitake').to_not be_truthy + end + end + end +end diff --git a/spec/models/feed_spec.rb b/spec/models/home_feed_spec.rb index 8719369db..3acb997f1 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/home_feed_spec.rb @@ -1,9 +1,9 @@ require 'rails_helper' -RSpec.describe Feed, type: :model do +RSpec.describe HomeFeed, type: :model do let(:account) { Fabricate(:account) } - subject { described_class.new(:home, account) } + subject { described_class.new(account) } describe '#get' do before do diff --git a/spec/models/list_account_spec.rb b/spec/models/list_account_spec.rb new file mode 100644 index 000000000..a132e09b0 --- /dev/null +++ b/spec/models/list_account_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ListAccount, type: :model do + +end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb new file mode 100644 index 000000000..c302482b4 --- /dev/null +++ b/spec/models/list_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe List, type: :model do + +end diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 9fce5bc4f..b40a641f7 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -1,6 +1,83 @@ require 'rails_helper' RSpec.describe MediaAttachment, type: :model do + describe 'local?' do + let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url) } + + subject { media_attachment.local? } + + context 'remote_url is blank' do + let(:remote_url) { '' } + + it 'returns true' do + is_expected.to be true + end + end + + context 'remote_url is present' do + let(:remote_url) { 'remote_url' } + + it 'returns false' do + is_expected.to be false + end + end + end + + describe 'needs_redownload?' do + let(:media_attachment) { Fabricate(:media_attachment, remote_url: remote_url, file: file) } + + subject { media_attachment.needs_redownload? } + + context 'file is blank' do + let(:file) { nil } + + context 'remote_url is blank' do + let(:remote_url) { '' } + + it 'returns false' do + is_expected.to be false + end + end + + context 'remote_url is present' do + let(:remote_url) { 'remote_url' } + + it 'returns true' do + is_expected.to be true + end + end + end + + context 'file is present' do + let(:file) { attachment_fixture('avatar.gif') } + + context 'remote_url is blank' do + let(:remote_url) { '' } + + it 'returns false' do + is_expected.to be false + end + end + + context 'remote_url is present' do + let(:remote_url) { 'remote_url' } + + it 'returns true' do + is_expected.to be false + end + end + end + end + + describe '#to_param' do + let(:media_attachment) { Fabricate(:media_attachment) } + let(:shortcode) { media_attachment.shortcode } + + it 'returns shortcode' do + expect(media_attachment.to_param).to eq shortcode + end + end + describe 'animated gif conversion' do let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('avatar.gif')) } @@ -20,20 +97,29 @@ RSpec.describe MediaAttachment, type: :model do end describe 'non-animated gif non-conversion' do - let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture('attachment.gif')) } + fixtures = [ + { filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 }, + { filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 }, + ] - it 'sets type to image' do - expect(media.type).to eq 'image' - end + fixtures.each do |fixture| + context fixture[:filename] do + let(:media) { MediaAttachment.create(account: Fabricate(:account), file: attachment_fixture(fixture[:filename])) } - it 'leaves original file as-is' do - expect(media.file_content_type).to eq 'image/gif' - end + it 'sets type to image' do + expect(media.type).to eq 'image' + end - it 'sets meta' do - expect(media.file.meta["original"]["width"]).to eq 600 - expect(media.file.meta["original"]["height"]).to eq 400 - expect(media.file.meta["original"]["aspect"]).to eq 1.5 + it 'leaves original file as-is' do + expect(media.file_content_type).to eq 'image/gif' + end + + it 'sets meta' do + expect(media.file.meta["original"]["width"]).to eq fixture[:width] + expect(media.file.meta["original"]["height"]).to eq fixture[:height] + expect(media.file.meta["original"]["aspect"]).to eq fixture[:aspect] + end + end end end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 97e8095cd..763b1523f 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -5,6 +5,74 @@ RSpec.describe Notification, type: :model do pending end + describe '#target_status' do + before do + allow(notification).to receive(:type).and_return(type) + allow(notification).to receive(:activity).and_return(activity) + end + + let(:notification) { Fabricate(:notification) } + let(:status) { instance_double('Status') } + let(:favourite) { instance_double('Favourite') } + let(:mention) { instance_double('Mention') } + + context 'type is :reblog' do + let(:type) { :reblog } + let(:activity) { status } + + it 'calls activity.reblog' do + expect(activity).to receive(:reblog) + notification.target_status + end + end + + context 'type is :favourite' do + let(:type) { :favourite } + let(:activity) { favourite } + + it 'calls activity.status' do + expect(activity).to receive(:status) + notification.target_status + end + end + + context 'type is :mention' do + let(:type) { :mention } + let(:activity) { mention } + + it 'calls activity.status' do + expect(activity).to receive(:status) + notification.target_status + end + end + end + + describe '#browserable?' do + let(:notification) { Fabricate(:notification) } + + subject { notification.browserable? } + + context 'type is :follow_request' do + before do + allow(notification).to receive(:type).and_return(:follow_request) + end + + it 'returns false' do + is_expected.to be false + end + end + + context 'type is not :follow_request' do + before do + allow(notification).to receive(:type).and_return(:else) + end + + it 'returns true' do + is_expected.to be true + end + end + end + describe '#type' do it 'returns :reblog for a Status' do notification = Notification.new(activity: Status.new) @@ -26,4 +94,49 @@ RSpec.describe Notification, type: :model do expect(notification.type).to eq :follow end end + + describe '.reload_stale_associations!' do + context 'account_ids are empty' do + let(:cached_items) { [] } + + subject { described_class.reload_stale_associations!(cached_items) } + + it 'returns nil' do + is_expected.to be nil + end + end + + context 'account_ids are present' do + before do + allow(accounts_with_ids).to receive(:[]).with(stale_account1.id).and_return(account1) + allow(accounts_with_ids).to receive(:[]).with(stale_account2.id).and_return(account2) + allow(Account).to receive_message_chain(:where, :map, :to_h).and_return(accounts_with_ids) + end + + let(:cached_items) do + [ + Fabricate(:notification, activity: Fabricate(:status)), + Fabricate(:notification, activity: Fabricate(:follow)), + ] + end + + let(:stale_account1) { cached_items[0].from_account } + let(:stale_account2) { cached_items[1].from_account } + + let(:account1) { Fabricate(:account) } + let(:account2) { Fabricate(:account) } + + let(:accounts_with_ids) { { account1.id => account1, account2.id => account2 } } + + it 'reloads associations' do + expect(cached_items[0].from_account).to be stale_account1 + expect(cached_items[1].from_account).to be stale_account2 + + described_class.reload_stale_associations!(cached_items) + + expect(cached_items[0].from_account).to be account1 + expect(cached_items[1].from_account).to be account2 + end + end + end end diff --git a/spec/models/remote_follow_spec.rb b/spec/models/remote_follow_spec.rb new file mode 100644 index 000000000..72c580f9f --- /dev/null +++ b/spec/models/remote_follow_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RemoteFollow do + before do + stub_request(:get, 'https://quitter.no/.well-known/webfinger?resource=acct:gargron@quitter.no').to_return(request_fixture('webfinger.txt')) + end + + let(:attrs) { nil } + let(:remote_follow) { described_class.new(attrs) } + + describe '.initialize' do + subject { remote_follow.acct } + + context 'attrs with acct' do + let(:attrs) { { acct: 'gargron@quitter.no' } } + + it 'returns acct' do + is_expected.to eq 'gargron@quitter.no' + end + end + + context 'attrs without acct' do + let(:attrs) { {} } + + it do + is_expected.to be_nil + end + end + end + + describe '#valid?' do + subject { remote_follow.valid? } + + context 'attrs with acct' do + let(:attrs) { { acct: 'gargron@quitter.no' }} + + it do + is_expected.to be true + end + end + + context 'attrs without acct' do + let(:attrs) { { } } + + it do + is_expected.to be false + end + end + end + + describe '#subscribe_address_for' do + before do + remote_follow.valid? + end + + let(:attrs) { { acct: 'gargron@quitter.no' } } + let(:account) { Fabricate(:account, username: 'alice') } + + subject { remote_follow.subscribe_address_for(account) } + + it 'returns subscribe address' do + is_expected.to eq 'https://quitter.no/main/ostatussub?profile=alice%40cb6e6126.ngrok.io' + end + end +end diff --git a/spec/models/remote_profile_spec.rb b/spec/models/remote_profile_spec.rb new file mode 100644 index 000000000..da5048f0a --- /dev/null +++ b/spec/models/remote_profile_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe RemoteProfile do + let(:remote_profile) { RemoteProfile.new(body) } + let(:body) do + <<-XML + <feed xmlns="http://www.w3.org/2005/Atom"> + <author>John</author> + XML + end + + describe '.initialize' do + it 'calls Nokogiri::XML.parse' do + expect(Nokogiri::XML).to receive(:parse).with(body, nil, 'utf-8') + RemoteProfile.new(body) + end + + it 'sets document' do + remote_profile = RemoteProfile.new(body) + expect(remote_profile).not_to be nil + end + end + + describe '#root' do + let(:document) { remote_profile.document } + + it 'callse document.at_xpath' do + expect(document).to receive(:at_xpath).with( + '/atom:feed|/atom:entry', + atom: OStatus::TagManager::XMLNS + ) + + remote_profile.root + end + end + + describe '#author' do + let(:root) { remote_profile.root } + + it 'calls root.at_xpath' do + expect(root).to receive(:at_xpath).with( + './atom:author|./dfrn:owner', + atom: OStatus::TagManager::XMLNS, + dfrn: OStatus::TagManager::DFRN_XMLNS + ) + + remote_profile.author + end + end + + describe '#hub_link' do + let(:root) { remote_profile.root } + + it 'calls #link_href_from_xml' do + expect(remote_profile).to receive(:link_href_from_xml).with(root, 'hub') + remote_profile.hub_link + end + end + + describe '#display_name' do + let(:author) { remote_profile.author } + + it 'calls author.at_xpath.content' do + expect(author).to receive_message_chain(:at_xpath, :content).with( + './poco:displayName', + poco: OStatus::TagManager::POCO_XMLNS + ).with(no_args) + + remote_profile.display_name + end + end + + describe '#note' do + let(:author) { remote_profile.author } + + it 'calls author.at_xpath.content' do + expect(author).to receive_message_chain(:at_xpath, :content).with( + './atom:summary|./poco:note', + atom: OStatus::TagManager::XMLNS, + poco: OStatus::TagManager::POCO_XMLNS + ).with(no_args) + + remote_profile.note + end + end + + describe '#scope' do + let(:author) { remote_profile.author } + + it 'calls author.at_xpath.content' do + expect(author).to receive_message_chain(:at_xpath, :content).with( + './mastodon:scope', + mastodon: OStatus::TagManager::MTDN_XMLNS + ).with(no_args) + + remote_profile.scope + end + end + + describe '#avatar' do + let(:author) { remote_profile.author } + + it 'calls #link_href_from_xml' do + expect(remote_profile).to receive(:link_href_from_xml).with(author, 'avatar') + remote_profile.avatar + end + end + + describe '#header' do + let(:author) { remote_profile.author } + + it 'calls #link_href_from_xml' do + expect(remote_profile).to receive(:link_href_from_xml).with(author, 'header') + remote_profile.header + end + end + + describe '#locked?' do + before do + allow(remote_profile).to receive(:scope).and_return(scope) + end + + subject { remote_profile.locked? } + + context 'scope is private' do + let(:scope) { 'private' } + + it 'returns true' do + is_expected.to be true + end + end + + context 'scope is not private' do + let(:scope) { 'public' } + + it 'returns false' do + is_expected.to be false + end + end + end +end diff --git a/spec/models/session_activation_spec.rb b/spec/models/session_activation_spec.rb index 49c72fbd4..2aa695037 100644 --- a/spec/models/session_activation_spec.rb +++ b/spec/models/session_activation_spec.rb @@ -1,5 +1,127 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe SessionActivation, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + describe '#detection' do + let(:session_activation) { Fabricate(:session_activation, user_agent: 'Chrome/62.0.3202.89') } + + it 'sets a Browser instance as detection' do + expect(session_activation.detection).to be_kind_of Browser::Chrome + end + end + + describe '#browser' do + before do + allow(session_activation).to receive(:detection).and_return(detection) + end + + let(:detection) { double(id: 1) } + let(:session_activation) { Fabricate(:session_activation) } + + it 'returns detection.id' do + expect(session_activation.browser).to be 1 + end + end + + describe '#platform' do + before do + allow(session_activation).to receive(:detection).and_return(detection) + end + + let(:session_activation) { Fabricate(:session_activation) } + let(:detection) { double(platform: double(id: 1)) } + + it 'returns detection.platform.id' do + expect(session_activation.platform).to be 1 + end + end + + describe '.active?' do + subject { described_class.active?(id) } + + context 'id is absent' do + let(:id) { nil } + + it 'returns nil' do + is_expected.to be nil + end + end + + context 'id is present' do + let(:id) { '1' } + let!(:session_activation) { Fabricate(:session_activation, session_id: id) } + + context 'id exists as session_id' do + it 'returns true' do + is_expected.to be true + end + end + + context 'id does not exist as session_id' do + before do + session_activation.update!(session_id: '2') + end + + it 'returns false' do + is_expected.to be false + end + end + end + end + + describe '.activate' do + let(:options) { { user: Fabricate(:user), session_id: '1' } } + + it 'calls create! and purge_old' do + expect(described_class).to receive(:create!).with(options) + expect(described_class).to receive(:purge_old) + described_class.activate(options) + end + + it 'returns an instance of SessionActivation' do + expect(described_class.activate(options)).to be_kind_of SessionActivation + end + end + + describe '.deactivate' do + context 'id is absent' do + let(:id) { nil } + + it 'returns nil' do + expect(described_class.deactivate(id)).to be nil + end + end + + context 'id exists' do + let(:id) { '1' } + + it 'calls where.destroy_all' do + expect(described_class).to receive_message_chain(:where, :destroy_all) + .with(session_id: id).with(no_args) + + described_class.deactivate(id) + end + end + end + + describe '.purge_old' do + it 'calls order.offset.destroy_all' do + expect(described_class).to receive_message_chain(:order, :offset, :destroy_all) + .with('created_at desc').with(Rails.configuration.x.max_session_activations).with(no_args) + + described_class.purge_old + end + end + + describe '.exclusive' do + let(:id) { '1' } + + it 'calls where.destroy_all' do + expect(described_class).to receive_message_chain(:where, :destroy_all) + .with('session_id != ?', id).with(no_args) + + described_class.exclusive(id) + end + end end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb new file mode 100644 index 000000000..e99dfc0d7 --- /dev/null +++ b/spec/models/setting_spec.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Setting, type: :model do + describe '#to_param' do + let(:setting) { Fabricate(:setting, var: var) } + let(:var) { 'var' } + + it 'returns setting.var' do + expect(setting.to_param).to eq var + end + end + + describe '.[]' do + before do + allow(described_class).to receive(:rails_initialized?).and_return(rails_initialized) + end + + let(:key) { 'key' } + + context 'rails_initialized? is falsey' do + let(:rails_initialized) { false } + + it 'calls RailsSettings::Base#[]' do + expect(RailsSettings::Base).to receive(:[]).with(key) + described_class[key] + end + end + + context 'rails_initialized? is truthy' do + before do + allow(RailsSettings::Base).to receive(:cache_key).with(key, nil).and_return(cache_key) + end + + let(:rails_initialized) { true } + let(:cache_key) { 'cache-key' } + let(:cache_value) { 'cache-value' } + + it 'calls not RailsSettings::Base#[]' do + expect(RailsSettings::Base).not_to receive(:[]).with(key) + described_class[key] + end + + it 'calls Rails.cache.fetch' do + expect(Rails).to receive_message_chain(:cache, :fetch).with(cache_key) + described_class[key] + end + + context 'Rails.cache does not exists' do + before do + allow(RailsSettings::Settings).to receive(:object).with(key).and_return(object) + allow(described_class).to receive(:default_settings).and_return(default_settings) + allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) + Rails.cache.clear(cache_key) + end + + let(:object) { nil } + let(:default_value) { 'default_value' } + let(:default_settings) { { key => default_value } } + let(:records) { [Fabricate(:setting, var: key, value: nil)] } + + it 'calls RailsSettings::Settings.object' do + expect(RailsSettings::Settings).to receive(:object).with(key) + described_class[key] + end + + context 'RailsSettings::Settings.object returns truthy' do + let(:object) { db_val } + let(:db_val) { double(value: 'db_val') } + + context 'default_value is a Hash' do + let(:default_value) { { default_value: 'default_value' } } + + it 'calls default_value.with_indifferent_access.merge!' do + expect(default_value).to receive_message_chain(:with_indifferent_access, :merge!) + .with(db_val.value) + + described_class[key] + end + end + + context 'default_value is not a Hash' do + let(:default_value) { 'default_value' } + + it 'returns db_val.value' do + expect(described_class[key]).to be db_val.value + end + end + end + + context 'RailsSettings::Settings.object returns falsey' do + let(:object) { nil } + + it 'returns default_settings[key]' do + expect(described_class[key]).to be default_settings[key] + end + end + end + + context 'Rails.cache exists' do + before do + Rails.cache.write(cache_key, cache_value) + end + + it 'returns the cached value' do + expect(described_class[key]).to eq cache_value + end + end + end + end + + describe '.all_as_records' do + before do + allow_any_instance_of(Settings::ScopedSettings).to receive(:thing_scoped).and_return(records) + allow(described_class).to receive(:default_settings).and_return(default_settings) + end + + let(:key) { 'key' } + let(:default_value) { 'default_value' } + let(:default_settings) { { key => default_value } } + let(:original_setting) { Fabricate(:setting, var: key, value: nil) } + let(:records) { [original_setting] } + + it 'returns a Hash' do + expect(described_class.all_as_records).to be_kind_of Hash + end + + context 'records includes Setting with var as the key' do + let(:records) { [original_setting] } + + it 'includes the original Setting' do + setting = described_class.all_as_records[key] + expect(setting).to eq original_setting + end + end + + context 'records includes nothing' do + let(:records) { [] } + + context 'default_value is not a Hash' do + it 'includes Setting with value of default_value' do + setting = described_class.all_as_records[key] + + expect(setting).to be_kind_of Setting + expect(setting).to have_attributes(var: key) + expect(setting).to have_attributes(value: 'default_value') + end + end + + context 'default_value is a Hash' do + let(:default_value) { { 'foo' => 'fuga' } } + + it 'returns {}' do + expect(described_class.all_as_records).to eq({}) + end + end + end + end + + describe '.default_settings' do + before do + allow(RailsSettings::Default).to receive(:enabled?).and_return(enabled) + end + + subject { described_class.default_settings } + + context 'RailsSettings::Default.enabled? is false' do + let(:enabled) { false } + + it 'returns {}' do + is_expected.to eq({}) + end + end + + context 'RailsSettings::Settings.enabled? is true' do + let(:enabled) { true } + + it 'returns instance of RailsSettings::Default' do + is_expected.to be_kind_of RailsSettings::Default + end + end + end +end diff --git a/spec/models/site_upload_spec.rb b/spec/models/site_upload_spec.rb index 8745d54b8..f7ea06921 100644 --- a/spec/models/site_upload_spec.rb +++ b/spec/models/site_upload_spec.rb @@ -1,5 +1,13 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe SiteUpload, type: :model do + describe '#cache_key' do + let(:site_upload) { SiteUpload.new(var: 'var') } + it 'returns cache_key' do + expect(site_upload.cache_key).to eq 'site_uploads/var' + end + end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 9cb71d715..89ad3adcf 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -47,8 +47,27 @@ RSpec.describe Status, type: :model do end describe '#verb' do - it 'is always post' do - expect(subject.verb).to be :post + context 'if destroyed?' do + it 'returns :delete' do + subject.destroy! + expect(subject.verb).to be :delete + end + end + + context 'unless destroyed?' do + context 'if reblog?' do + it 'returns :share' do + subject.reblog = other + expect(subject.verb).to be :share + end + end + + context 'unless reblog?' do + it 'returns :post' do + subject.reblog = nil + expect(subject.verb).to be :post + end + end end end @@ -69,6 +88,36 @@ RSpec.describe Status, type: :model do end end + describe '#hidden?' do + context 'if private_visibility?' do + it 'returns true' do + subject.visibility = :private + expect(subject.hidden?).to be true + end + end + + context 'if direct_visibility?' do + it 'returns true' do + subject.visibility = :direct + expect(subject.hidden?).to be true + end + end + + context 'if public_visibility?' do + it 'returns false' do + subject.visibility = :public + expect(subject.hidden?).to be false + end + end + + context 'if unlisted_visibility?' do + it 'returns false' do + subject.visibility = :unlisted + expect(subject.hidden?).to be false + end + end + end + describe '#content' do it 'returns the text of the status if it is not a reblog' do expect(subject.content).to eql subject.text @@ -232,6 +281,55 @@ RSpec.describe Status, type: :model do end end + describe '.as_direct_timeline' do + let(:account) { Fabricate(:account) } + let(:followed) { Fabricate(:account) } + let(:not_followed) { Fabricate(:account) } + + before do + Fabricate(:follow, account: account, target_account: followed) + + @self_public_status = Fabricate(:status, account: account, visibility: :public) + @self_direct_status = Fabricate(:status, account: account, visibility: :direct) + @followed_public_status = Fabricate(:status, account: followed, visibility: :public) + @followed_direct_status = Fabricate(:status, account: followed, visibility: :direct) + @not_followed_direct_status = Fabricate(:status, account: not_followed, visibility: :direct) + + @results = Status.as_direct_timeline(account) + end + + it 'does not include public statuses from self' do + expect(@results).to_not include(@self_public_status) + end + + it 'includes direct statuses from self' do + expect(@results).to include(@self_direct_status) + end + + it 'does not include public statuses from followed' do + expect(@results).to_not include(@followed_public_status) + end + + it 'includes direct statuses mentioning recipient from followed' do + Fabricate(:mention, account: account, status: @followed_direct_status) + expect(@results).to include(@followed_direct_status) + end + + it 'does not include direct statuses not mentioning recipient from followed' do + expect(@results).to_not include(@followed_direct_status) + end + + it 'includes direct statuses mentioning recipient from non-followed' do + Fabricate(:mention, account: account, status: @not_followed_direct_status) + expect(@results).to include(@not_followed_direct_status) + end + + it 'does not include direct statuses not mentioning recipient from non-followed' do + expect(@results).to_not include(@not_followed_direct_status) + end + + end + describe '.as_public_timeline' do it 'only includes statuses with public visibility' do public_status = Fabricate(:status, visibility: :public) diff --git a/spec/models/stream_entry_spec.rb b/spec/models/stream_entry_spec.rb index 3b7ff5143..8f8bfbd58 100644 --- a/spec/models/stream_entry_spec.rb +++ b/spec/models/stream_entry_spec.rb @@ -6,6 +6,121 @@ RSpec.describe StreamEntry, type: :model do let(:status) { Fabricate(:status, account: alice) } let(:reblog) { Fabricate(:status, account: bob, reblog: status) } let(:reply) { Fabricate(:status, account: bob, thread: status) } + let(:stream_entry) { Fabricate(:stream_entry, activity: activity) } + let(:activity) { reblog } + + describe '#object_type' do + before do + allow(stream_entry).to receive(:orphaned?).and_return(orphaned) + allow(stream_entry).to receive(:targeted?).and_return(targeted) + end + + subject { stream_entry.object_type } + + context 'orphaned? is true' do + let(:orphaned) { true } + let(:targeted) { false } + + it 'returns :activity' do + is_expected.to be :activity + end + end + + context 'targeted? is true' do + let(:orphaned) { false } + let(:targeted) { true } + + it 'returns :activity' do + is_expected.to be :activity + end + end + + context 'orphaned? and targeted? are false' do + let(:orphaned) { false } + let(:targeted) { false } + + context 'activity is reblog' do + let(:activity) { reblog } + + it 'returns :note' do + is_expected.to be :note + end + end + + context 'activity is reply' do + let(:activity) { reply } + + it 'returns :comment' do + is_expected.to be :comment + end + end + end + end + + describe '#verb' do + before do + allow(stream_entry).to receive(:orphaned?).and_return(orphaned) + end + + subject { stream_entry.verb } + + context 'orphaned? is true' do + let(:orphaned) { true } + + it 'returns :delete' do + is_expected.to be :delete + end + end + + context 'orphaned? is false' do + let(:orphaned) { false } + + context 'activity is reblog' do + let(:activity) { reblog } + + it 'returns :share' do + is_expected.to be :share + end + end + + context 'activity is reply' do + let(:activity) { reply } + + it 'returns :post' do + is_expected.to be :post + end + end + end + end + + describe '#mentions' do + before do + allow(stream_entry).to receive(:orphaned?).and_return(orphaned) + end + + subject { stream_entry.mentions } + + context 'orphaned? is true' do + let(:orphaned) { true } + + it 'returns []' do + is_expected.to eq [] + end + end + + context 'orphaned? is false' do + before do + reblog.mentions << Fabricate(:mention, account: alice) + reblog.mentions << Fabricate(:mention, account: bob) + end + + let(:orphaned) { false } + + it 'returns [Account] includes alice and bob' do + is_expected.to eq [alice, bob] + end + end + end describe '#targeted?' do it 'returns true for a reblog' do diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index f727fa1dd..1ca50cc29 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -35,6 +35,13 @@ RSpec.describe Tag, type: :model do end end + describe '#to_param' do + it 'returns name' do + tag = Fabricate(:tag, name: 'foo') + expect(tag.to_param).to eq 'foo' + end + end + describe '.search_for' do it 'finds tag records with matching names' do tag = Fabricate(:tag, name: "match") diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 99aeca01b..77a12c26d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -177,27 +177,10 @@ RSpec.describe User, type: :model do end end - describe '#setting_auto_play_gif' do - it 'returns auto-play gif setting' do + describe 'settings' do + it 'is instance of Settings::ScopedSettings' do user = Fabricate(:user) - user.settings[:auto_play_gif] = false - expect(user.setting_auto_play_gif).to eq false - end - end - - describe '#setting_system_font_ui' do - it 'returns system font ui setting' do - user = Fabricate(:user) - user.settings[:system_font_ui] = false - expect(user.setting_system_font_ui).to eq false - end - end - - describe '#setting_boost_modal' do - it 'returns boost modal setting' do - user = Fabricate(:user) - user.settings[:boost_modal] = false - expect(user.setting_boost_modal).to eq false + expect(user.settings).to be_kind_of Settings::ScopedSettings end end @@ -219,22 +202,6 @@ RSpec.describe User, type: :model do end end - describe '#setting_unfollow_modal' do - it 'returns unfollow modal setting' do - user = Fabricate(:user) - user.settings[:unfollow_modal] = true - expect(user.setting_unfollow_modal).to eq true - end - end - - describe '#setting_delete_modal' do - it 'returns delete modal setting' do - user = Fabricate(:user) - user.settings[:delete_modal] = false - expect(user.setting_delete_modal).to eq false - end - end - describe 'whitelist' do around(:each) do |example| old_whitelist = Rails.configuration.x.email_whitelist diff --git a/spec/policies/status_policy_spec.rb b/spec/policies/status_policy_spec.rb index bacb8fd9e..a90e22aad 100644 --- a/spec/policies/status_policy_spec.rb +++ b/spec/policies/status_policy_spec.rb @@ -71,6 +71,12 @@ RSpec.describe StatusPolicy, type: :model do expect(subject).to_not permit(viewer, status) end + + it 'denies access when local-only and the viewer is not logged in' do + allow(status).to receive(:local_only?) { true } + + expect(subject).to_not permit(nil, status) + end end permissions :reblog? do diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index ebf422392..51f3fe3a1 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -27,7 +27,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do it 'creates status' do status = sender.statuses.first - + expect(status).to_not be_nil expect(status.text).to eq 'Lorem ipsum' end diff --git a/spec/services/after_block_service_spec.rb b/spec/services/after_block_service_spec.rb index 65f36c7d1..1b115c938 100644 --- a/spec/services/after_block_service_spec.rb +++ b/spec/services/after_block_service_spec.rb @@ -18,8 +18,8 @@ RSpec.describe AfterBlockService do end it "clears account's statuses" do - FeedManager.instance.push(:home, account, status) - FeedManager.instance.push(:home, account, other_account_status) + FeedManager.instance.push_to_home(account, status) + FeedManager.instance.push_to_home(account, other_account_status) is_expected.to change { Redis.current.zrange(home_timeline_key, 0, -1) diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index c82c45e09..437da2a9d 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -30,11 +30,11 @@ RSpec.describe BatchedRemoveStatusService do end it 'removes statuses from author\'s home feed' do - expect(Feed.new(:home, alice).get(10)).to_not include([status1.id, status2.id]) + expect(HomeFeed.new(alice).get(10)).to_not include([status1.id, status2.id]) end it 'removes statuses from local follower\'s home feed' do - expect(Feed.new(:home, jeff).get(10)).to_not include([status1.id, status2.id]) + expect(HomeFeed.new(jeff).get(10)).to_not include([status1.id, status2.id]) end it 'notifies streaming API of followers' do diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index 6ee225c4c..764318e34 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -19,12 +19,12 @@ RSpec.describe FanOutOnWriteService do end it 'delivers status to home timeline' do - expect(Feed.new(:home, author).get(10).map(&:id)).to include status.id + expect(HomeFeed.new(author).get(10).map(&:id)).to include status.id end it 'delivers status to local followers' do pending 'some sort of problem in test environment causes this to sometimes fail' - expect(Feed.new(:home, follower).get(10).map(&:id)).to include status.id + expect(HomeFeed.new(follower).get(10).map(&:id)).to include status.id end it 'delivers status to hashtag' do diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index ceb39e5e6..e59a2f1a6 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -13,8 +13,20 @@ RSpec.describe FollowService do subject.call(sender, bob.acct) end - it 'creates a follow request' do - expect(FollowRequest.find_by(account: sender, target_account: bob)).to_not be_nil + it 'creates a follow request with reblogs' do + expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: true)).to_not be_nil + end + end + + describe 'locked account, no reblogs' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, locked: true, username: 'bob')).account } + + before do + subject.call(sender, bob.acct, reblogs: false) + end + + it 'creates a follow request without reblogs' do + expect(FollowRequest.find_by(account: sender, target_account: bob, show_reblogs: false)).to_not be_nil end end @@ -25,8 +37,22 @@ RSpec.describe FollowService do subject.call(sender, bob.acct) end - it 'creates a following relation' do + it 'creates a following relation with reblogs' do + expect(sender.following?(bob)).to be true + expect(sender.muting_reblogs?(bob)).to be false + end + end + + describe 'unlocked account, no reblogs' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + subject.call(sender, bob.acct, reblogs: false) + end + + it 'creates a following relation without reblogs' do expect(sender.following?(bob)).to be true + expect(sender.muting_reblogs?(bob)).to be true end end @@ -42,6 +68,32 @@ RSpec.describe FollowService do expect(sender.following?(bob)).to be true end end + + describe 'already followed account, turning reblogs off' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.follow!(bob, reblogs: true) + subject.call(sender, bob.acct, reblogs: false) + end + + it 'disables reblogs' do + expect(sender.muting_reblogs?(bob)).to be true + end + end + + describe 'already followed account, turning reblogs on' do + let(:bob) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account } + + before do + sender.follow!(bob, reblogs: false) + subject.call(sender, bob.acct, reblogs: true) + end + + it 'disables reblogs' do + expect(sender.muting_reblogs?(bob)).to be false + end + end end context 'remote OStatus account' do diff --git a/spec/services/mute_service_spec.rb b/spec/services/mute_service_spec.rb index 8097cb250..2b3e3e152 100644 --- a/spec/services/mute_service_spec.rb +++ b/spec/services/mute_service_spec.rb @@ -18,8 +18,8 @@ RSpec.describe MuteService do end it "clears account's statuses" do - FeedManager.instance.push(:home, account, status) - FeedManager.instance.push(:home, account, other_account_status) + FeedManager.instance.push_to_home(account, status) + FeedManager.instance.push_to_home(account, other_account_status) is_expected.to change { Redis.current.zrange(home_timeline_key, 0, -1) @@ -32,4 +32,36 @@ RSpec.describe MuteService do account.muting?(target_account) }.from(false).to(true) end + + context 'without specifying a notifications parameter' do + it 'mutes notifications from the account' do + is_expected.to change { + account.muting_notifications?(target_account) + }.from(false).to(true) + end + end + + context 'with a true notifications parameter' do + subject do + -> { described_class.new.call(account, target_account, notifications: true) } + end + + it 'mutes notifications from the account' do + is_expected.to change { + account.muting_notifications?(target_account) + }.from(false).to(true) + end + end + + context 'with a false notifications parameter' do + subject do + -> { described_class.new.call(account, target_account, notifications: false) } + end + + it 'does not mute notifications from the account' do + is_expected.to_not change { + account.muting_notifications?(target_account) + }.from(false) + end + end end diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb index 7a66bd0fe..a8ebc16b8 100644 --- a/spec/services/notify_service_spec.rb +++ b/spec/services/notify_service_spec.rb @@ -17,6 +17,16 @@ RSpec.describe NotifyService do is_expected.to_not change(Notification, :count) end + it 'does not notify when sender is muted with hide_notifications' do + recipient.mute!(sender, notifications: true) + is_expected.to_not change(Notification, :count) + end + + it 'does notify when sender is muted without hide_notifications' do + recipient.mute!(sender, notifications: false) + is_expected.to change(Notification, :count) + end + it 'does not notify when sender\'s domain is blocked' do recipient.block_domain!(sender.domain) is_expected.to_not change(Notification, :count) @@ -38,6 +48,59 @@ RSpec.describe NotifyService do is_expected.to_not change(Notification, :count) end + describe 'reblogs' do + let(:status) { Fabricate(:status, account: Fabricate(:account)) } + let(:activity) { Fabricate(:status, account: sender, reblog: status) } + + it 'shows reblogs by default' do + recipient.follow!(sender) + is_expected.to change(Notification, :count) + end + + it 'shows reblogs when explicitly enabled' do + recipient.follow!(sender, reblogs: true) + is_expected.to change(Notification, :count) + end + + it 'hides reblogs when disabled' do + recipient.follow!(sender, reblogs: false) + is_expected.to_not change(Notification, :count) + end + end + + context 'for direct messages' do + let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct)) } + + before do + user.settings.interactions = user.settings.interactions.merge('must_be_following_dm' => enabled) + end + + context 'if recipient is supposed to be following sender' do + let(:enabled) { true } + + it 'does not notify' do + is_expected.to_not change(Notification, :count) + end + + context 'if the message chain initiated by recipient' do + let(:reply_to) { Fabricate(:status, account: recipient) } + let(:activity) { Fabricate(:mention, account: recipient, status: Fabricate(:status, account: sender, visibility: :direct, thread: reply_to)) } + + it 'does notify' do + is_expected.to change(Notification, :count) + end + end + end + + context 'if recipient is NOT supposed to be following sender' do + let(:enabled) { false } + + it 'does notify' do + is_expected.to change(Notification, :count) + end + end + end + context do let(:asshole) { Fabricate(:account, username: 'asshole') } let(:reply_to) { Fabricate(:status, account: asshole) } diff --git a/spec/services/remove_status_service_spec.rb b/spec/services/remove_status_service_spec.rb index b60015928..5bb75b820 100644 --- a/spec/services/remove_status_service_spec.rb +++ b/spec/services/remove_status_service_spec.rb @@ -25,11 +25,11 @@ RSpec.describe RemoveStatusService do end it 'removes status from author\'s home feed' do - expect(Feed.new(:home, alice).get(10)).to_not include(@status.id) + expect(HomeFeed.new(alice).get(10)).to_not include(@status.id) end it 'removes status from local follower\'s home feed' do - expect(Feed.new(:home, jeff).get(10)).to_not include(@status.id) + expect(HomeFeed.new(jeff).get(10)).to_not include(@status.id) end it 'sends PuSH update to PuSH subscribers' do diff --git a/spec/support/matchers/model/model_have_error_on_field.rb b/spec/support/matchers/model/model_have_error_on_field.rb index 5d5fe1c7b..a5dfbf457 100644 --- a/spec/support/matchers/model/model_have_error_on_field.rb +++ b/spec/support/matchers/model/model_have_error_on_field.rb @@ -9,7 +9,7 @@ RSpec::Matchers.define :model_have_error_on_field do |expected| failure_message do |record| keys = record.errors.keys - + "expect record.errors(#{keys}) to include #{expected}" end end diff --git a/spec/validators/status_length_validator_spec.rb b/spec/validators/status_length_validator_spec.rb index e2d1a15ec..9355c7e3f 100644 --- a/spec/validators/status_length_validator_spec.rb +++ b/spec/validators/status_length_validator_spec.rb @@ -7,26 +7,31 @@ describe StatusLengthValidator do it 'does not add errors onto remote statuses' it 'does not add errors onto local reblogs' - it 'adds an error when content warning is over 500 characters' do - status = double(spoiler_text: 'a' * 520, text: '', errors: double(add: nil), local?: true, reblog?: false) + it 'adds an error when content warning is over MAX_CHARS characters' do + chars = StatusLengthValidator::MAX_CHARS + 1 + status = double(spoiler_text: 'a' * chars, text: '', errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end - it 'adds an error when text is over 500 characters' do - status = double(spoiler_text: '', text: 'a' * 520, errors: double(add: nil), local?: true, reblog?: false) + it 'adds an error when text is over MAX_CHARS characters' do + chars = StatusLengthValidator::MAX_CHARS + 1 + status = double(spoiler_text: '', text: 'a' * chars, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end - it 'adds an error when text and content warning are over 500 characters total' do - status = double(spoiler_text: 'a' * 250, text: 'b' * 251, errors: double(add: nil), local?: true, reblog?: false) + it 'adds an error when text and content warning are over MAX_CHARS characters total' do + chars1 = 20 + chars2 = StatusLengthValidator::MAX_CHARS + 1 - chars1 + status = double(spoiler_text: 'a' * chars1, text: 'b' * chars2, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) expect(status.errors).to have_received(:add) end it 'counts URLs as 23 characters flat' do - text = ('a' * 476) + " http://#{'b' * 30}.com/example" + chars = StatusLengthValidator::MAX_CHARS - 1 - 23 + text = ('a' * chars) + " http://#{'b' * 30}.com/example" status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) @@ -34,7 +39,9 @@ describe StatusLengthValidator do end it 'counts only the front part of remote usernames' do - text = ('a' * 475) + " @alice@#{'b' * 30}.com" + username = '@alice' + chars = StatusLengthValidator::MAX_CHARS - 1 - username.length + text = ('a' * 475) + " #{username}@#{'b' * 30}.com" status = double(spoiler_text: '', text: text, errors: double(add: nil), local?: true, reblog?: false) subject.validate(status) diff --git a/spec/views/about/show.html.haml_spec.rb b/spec/views/about/show.html.haml_spec.rb index 724643cbc..ca59fa9e3 100644 --- a/spec/views/about/show.html.haml_spec.rb +++ b/spec/views/about/show.html.haml_spec.rb @@ -3,6 +3,8 @@ require 'rails_helper' describe 'about/show.html.haml', without_verify_partial_doubles: true do + let(:commit_hash) { '8925731c9869f55780644304e4420a1998e52607' } + before do allow(view).to receive(:site_hostname).and_return('example.com') allow(view).to receive(:site_title).and_return('example site') @@ -16,7 +18,9 @@ describe 'about/show.html.haml', without_verify_partial_doubles: true do source_url: 'https://github.com/tootsuite/mastodon', open_registrations: false, thumbnail: nil, - closed_registrations_message: 'yes') + closed_registrations_message: 'yes', + commit_hash: commit_hash) + assign(:instance_presenter, instance_presenter) render diff --git a/spec/workers/feed_insert_worker_spec.rb b/spec/workers/feed_insert_worker_spec.rb index 71a3dea00..3509f1f50 100644 --- a/spec/workers/feed_insert_worker_spec.rb +++ b/spec/workers/feed_insert_worker_spec.rb @@ -11,41 +11,41 @@ describe FeedInsertWorker do context 'when there are no records' do it 'skips push with missing status' do - instance = double(push: nil) + instance = double(push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(nil, follower.id) expect(result).to eq true - expect(instance).not_to have_received(:push) + expect(instance).not_to have_received(:push_to_home) end it 'skips push with missing account' do - instance = double(push: nil) + instance = double(push_to_home: nil) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, nil) expect(result).to eq true - expect(instance).not_to have_received(:push) + expect(instance).not_to have_received(:push_to_home) end end context 'when there are real records' do it 'skips the push when there is a filter' do - instance = double(push: nil, filter?: true) + instance = double(push_to_home: nil, filter?: true) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) expect(result).to be_nil - expect(instance).not_to have_received(:push) + expect(instance).not_to have_received(:push_to_home) end it 'pushes the status onto the home timeline without filter' do - instance = double(push: nil, filter?: false) + instance = double(push_to_home: nil, filter?: false) allow(FeedManager).to receive(:instance).and_return(instance) result = subject.perform(status.id, follower.id) expect(result).to be_nil - expect(instance).to have_received(:push).with(:home, follower, status) + expect(instance).to have_received(:push_to_home).with(follower, status) end end end |