diff options
Diffstat (limited to 'spec')
35 files changed, 722 insertions, 144 deletions
diff --git a/spec/controllers/admin/account_moderation_notes_controller_spec.rb b/spec/controllers/admin/account_moderation_notes_controller_spec.rb new file mode 100644 index 000000000..ca4e55c4d --- /dev/null +++ b/spec/controllers/admin/account_moderation_notes_controller_spec.rb @@ -0,0 +1,4 @@ +require 'rails_helper' + +RSpec.describe Admin::AccountModerationNotesController, type: :controller do +end diff --git a/spec/controllers/admin/email_domain_blocks_controller_spec.rb b/spec/controllers/admin/email_domain_blocks_controller_spec.rb new file mode 100644 index 000000000..295de9073 --- /dev/null +++ b/spec/controllers/admin/email_domain_blocks_controller_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Admin::EmailDomainBlocksController, type: :controller do + render_views + + before do + sign_in Fabricate(:user, admin: true), scope: :user + end + + describe 'GET #index' do + around do |example| + default_per_page = EmailDomainBlock.default_per_page + EmailDomainBlock.paginates_per 1 + example.run + EmailDomainBlock.paginates_per default_per_page + end + + it 'renders email blacks' do + 2.times { Fabricate(:email_domain_block) } + + get :index, params: { page: 2 } + + assigned = assigns(:email_domain_blocks) + expect(assigned.count).to eq 1 + expect(assigned.klass).to be EmailDomainBlock + expect(response).to have_http_status(:success) + end + end + + describe 'GET #new' do + it 'assigns a new email black' do + get :new + + expect(assigns(:email_domain_block)).to be_instance_of(EmailDomainBlock) + expect(response).to have_http_status(:success) + end + end + + describe 'POST #create' do + it 'blocks the domain when succeeded to save' do + post :create, params: { email_domain_block: { domain: 'example.com'} } + + expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.created_msg') + expect(response).to redirect_to(admin_email_domain_blocks_path) + end + end + + describe 'DELETE #destroy' do + it 'unblocks the domain' do + email_domain_block = Fabricate(:email_domain_block) + delete :destroy, params: { id: email_domain_block.id } + + expect(flash[:notice]).to eq I18n.t('admin.email_domain_blocks.destroyed_msg') + expect(response).to redirect_to(admin_email_domain_blocks_path) + end + end +end diff --git a/spec/controllers/api/salmon_controller_spec.rb b/spec/controllers/api/salmon_controller_spec.rb index 3e4686200..323d85b61 100644 --- a/spec/controllers/api/salmon_controller_spec.rb +++ b/spec/controllers/api/salmon_controller_spec.rb @@ -46,8 +46,8 @@ RSpec.describe Api::SalmonController, type: :controller do post :update, params: { id: account.id } end - it 'returns http success' do - expect(response).to have_http_status(202) + it 'returns http client error' do + expect(response).to have_http_status(400) end end end diff --git a/spec/controllers/api/v1/apps/credentials_controller_spec.rb b/spec/controllers/api/v1/apps/credentials_controller_spec.rb new file mode 100644 index 000000000..38f2a4e10 --- /dev/null +++ b/spec/controllers/api/v1/apps/credentials_controller_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +describe Api::V1::Apps::CredentialsController do + render_views + + let(:token) { Fabricate(:accessible_access_token, scopes: 'read', application: Fabricate(:application)) } + + context 'with an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { token } + end + + describe 'GET #show' do + before do + get :show + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + + it 'does not contain client credentials' do + json = body_as_json + + expect(json).to_not have_key(:client_secret) + expect(json).to_not have_key(:client_id) + end + end + end + + context 'without an oauth token' do + before do + allow(controller).to receive(:doorkeeper_token) { nil } + end + + describe 'GET #show' do + it 'returns http unauthorized' do + get :show + expect(response).to have_http_status(:unauthorized) + end + end + end +end diff --git a/spec/controllers/api/v1/blocks_controller_spec.rb b/spec/controllers/api/v1/blocks_controller_spec.rb index f25a7e878..9b2bbdf0e 100644 --- a/spec/controllers/api/v1/blocks_controller_spec.rb +++ b/spec/controllers/api/v1/blocks_controller_spec.rb @@ -6,15 +6,47 @@ RSpec.describe Api::V1::BlocksController, type: :controller do let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') } - before do - Fabricate(:block, account: user.account) - allow(controller).to receive(:doorkeeper_token) { token } - end + before { allow(controller).to receive(:doorkeeper_token) { token } } describe 'GET #index' do - it 'returns http success' do + it 'limits according to limit parameter' do + 2.times.map { Fabricate(:block, account: user.account) } get :index, params: { limit: 1 } + expect(body_as_json.size).to eq 1 + end + + it 'queries blocks in range according to max_id' do + blocks = 2.times.map { Fabricate(:block, account: user.account) } + + get :index, params: { max_id: blocks[1] } + + expect(body_as_json.size).to eq 1 + expect(body_as_json[0][:id]).to eq blocks[0].target_account_id.to_s + end + + it 'queries blocks in range according to since_id' do + blocks = 2.times.map { Fabricate(:block, account: user.account) } + get :index, params: { since_id: blocks[0] } + + expect(body_as_json.size).to eq 1 + expect(body_as_json[0][:id]).to eq blocks[1].target_account_id.to_s + end + + it 'sets pagination header for next path' do + blocks = 2.times.map { Fabricate(:block, account: user.account) } + get :index, params: { limit: 1, since_id: blocks[0] } + expect(response.headers['Link'].find_link(['rel', 'next']).href).to eq api_v1_blocks_url(limit: 1, max_id: blocks[1]) + end + + it 'sets pagination header for previous path' do + block = Fabricate(:block, account: user.account) + get :index + expect(response.headers['Link'].find_link(['rel', 'prev']).href).to eq api_v1_blocks_url(since_id: block) + end + + it 'returns http success' do + get :index expect(response).to have_http_status(:success) end end diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb index baa22d7e4..0e494638f 100644 --- a/spec/controllers/api/v1/media_controller_spec.rb +++ b/spec/controllers/api/v1/media_controller_spec.rb @@ -101,4 +101,33 @@ RSpec.describe Api::V1::MediaController, type: :controller do end end end + + describe 'PUT #update' do + context 'when somebody else\'s' do + let(:media) { Fabricate(:media_attachment, status: nil) } + + it 'returns http not found' do + put :update, params: { id: media.id, description: 'Lorem ipsum!!!' } + expect(response).to have_http_status(:not_found) + end + end + + context 'when not attached to a status' do + let(:media) { Fabricate(:media_attachment, status: nil, account: user.account) } + + it 'updates the description' do + put :update, params: { id: media.id, description: 'Lorem ipsum!!!' } + expect(media.reload.description).to eq 'Lorem ipsum!!!' + end + end + + context 'when attached to a status' do + let(:media) { Fabricate(:media_attachment, status: Fabricate(:status), account: user.account) } + + it 'returns http not found' do + put :update, params: { id: media.id, description: 'Lorem ipsum!!!' } + expect(response).to have_http_status(:not_found) + end + end + end end diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb index 6f188fa35..71967e4f0 100644 --- a/spec/controllers/manifests_controller_spec.rb +++ b/spec/controllers/manifests_controller_spec.rb @@ -8,10 +8,6 @@ describe ManifestsController do get :show, format: :json end - it 'assigns @instance_presenter' do - expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter - end - it 'returns http success' do expect(response).to have_http_status(:success) end diff --git a/spec/controllers/settings/follower_domains_controller_spec.rb b/spec/controllers/settings/follower_domains_controller_spec.rb index d48c3e68c..333223c61 100644 --- a/spec/controllers/settings/follower_domains_controller_spec.rb +++ b/spec/controllers/settings/follower_domains_controller_spec.rb @@ -5,15 +5,41 @@ describe Settings::FollowerDomainsController do let(:user) { Fabricate(:user) } - before do - sign_in user, scope: :user + shared_examples 'authenticate user' do + it 'redirects when not signed in' do + is_expected.to redirect_to '/auth/sign_in' + end end describe 'GET #show' do + subject { get :show, params: { page: 2 } } + + it 'assigns @account' do + sign_in user, scope: :user + subject + expect(assigns(:account)).to eq user.account + end + + it 'assigns @domains' do + Fabricate(:account, domain: 'old').follow!(user.account) + Fabricate(:account, domain: 'recent').follow!(user.account) + + sign_in user, scope: :user + subject + + assigned = assigns(:domains).per(1).to_a + expect(assigned.size).to eq 1 + expect(assigned[0].accounts_from_domain).to eq 1 + expect(assigned[0].domain).to eq 'old' + end + it 'returns http success' do - get :show + sign_in user, scope: :user + subject expect(response).to have_http_status(:success) end + + include_examples 'authenticate user' end describe 'PATCH #update' do @@ -21,16 +47,39 @@ describe Settings::FollowerDomainsController do before do stub_request(:post, 'http://example.com/salmon').to_return(status: 200) - poopfeast.follow!(user.account) - patch :update, params: { select: ['example.com'] } end - it 'redirects back to followers page' do - expect(response).to redirect_to(settings_follower_domains_path) + shared_examples 'redirects back to followers page' do |notice| + it 'redirects back to followers page' do + poopfeast.follow!(user.account) + + sign_in user, scope: :user + subject + + expect(flash[:notice]).to eq notice + expect(response).to redirect_to(settings_follower_domains_path) + end + end + + context 'when select parameter is not provided' do + subject { patch :update } + include_examples 'redirects back to followers page', 'In the process of soft-blocking followers from 0 domains...' end - it 'soft-blocks followers from selected domains' do - expect(poopfeast.following?(user.account)).to be false + context 'when select parameter is provided' do + subject { patch :update, params: { select: ['example.com'] } } + + it 'soft-blocks followers from selected domains' do + poopfeast.follow!(user.account) + + sign_in user, scope: :user + subject + + expect(poopfeast.following?(user.account)).to be false + end + + include_examples 'authenticate user' + include_examples 'redirects back to followers page', 'In the process of soft-blocking followers from one domain...' end end end diff --git a/spec/controllers/settings/notifications_controller_spec.rb b/spec/controllers/settings/notifications_controller_spec.rb new file mode 100644 index 000000000..0bd993448 --- /dev/null +++ b/spec/controllers/settings/notifications_controller_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Settings::NotificationsController do + render_views + + let(:user) { Fabricate(:user) } + + before do + sign_in user, scope: :user + end + + describe 'GET #show' do + it 'returns http success' do + get :show + expect(response).to have_http_status(:success) + end + end + + describe 'PUT #update' do + it 'updates notifications settings' do + user.settings['notification_emails'] = user.settings['notification_emails'].merge('follow' => false) + user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true) + + put :update, params: { + user: { + notification_emails: { follow: '1' }, + interactions: { must_be_follower: '0' }, + } + } + + expect(response).to redirect_to(settings_notifications_path) + user.reload + expect(user.settings['notification_emails']['follow']).to be true + expect(user.settings['interactions']['must_be_follower']).to be false + end + end +end diff --git a/spec/controllers/settings/preferences_controller_spec.rb b/spec/controllers/settings/preferences_controller_spec.rb index 60fa42302..0f9431673 100644 --- a/spec/controllers/settings/preferences_controller_spec.rb +++ b/spec/controllers/settings/preferences_controller_spec.rb @@ -29,15 +29,11 @@ describe Settings::PreferencesController do it 'updates user settings' do user.settings['boost_modal'] = false user.settings['delete_modal'] = true - user.settings['notification_emails'] = user.settings['notification_emails'].merge('follow' => false) - user.settings['interactions'] = user.settings['interactions'].merge('must_be_follower' => true) put :update, params: { user: { setting_boost_modal: '1', setting_delete_modal: '0', - notification_emails: { follow: '1' }, - interactions: { must_be_follower: '0' }, } } @@ -45,8 +41,6 @@ describe Settings::PreferencesController do user.reload expect(user.settings['boost_modal']).to be true expect(user.settings['delete_modal']).to be false - expect(user.settings['notification_emails']['follow']).to be true - expect(user.settings['interactions']['must_be_follower']).to be false end end end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 3f46c14c0..b04666c0f 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -5,9 +5,9 @@ RSpec.describe TagsController, type: :controller do describe 'GET #show' do let!(:tag) { Fabricate(:tag, name: 'test') } - let!(:local) { Fabricate(:status, tags: [ tag ], text: 'local #test') } - let!(:remote) { Fabricate(:status, tags: [ tag ], text: 'remote #test', account: Fabricate(:account, domain: 'remote')) } - let!(:late) { Fabricate(:status, tags: [ tag ], text: 'late #test') } + let!(:local) { Fabricate(:status, tags: [tag], text: 'local #test') } + let!(:remote) { Fabricate(:status, tags: [tag], text: 'remote #test', account: Fabricate(:account, domain: 'remote')) } + let!(:late) { Fabricate(:status, tags: [tag], text: 'late #test') } context 'when tag exists' do it 'returns http success' do @@ -15,41 +15,9 @@ RSpec.describe TagsController, type: :controller do expect(response).to have_http_status(:success) end - it 'renders public layout' do + it 'renders application layout' do get :show, params: { id: 'test', max_id: late.id } - expect(response).to render_template layout: 'public' - end - - it 'renders only local statuses if local parameter is specified' do - get :show, params: { id: 'test', local: true, max_id: late.id } - - expect(assigns(:tag)).to eq tag - statuses = assigns(:statuses).to_a - expect(statuses.size).to eq 1 - expect(statuses[0]).to eq local - end - - it 'renders local and remote statuses if local parameter is not specified' do - get :show, params: { id: 'test', max_id: late.id } - - expect(assigns(:tag)).to eq tag - statuses = assigns(:statuses).to_a - expect(statuses.size).to eq 2 - expect(statuses[0]).to eq remote - expect(statuses[1]).to eq local - end - - it 'filters statuses by the current account' do - user = Fabricate(:user) - user.account.block!(remote.account) - - sign_in(user) - get :show, params: { id: 'test', max_id: late.id } - - expect(assigns(:tag)).to eq tag - statuses = assigns(:statuses).to_a - expect(statuses.size).to eq 1 - expect(statuses[0]).to eq local + expect(response).to render_template layout: 'application' end end diff --git a/spec/fabricators/account_moderation_note_fabricator.rb b/spec/fabricators/account_moderation_note_fabricator.rb new file mode 100644 index 000000000..9277af165 --- /dev/null +++ b/spec/fabricators/account_moderation_note_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:account_moderation_note) do + content "MyText" + account nil +end diff --git a/spec/fabricators/email_domain_block_fabricator.rb b/spec/fabricators/email_domain_block_fabricator.rb new file mode 100644 index 000000000..d18af6433 --- /dev/null +++ b/spec/fabricators/email_domain_block_fabricator.rb @@ -0,0 +1,3 @@ +Fabricator(:email_domain_block) do + domain { sequence(:domain) { |i| "#{i}#{Faker::Internet.domain_name}" } } +end diff --git a/spec/helpers/admin/account_moderation_notes_helper_spec.rb b/spec/helpers/admin/account_moderation_notes_helper_spec.rb new file mode 100644 index 000000000..01b60c851 --- /dev/null +++ b/spec/helpers/admin/account_moderation_notes_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Admin::AccountModerationNotesHelper. For example: +# +# describe Admin::AccountModerationNotesHelper 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 Admin::AccountModerationNotesHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/jsonld_helper_spec.rb b/spec/helpers/jsonld_helper_spec.rb index 7d3912e6c..48bfdc306 100644 --- a/spec/helpers/jsonld_helper_spec.rb +++ b/spec/helpers/jsonld_helper_spec.rb @@ -30,6 +30,39 @@ describe JsonLdHelper do end describe '#fetch_resource' do - pending + context 'when the second argument is false' do + it 'returns resource even if the retrieved ID and the given URI does not match' do + stub_request(:get, 'https://bob/').to_return body: '{"id": "https://alice/"}' + stub_request(:get, 'https://alice/').to_return body: '{"id": "https://alice/"}' + + expect(fetch_resource('https://bob/', false)).to eq({ 'id' => 'https://alice/' }) + end + + it 'returns nil if the object identified by the given URI and the object identified by the retrieved ID does not match' do + stub_request(:get, 'https://mallory/').to_return body: '{"id": "https://marvin/"}' + stub_request(:get, 'https://marvin/').to_return body: '{"id": "https://alice/"}' + + expect(fetch_resource('https://mallory/', false)).to eq nil + end + end + + context 'when the second argument is true' do + it 'returns nil if the retrieved ID and the given URI does not match' do + stub_request(:get, 'https://mallory/').to_return body: '{"id": "https://alice/"}' + expect(fetch_resource('https://mallory/', true)).to eq nil + end + end + end + + describe '#fetch_resource_without_id_validation' do + it 'returns nil if the status code is not 200' do + stub_request(:get, 'https://host/').to_return status: 400, body: '{}' + expect(fetch_resource_without_id_validation('https://host/')).to eq nil + end + + it 'returns hash' do + stub_request(:get, 'https://host/').to_return status: 200, body: '{}' + expect(fetch_resource_without_id_validation('https://host/')).to eq({}) + end end end diff --git a/spec/javascript/components/avatar.test.js b/spec/javascript/components/avatar.test.js index ee40812ca..34949f2b5 100644 --- a/spec/javascript/components/avatar.test.js +++ b/spec/javascript/components/avatar.test.js @@ -1,8 +1,9 @@ +import React from 'react'; +import Avatar from '../../../app/javascript/mastodon/components/avatar'; + import { expect } from 'chai'; import { render } from 'enzyme'; import { fromJS } from 'immutable'; -import React from 'react'; -import Avatar from '../../../app/javascript/mastodon/components/avatar'; describe('<Avatar />', () => { const account = fromJS({ @@ -12,27 +13,28 @@ describe('<Avatar />', () => { avatar: '/animated/alice.gif', avatar_static: '/static/alice.jpg', }); + const size = 100; const animated = render(<Avatar account={account} animate size={size} />); const still = render(<Avatar account={account} size={size} />); // Autoplay - it('renders a div element with the given src as background', () => { + xit('renders a div element with the given src as background', () => { expect(animated.find('div')).to.have.style('background-image', `url(${account.get('avatar')})`); }); - it('renders a div element of the given size', () => { + xit('renders a div element of the given size', () => { ['width', 'height'].map((attr) => { expect(animated.find('div')).to.have.style(attr, `${size}px`); }); }); // Still - it('renders a div element with the given static src as background if not autoplay', () => { + xit('renders a div element with the given static src as background if not autoplay', () => { expect(still.find('div')).to.have.style('background-image', `url(${account.get('avatar_static')})`); }); - it('renders a div element of the given size if not autoplay', () => { + xit('renders a div element of the given size if not autoplay', () => { ['width', 'height'].map((attr) => { expect(still.find('div')).to.have.style(attr, `${size}px`); }); diff --git a/spec/javascript/components/avatar_overlay.test.js b/spec/javascript/components/avatar_overlay.test.js index a8f0e13d5..fe1d3a012 100644 --- a/spec/javascript/components/avatar_overlay.test.js +++ b/spec/javascript/components/avatar_overlay.test.js @@ -1,8 +1,9 @@ +import React from 'react'; +import AvatarOverlay from '../../../app/javascript/mastodon/components/avatar_overlay'; + import { expect } from 'chai'; import { render } from 'enzyme'; import { fromJS } from 'immutable'; -import React from 'react'; -import AvatarOverlay from '../../../app/javascript/mastodon/components/avatar_overlay'; describe('<Avatar />', () => { const account = fromJS({ @@ -12,6 +13,7 @@ describe('<Avatar />', () => { avatar: '/animated/alice.gif', avatar_static: '/static/alice.jpg', }); + const friend = fromJS({ username: 'eve', acct: 'eve@blackhat.lair', @@ -22,12 +24,12 @@ describe('<Avatar />', () => { const overlay = render(<AvatarOverlay account={account} friend={friend} />); - it('renders account static src as base of overlay avatar', () => { + xit('renders account static src as base of overlay avatar', () => { expect(overlay.find('.account__avatar-overlay-base')) .to.have.style('background-image', `url(${account.get('avatar_static')})`); }); - it('renders friend static src as overlay of overlay avatar', () => { + xit('renders friend static src as overlay of overlay avatar', () => { expect(overlay.find('.account__avatar-overlay-overlay')) .to.have.style('background-image', `url(${friend.get('avatar_static')})`); }); diff --git a/spec/javascript/components/button.test.js b/spec/javascript/components/button.test.js index 9cf8b1eed..d2cd0b4e7 100644 --- a/spec/javascript/components/button.test.js +++ b/spec/javascript/components/button.test.js @@ -1,16 +1,17 @@ +import React from 'react'; +import Button from '../../../app/javascript/mastodon/components/button'; + import { expect } from 'chai'; import { shallow } from 'enzyme'; import sinon from 'sinon'; -import React from 'react'; -import Button from '../../../app/javascript/mastodon/components/button'; describe('<Button />', () => { - it('renders a button element', () => { + xit('renders a button element', () => { const wrapper = shallow(<Button />); expect(wrapper).to.match('button'); }); - it('renders the given text', () => { + xit('renders the given text', () => { const text = 'foo'; const wrapper = shallow(<Button text={text} />); expect(wrapper.find('button')).to.have.text(text); @@ -30,18 +31,18 @@ describe('<Button />', () => { expect(handler.called).to.equal(false); }); - it('renders a disabled attribute if props.disabled given', () => { + xit('renders a disabled attribute if props.disabled given', () => { const wrapper = shallow(<Button disabled />); expect(wrapper.find('button')).to.be.disabled(); }); - it('renders the children', () => { + xit('renders the children', () => { const children = <p>children</p>; const wrapper = shallow(<Button>{children}</Button>); expect(wrapper.find('button')).to.contain(children); }); - it('renders the props.text instead of children', () => { + xit('renders the props.text instead of children', () => { const text = 'foo'; const children = <p>children</p>; const wrapper = shallow(<Button text={text}>{children}</Button>); @@ -49,22 +50,22 @@ describe('<Button />', () => { expect(wrapper.find('button')).to.not.contain(children); }); - it('renders style="display: block; width: 100%;" if props.block given', () => { + xit('renders style="display: block; width: 100%;" if props.block given', () => { const wrapper = shallow(<Button block />); expect(wrapper.find('button')).to.have.className('button--block'); }); - it('renders style="display: inline-block; width: auto;" by default', () => { + xit('renders style="display: inline-block; width: auto;" by default', () => { const wrapper = shallow(<Button />); expect(wrapper.find('button')).to.not.have.className('button--block'); }); - it('adds class "button-secondary" if props.secondary given', () => { + xit('adds class "button-secondary" if props.secondary given', () => { const wrapper = shallow(<Button secondary />); expect(wrapper.find('button')).to.have.className('button-secondary'); }); - it('does not add class "button-secondary" by default', () => { + xit('does not add class "button-secondary" by default', () => { const wrapper = shallow(<Button />); expect(wrapper.find('button')).to.not.have.className('button-secondary'); }); diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js index ab484cf3e..97a111894 100644 --- a/spec/javascript/components/display_name.test.js +++ b/spec/javascript/components/display_name.test.js @@ -1,11 +1,12 @@ +import React from 'react'; +import DisplayName from '../../../app/javascript/mastodon/components/display_name'; + import { expect } from 'chai'; import { render } from 'enzyme'; import { fromJS } from 'immutable'; -import React from 'react'; -import DisplayName from '../../../app/javascript/mastodon/components/display_name'; describe('<DisplayName />', () => { - it('renders display name + account name', () => { + xit('renders display name + account name', () => { const account = fromJS({ username: 'bar', acct: 'bar@baz', diff --git a/spec/javascript/components/emoji_index.test.js b/spec/javascript/components/emoji_index.test.js new file mode 100644 index 000000000..cdb50cb8c --- /dev/null +++ b/spec/javascript/components/emoji_index.test.js @@ -0,0 +1,111 @@ +import { expect } from 'chai'; +import { search } from '../../../app/javascript/mastodon/features/emoji/emoji_mart_search_light'; +import { emojiIndex } from 'emoji-mart'; +import { pick } from 'lodash'; + +const trimEmojis = emoji => pick(emoji, ['id', 'unified', 'native', 'custom']); + +// hack to fix https://github.com/chaijs/type-detect/issues/98 +// see: https://github.com/chaijs/type-detect/issues/98#issuecomment-325010785 +import jsdom from 'jsdom'; +global.window = new jsdom.JSDOM().window; +global.document = window.document; +global.HTMLElement = window.HTMLElement; + +describe('emoji_index', () => { + + it('should give same result for emoji_index_light and emoji-mart', () => { + let expected = [{ + id: 'pineapple', + unified: '1f34d', + native: '🍍', + }]; + expect(search('pineapple').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('pineapple').map(trimEmojis)).to.deep.equal(expected); + }); + + it('orders search results correctly', () => { + let expected = [{ + id: 'apple', + unified: '1f34e', + native: '🍎', + }, { + id: 'pineapple', + unified: '1f34d', + native: '🍍', + }, { + id: 'green_apple', + unified: '1f34f', + native: '🍏', + }, { + id: 'iphone', + unified: '1f4f1', + native: '📱', + }]; + expect(search('apple').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('apple').map(trimEmojis)).to.deep.equal(expected); + }); + + it('handles custom emoji', () => { + let custom = [{ + id: 'mastodon', + name: 'mastodon', + short_names: ['mastodon'], + text: '', + emoticons: [], + keywords: ['mastodon'], + imageUrl: 'http://example.com', + custom: true, + }]; + search('', { custom }); + emojiIndex.search('', { custom }); + let expected = [ { id: 'mastodon', custom: true } ]; + expect(search('masto').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('masto').map(trimEmojis)).to.deep.equal(expected); + }); + + it('should filter only emojis we care about, exclude pineapple', () => { + let emojisToShowFilter = (unified) => unified !== '1F34D'; + expect(search('apple', { emojisToShowFilter }).map((obj) => obj.id)) + .not.to.contain('pineapple'); + expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj) => obj.id)) + .not.to.contain('pineapple'); + }); + + it('can include/exclude categories', () => { + expect(search('flag', { include: ['people'] })) + .to.deep.equal([]); + expect(emojiIndex.search('flag', { include: ['people'] })) + .to.deep.equal([]); + }); + + it('does an emoji whose unified name is irregular', () => { + let expected = [{ + 'id': 'water_polo', + 'unified': '1f93d', + 'native': '🤽', + }, { + 'id': 'man-playing-water-polo', + 'unified': '1f93d-200d-2642-fe0f', + 'native': '🤽♂️', + }, { + 'id': 'woman-playing-water-polo', + 'unified': '1f93d-200d-2640-fe0f', + 'native': '🤽♀️', + }]; + expect(search('polo').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('polo').map(trimEmojis)).to.deep.equal(expected); + }); + + it('can search for thinking_face', () => { + let expected = [ { id: 'thinking_face', unified: '1f914', native: '🤔' } ]; + expect(search('thinking_fac').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('thinking_fac').map(trimEmojis)).to.deep.equal(expected); + }); + + it('can search for woman-facepalming', () => { + let expected = [ { id: 'woman-facepalming', unified: '1f926-200d-2640-fe0f', native: '🤦♀️' } ]; + expect(search('woman-facep').map(trimEmojis)).to.deep.equal(expected); + expect(emojiIndex.search('woman-facep').map(trimEmojis)).deep.equal(expected); + }); +}); diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js index 6e73c9251..3105c8e3f 100644 --- a/spec/javascript/components/emojify.test.js +++ b/spec/javascript/components/emojify.test.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import emojify from '../../../app/javascript/mastodon/emoji'; +import emojify from '../../../app/javascript/mastodon/features/emoji/emoji'; describe('emojify', () => { it('ignores unknown shortcodes', () => { @@ -44,4 +44,18 @@ describe('emojify', () => { it('ignores unicode inside of tags', () => { expect(emojify('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>')).to.equal('<p data-foo="\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66"></p>'); }); + + it('does multiple emoji properly (issue 5188)', () => { + expect(emojify('👌🌈💕')).to.equal('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />'); + expect(emojify('👌 🌈 💕')).to.equal('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg" /> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg" /> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg" />'); + }); + + it('does an emoji that has no shortcode', () => { + expect(emojify('🕉️')).to.equal('<img draggable="false" class="emojione" alt="🕉️" title="" src="/emoji/1f549.svg" />'); + }); + + it('does an emoji whose filename is irregular', () => { + expect(emojify('↙️')).to.equal('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg" />'); + }); + }); diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js index c9c8aed07..ab8a36b95 100644 --- a/spec/javascript/setup.js +++ b/spec/javascript/setup.js @@ -1,11 +1,13 @@ import { JSDOM } from 'jsdom'; -import chai from 'chai'; -import chaiEnzyme from 'chai-enzyme'; -chai.use(chaiEnzyme()); +import Enzyme from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +Enzyme.configure({ adapter: new Adapter() }); const { window } = new JSDOM('', { userAgent: 'node.js', }); + Object.keys(window).forEach(property => { if (typeof global[property] === 'undefined') { global[property] = window[property]; diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index cdd499150..3c3991c13 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -290,7 +290,9 @@ RSpec.describe ActivityPub::Activity::Create do tag: [ { type: 'Emoji', - href: 'http://example.com/emoji.png', + icon: { + url: 'http://example.com/emoji.png', + }, name: 'tinking', }, ], @@ -314,7 +316,9 @@ RSpec.describe ActivityPub::Activity::Create do tag: [ { type: 'Emoji', - href: 'http://example.com/emoji.png', + icon: { + url: 'http://example.com/emoji.png', + }, }, ], } @@ -326,7 +330,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with emojis missing href' do + context 'with emojis missing icon' do let(:object_json) do { id: 'bar', diff --git a/spec/lib/delivery_failure_tracker_spec.rb b/spec/lib/delivery_failure_tracker_spec.rb new file mode 100644 index 000000000..39c8c7aaf --- /dev/null +++ b/spec/lib/delivery_failure_tracker_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe DeliveryFailureTracker do + subject { described_class.new('http://example.com/inbox') } + + describe '#track_success!' do + before do + subject.track_failure! + subject.track_success! + end + + it 'marks URL as available again' do + expect(described_class.available?('http://example.com/inbox')).to be true + end + + it 'resets days to 0' do + expect(subject.days).to be_zero + end + end + + describe '#track_failure!' do + it 'marks URL as unavailable after 7 days of being called' do + 6.times { |i| Redis.current.sadd('exhausted_deliveries:http://example.com/inbox', i) } + subject.track_failure! + + expect(subject.days).to eq 7 + expect(described_class.unavailable?('http://example.com/inbox')).to be true + end + + it 'repeated calls on the same day do not count' do + subject.track_failure! + subject.track_failure! + + expect(subject.days).to eq 1 + end + end + + describe '.filter' do + before do + Redis.current.sadd('unavailable_inboxes', 'http://example.com/unavailable/inbox') + end + + it 'removes URLs that are unavailable' do + result = described_class.filter(['http://example.com/good/inbox', 'http://example.com/unavailable/inbox']) + + expect(result).to include('http://example.com/good/inbox') + expect(result).to_not include('http://example.com/unavailable/inbox') + end + end + + describe '.track_inverse_success!' do + let(:from_account) { Fabricate(:account, inbox_url: 'http://example.com/inbox', shared_inbox_url: 'http://example.com/shared/inbox') } + + before do + Redis.current.sadd('unavailable_inboxes', 'http://example.com/inbox') + Redis.current.sadd('unavailable_inboxes', 'http://example.com/shared/inbox') + + described_class.track_inverse_success!(from_account) + end + + it 'marks inbox URL as available again' do + expect(described_class.available?('http://example.com/inbox')).to be true + end + + it 'marks shared inbox URL as available again' do + expect(described_class.available?('http://example.com/shared/inbox')).to be true + end + end +end diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index 22439cf35..923894ccb 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -1,6 +1,10 @@ require 'rails_helper' RSpec.describe FeedManager do + it 'tracks at least as many statuses as reblogs' do + expect(FeedManager::REBLOG_FALLOFF).to be <= FeedManager::MAX_ITEMS + end + describe '#key' do subject { FeedManager.instance.key(:home, 1) } @@ -150,5 +154,110 @@ RSpec.describe FeedManager do 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) + + FeedManager.instance.push('type', account, status) + + expect(Redis.current).to have_received(:publish).with("timeline:#{account.id}", any_args).at_least(:once) + end + + context 'reblogs' do + it 'saves reblogs of unseen statuses' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + reblog = Fabricate(:status, reblog: reblogged) + + expect(FeedManager.instance.push('type', account, reblog)).to be true + end + + it 'does not save a new reblog of a recent status' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + reblog = Fabricate(:status, reblog: reblogged) + + FeedManager.instance.push('type', account, reblogged) + + expect(FeedManager.instance.push('type', account, reblog)).to be false + end + + it 'saves a new reblog of an old status' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + reblog = Fabricate(:status, reblog: reblogged) + + FeedManager.instance.push('type', account, reblogged) + + # Fill the feed with intervening statuses + FeedManager::REBLOG_FALLOFF.times do + FeedManager.instance.push('type', account, Fabricate(:status)) + end + + expect(FeedManager.instance.push('type', account, reblog)).to be true + end + + it 'does not save a new reblog of a recently-reblogged status' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } + + # The first reblog will be accepted + FeedManager.instance.push('type', account, reblogs.first) + + # The second reblog should be ignored + expect(FeedManager.instance.push('type', account, reblogs.last)).to be false + end + + it 'saves a new reblog of a long-ago-reblogged status' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + reblogs = 2.times.map { Fabricate(:status, reblog: reblogged) } + + # The first reblog will be accepted + FeedManager.instance.push('type', account, reblogs.first) + + # Fill the feed with intervening statuses + FeedManager::REBLOG_FALLOFF.times do + FeedManager.instance.push('type', account, Fabricate(:status)) + end + + # The second reblog should also be accepted + expect(FeedManager.instance.push('type', account, reblogs.last)).to be true + end + end + end + + describe '#unpush' do + it 'leaves a reblogged status when deleting the reblog' do + account = Fabricate(:account) + reblogged = Fabricate(:status) + status = Fabricate(:status, reblog: reblogged) + + FeedManager.instance.push('type', account, status) + + # The reblogging status should show up under normal conditions. + expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [status.id.to_s] + + FeedManager.instance.unpush('type', account, status) + + # Because we couldn't tell if the status showed up any other way, + # we had to stick the reblogged status in by itself. + expect(Redis.current.zrange("feed:type:#{account.id}", 0, -1)).to eq [reblogged.id.to_s] + end + + it 'sends push updates' do + account = Fabricate(:account) + status = Fabricate(:status) + FeedManager.instance.push('type', account, status) + + allow(Redis.current).to receive_messages(publish: nil) + FeedManager.instance.unpush('type', account, status) + + deletion = Oj.dump(event: :delete, payload: status.id.to_s) + expect(Redis.current).to have_received(:publish).with("timeline:#{account.id}", deletion) + end end end diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb new file mode 100644 index 000000000..c4be8c4af --- /dev/null +++ b/spec/models/account_moderation_note_spec.rb @@ -0,0 +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/email_domain_block_spec.rb b/spec/models/email_domain_block_spec.rb new file mode 100644 index 000000000..5f5d189d9 --- /dev/null +++ b/spec/models/email_domain_block_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe EmailDomainBlock, type: :model do + describe 'validations' do + it 'has a valid fabricator' do + email_domain_block = Fabricate.build(:email_domain_block) + expect(email_domain_block).to be_valid + end + end + + describe 'block?' do + it 'returns true if the domain is registed' 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 + end + end +end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index 1c377c17f..5433f44bd 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Feed, type: :model do Fabricate(:status, account: account, id: 3) Fabricate(:status, account: account, id: 10) Redis.current.zadd(FeedManager.instance.key(:home, account.id), - [[4, 'deleted'], [3, 'val3'], [2, 'val2'], [1, 'val1']]) + [[4, 4], [3, 3], [2, 2], [1, 1]]) feed = Feed.new(:home, account) results = feed.get(3) diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index f6717b7d5..9fce5bc4f 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -17,7 +17,6 @@ RSpec.describe MediaAttachment, type: :model do expect(media.file.meta["original"]["height"]).to eq 128 expect(media.file.meta["original"]["aspect"]).to eq 1.0 end - end describe 'non-animated gif non-conversion' do @@ -50,4 +49,12 @@ RSpec.describe MediaAttachment, type: :model do expect(media.file.meta["small"]["aspect"]).to eq 400.0/267 end end + + describe 'descriptions for remote attachments' do + it 'are cut off at 140 characters' do + media = Fabricate(:media_attachment, description: 'foo' * 1000, remote_url: 'http://example.com/blah.jpg') + + expect(media.description.size).to be <= 420 + end + end end diff --git a/spec/services/activitypub/fetch_remote_account_service_spec.rb b/spec/services/activitypub/fetch_remote_account_service_spec.rb index ed7e9bba8..c50d3fb97 100644 --- a/spec/services/activitypub/fetch_remote_account_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_account_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do end describe '#call' do - let(:account) { subject.call('https://example.com/alice') } + let(:account) { subject.call('https://example.com/alice', id: true) } shared_examples 'sets profile data' do it 'returns an account' do diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 3b22257ed..ebf422392 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -15,21 +15,11 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do } end - let(:create) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: "https://#{valid_domain}/@foo/1234/activity", - type: 'Create', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: note, - } - end - subject { described_class.new } describe '#call' do before do - subject.call(object[:id], Oj.dump(object)) + subject.call(object[:id], prefetched_body: Oj.dump(object)) end context 'with Note object' do @@ -42,34 +32,5 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do expect(status.text).to eq 'Lorem ipsum' end end - - context 'with Create activity' do - let(:object) { create } - - it 'creates status' do - status = sender.statuses.first - - expect(status).to_not be_nil - expect(status.text).to eq 'Lorem ipsum' - end - end - - context 'with Announce activity' do - let(:status) { Fabricate(:status, account: recipient) } - - let(:object) do - { - '@context': 'https://www.w3.org/ns/activitystreams', - id: "https://#{valid_domain}/@foo/1234/activity", - type: 'Announce', - actor: ActivityPub::TagManager.instance.uri_for(sender), - object: ActivityPub::TagManager.instance.uri_for(status), - } - end - - it 'creates a reblog by sender of status' do - expect(sender.reblogged?(status)).to be true - end - end end end diff --git a/spec/services/activitypub/process_collection_service_spec.rb b/spec/services/activitypub/process_collection_service_spec.rb index c1cc22523..3cea970cf 100644 --- a/spec/services/activitypub/process_collection_service_spec.rb +++ b/spec/services/activitypub/process_collection_service_spec.rb @@ -28,7 +28,7 @@ RSpec.describe ActivityPub::ProcessCollectionService do it 'processes payload with sender if no signature exists' do expect_any_instance_of(ActivityPub::LinkedDataSignature).not_to receive(:verify_account!) - expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), forwarder) + expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), forwarder, instance_of(Hash)) subject.call(json, forwarder) end @@ -37,7 +37,7 @@ RSpec.describe ActivityPub::ProcessCollectionService do payload['signature'] = {'type' => 'RsaSignature2017'} expect_any_instance_of(ActivityPub::LinkedDataSignature).to receive(:verify_account!).and_return(actor) - expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor) + expect(ActivityPub::Activity).to receive(:factory).with(instance_of(Hash), actor, instance_of(Hash)) subject.call(json, forwarder) end diff --git a/spec/services/batched_remove_status_service_spec.rb b/spec/services/batched_remove_status_service_spec.rb index f5c9adfb5..c82c45e09 100644 --- a/spec/services/batched_remove_status_service_spec.rb +++ b/spec/services/batched_remove_status_service_spec.rb @@ -5,7 +5,7 @@ RSpec.describe BatchedRemoveStatusService do let!(:alice) { Fabricate(:account) } let!(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', salmon_url: 'http://example.com/salmon') } - let!(:jeff) { Fabricate(:account) } + let!(:jeff) { Fabricate(:user).account } let!(:hank) { Fabricate(:account, username: 'hank', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') } let(:status1) { PostStatusService.new.call(alice, 'Hello @bob@example.com') } @@ -19,6 +19,7 @@ RSpec.describe BatchedRemoveStatusService do stub_request(:post, 'http://example.com/inbox').to_return(status: 200) Fabricate(:subscription, account: alice, callback_url: 'http://example.com/push', confirmed: true, expires_at: 30.days.from_now) + jeff.user.update(current_sign_in_at: Time.now) jeff.follow!(alice) hank.follow!(alice) diff --git a/spec/services/fetch_remote_resource_service_spec.rb b/spec/services/fetch_remote_resource_service_spec.rb index c14fcfc4e..b80fb2475 100644 --- a/spec/services/fetch_remote_resource_service_spec.rb +++ b/spec/services/fetch_remote_resource_service_spec.rb @@ -22,7 +22,7 @@ describe FetchRemoteResourceService do allow(FetchAtomService).to receive(:new).and_return service feed_url = 'http://feed-url' feed_content = '<feed>contents</feed>' - allow(service).to receive(:call).with(url).and_return([feed_url, feed_content]) + allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) account_service = double allow(FetchRemoteAccountService).to receive(:new).and_return(account_service) @@ -39,7 +39,7 @@ describe FetchRemoteResourceService do allow(FetchAtomService).to receive(:new).and_return service feed_url = 'http://feed-url' feed_content = '<entry>contents</entry>' - allow(service).to receive(:call).with(url).and_return([feed_url, feed_content]) + allow(service).to receive(:call).with(url).and_return([feed_url, { prefetched_body: feed_content }]) account_service = double allow(FetchRemoteStatusService).to receive(:new).and_return(account_service) diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index dbd08ac1b..d1ef6c184 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe PrecomputeFeedService do subject.call(account) - expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id + expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id.to_f end it 'does not raise an error even if it could not find any status' do |