diff options
Diffstat (limited to 'spec')
5 files changed, 240 insertions, 156 deletions
diff --git a/spec/controllers/auth/sessions_controller_spec.rb b/spec/controllers/auth/sessions_controller_spec.rb index 64ec7b794..1b8fd0b7b 100644 --- a/spec/controllers/auth/sessions_controller_spec.rb +++ b/spec/controllers/auth/sessions_controller_spec.rb @@ -225,22 +225,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders two factor authentication page' do - expect(controller).to render_template("two_factor") - expect(controller).to render_template(partial: "_otp_authentication_form") - end - end - context 'using upcase email and password' do before do post :create, params: { user: { email: user.email.upcase, password: user.password } } @@ -266,21 +250,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end - context 'using a valid OTP, attempting to leverage previous half-login to bypass password auth' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, otp_attempt: user.current_otp } }, session: { attempt_user_updated_at: user.updated_at.to_s } - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - context 'when the server has an decryption error' do before do allow_any_instance_of(User).to receive(:validate_and_consume_otp!).and_raise(OpenSSL::Cipher::CipherError) @@ -401,126 +370,6 @@ RSpec.describe Auth::SessionsController, type: :controller do end end end - - context 'when 2FA is disabled and IP is unfamiliar' do - let!(:user) { Fabricate(:user, email: 'x@y.com', password: 'abcdefgh', current_sign_in_at: 3.weeks.ago) } - - before do - request.remote_ip = '10.10.10.10' - request.user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0' - - allow(UserMailer).to receive(:sign_in_token).and_return(double('email', deliver_later!: nil)) - end - - context 'using email and password' do - before do - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token) - end - end - - context 'using email and password after an unfinished log-in attempt to a 2FA-protected account' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token) - end - end - - context 'using email and password after an unfinished log-in attempt with a sign-in token challenge' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, password: user.password } } - end - - it 'renders sign in token authentication page' do - expect(controller).to render_template("sign_in_token") - end - - it 'generates sign in token' do - expect(user.reload.sign_in_token).to_not be_nil - end - - it 'sends sign in token e-mail' do - expect(UserMailer).to have_received(:sign_in_token).with(user, any_args) - end - end - - context 'using a valid sign in token' do - before do - user.generate_sign_in_token && user.save - post :create, params: { user: { sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } - end - - it 'redirects to home' do - expect(response).to redirect_to(root_path) - end - - it 'logs the user in' do - expect(controller.current_user).to eq user - end - end - - context 'using a valid sign in token, attempting to leverage previous half-login to bypass password auth' do - let!(:other_user) do - Fabricate(:user, email: 'z@y.com', password: 'abcdefgh', otp_required_for_login: false, current_sign_in_at: 1.month.ago) - end - - before do - user.generate_sign_in_token && user.save - post :create, params: { user: { email: other_user.email, password: other_user.password } } - post :create, params: { user: { email: user.email, sign_in_token_attempt: user.sign_in_token } }, session: { attempt_user_updated_at: user.updated_at.to_s } - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - - context 'using an invalid sign in token' do - before do - post :create, params: { user: { sign_in_token_attempt: 'wrongotp' } }, session: { attempt_user_id: user.id, attempt_user_updated_at: user.updated_at.to_s } - end - - it 'shows a login error' do - expect(flash[:alert]).to match I18n.t('users.invalid_sign_in_token') - end - - it "doesn't log the user in" do - expect(controller.current_user).to be_nil - end - end - end end describe 'GET #webauthn_options' do diff --git a/spec/lib/suspicious_sign_in_detector_spec.rb b/spec/lib/suspicious_sign_in_detector_spec.rb new file mode 100644 index 000000000..101a18aa0 --- /dev/null +++ b/spec/lib/suspicious_sign_in_detector_spec.rb @@ -0,0 +1,57 @@ +require 'rails_helper' + +RSpec.describe SuspiciousSignInDetector do + describe '#suspicious?' do + let(:user) { Fabricate(:user, current_sign_in_at: 1.day.ago) } + let(:request) { double(remote_ip: remote_ip) } + let(:remote_ip) { nil } + + subject { described_class.new(user).suspicious?(request) } + + context 'when user has 2FA enabled' do + before do + user.update!(otp_required_for_login: true) + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when exact IP has been used before' do + let(:remote_ip) { '1.1.1.1' } + + before do + user.update!(sign_up_ip: remote_ip) + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when similar IP has been used before' do + let(:remote_ip) { '1.1.2.2' } + + before do + user.update!(sign_up_ip: '1.1.1.1') + end + + it 'returns false' do + expect(subject).to be false + end + end + + context 'when IP is completely unfamiliar' do + let(:remote_ip) { '2.2.2.2' } + + before do + user.update!(sign_up_ip: '1.1.1.1') + end + + it 'returns true' do + expect(subject).to be true + end + end + end +end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 8de7d8669..95712e6cf 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -87,8 +87,8 @@ class UserMailerPreview < ActionMailer::Preview UserMailer.appeal_approved(User.first, Appeal.last) end - # Preview this email at http://localhost:3000/rails/mailers/user_mailer/sign_in_token - def sign_in_token - UserMailer.sign_in_token(User.first.tap { |user| user.generate_sign_in_token }, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) + # Preview this email at http://localhost:3000/rails/mailers/user_mailer/suspicious_sign_in + def suspicious_sign_in + UserMailer.suspicious_sign_in(User.first, '127.0.0.1', 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0', Time.now.utc) end end diff --git a/spec/services/activitypub/fetch_remote_status_service_spec.rb b/spec/services/activitypub/fetch_remote_status_service_spec.rb index 68816e554..943cb161d 100644 --- a/spec/services/activitypub/fetch_remote_status_service_spec.rb +++ b/spec/services/activitypub/fetch_remote_status_service_spec.rb @@ -195,7 +195,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do let(:existing_status) { Fabricate(:status, account: sender, text: 'Foo', uri: note[:id]) } context 'with a Note object' do - let(:object) { note } + let(:object) { note.merge(updated: '2021-09-08T22:39:25Z') } it 'updates status' do existing_status.reload @@ -211,7 +211,7 @@ RSpec.describe ActivityPub::FetchRemoteStatusService, type: :service do id: "https://#{valid_domain}/@foo/1234/create", type: 'Create', actor: ActivityPub::TagManager.instance.uri_for(sender), - object: note, + object: note.merge(updated: '2021-09-08T22:39:25Z'), } end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index f87adcae1..481572742 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -1,5 +1,9 @@ require 'rails_helper' +def poll_option_json(name, votes) + { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } +end + RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } @@ -46,6 +50,180 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do expect(status.reload.spoiler_text).to eq 'Show more' end + context 'when the changes are only in sanitized-out HTML' do + let!(:status) { Fabricate(:status, text: '<p>Hello world <a href="https://joinmastodon.org" rel="nofollow">joinmastodon.org</a></p>', account: Fabricate(:account, domain: 'example.com')) } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + updated: '2021-09-08T22:39:25Z', + content: '<p>Hello world <a href="https://joinmastodon.org" rel="noreferrer">joinmastodon.org</a></p>', + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.edited?).to be false + end + end + + context 'when the status has not been explicitly edited' do + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + content: 'Updated text', + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + end + + context 'when the status has not been explicitly edited and features a poll' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.from_now.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration + } + ) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + ], + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + + it 'updates tallies' do + expect(status.poll.reload.cached_tallies).to eq [4, 3] + end + end + + context 'when the status changes a poll despite being not explicitly marked as updated' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.from_now.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration + } + ) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + poll_option_json('Baz', 3), + ], + } + end + + before do + subject.call(status, json) + end + + it 'does not create any edits' do + expect(status.reload.edits).to be_empty + end + + it 'does not mark status as edited' do + expect(status.reload.edited?).to be false + end + + it 'does not update the text' do + expect(status.reload.text).to eq 'Hello world' + end + + it 'does not update tallies' do + expect(status.poll.reload.cached_tallies).to eq [0, 0] + end + end + + context 'when receiving an edit older than the latest processed' do + before do + status.snapshot!(at_time: status.created_at, rate_limit: false) + status.update!(text: 'Hello newer world', edited_at: Time.now.utc) + status.snapshot!(rate_limit: false) + end + + it 'does not create any edits' do + expect { subject.call(status, json) }.not_to change { status.reload.edits.pluck(&:id) } + end + + it 'does not update the text, spoiler_text or edited_at' do + expect { subject.call(status, json) }.not_to change { s = status.reload; [s.text, s.spoiler_text, s.edited_at] } + end + end + context 'with no changes at all' do let(:payload) do { |