about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/auth/sessions_controller_spec.rb151
-rw-r--r--spec/lib/suspicious_sign_in_detector_spec.rb57
-rw-r--r--spec/mailers/previews/user_mailer_preview.rb6
-rw-r--r--spec/services/activitypub/fetch_remote_status_service_spec.rb4
-rw-r--r--spec/services/activitypub/process_status_update_service_spec.rb178
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
         {