about summary refs log tree commit diff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/api/v1/apps_controller_spec.rb78
-rw-r--r--spec/controllers/api/v1/push/subscriptions_controller_spec.rb28
-rw-r--r--spec/controllers/api/web/push_subscriptions_controller_spec.rb23
-rw-r--r--spec/fabricators/canonical_email_block_fabricator.rb4
-rw-r--r--spec/fabricators/follow_recommendation_suppression_fabricator.rb3
-rw-r--r--spec/lib/spam_check_spec.rb192
-rw-r--r--spec/lib/tag_manager_spec.rb36
-rw-r--r--spec/models/canonical_email_block_spec.rb47
-rw-r--r--spec/models/follow_recommendation_suppression_spec.rb4
-rw-r--r--spec/models/web/push_subscription_spec.rb94
-rw-r--r--spec/validators/blacklisted_email_validator_spec.rb29
-rw-r--r--spec/workers/web/push_notification_worker_spec.rb48
12 files changed, 316 insertions, 270 deletions
diff --git a/spec/controllers/api/v1/apps_controller_spec.rb b/spec/controllers/api/v1/apps_controller_spec.rb
index 60a4c3b41..70cd62d48 100644
--- a/spec/controllers/api/v1/apps_controller_spec.rb
+++ b/spec/controllers/api/v1/apps_controller_spec.rb
@@ -4,23 +4,83 @@ RSpec.describe Api::V1::AppsController, type: :controller do
   render_views
 
   describe 'POST #create' do
+    let(:client_name) { 'Test app' }
+    let(:scopes) { nil }
+    let(:redirect_uris) { 'urn:ietf:wg:oauth:2.0:oob' }
+    let(:website) { nil }
+
+    let(:app_params) do
+      {
+        client_name: client_name,
+        redirect_uris: redirect_uris,
+        scopes: scopes,
+        website: website,
+      }
+    end
+
     before do
-      post :create, params: { client_name: 'Test app', redirect_uris: 'urn:ietf:wg:oauth:2.0:oob' }
+      post :create, params: app_params
     end
 
-    it 'returns http success' do
-      expect(response).to have_http_status(200)
+    context 'with valid params' do
+      it 'returns http success' do
+        expect(response).to have_http_status(200)
+      end
+
+      it 'creates an OAuth app' do
+        expect(Doorkeeper::Application.find_by(name: client_name)).to_not be nil
+      end
+
+      it 'returns client ID and client secret' do
+        json = body_as_json
+
+        expect(json[:client_id]).to_not be_blank
+        expect(json[:client_secret]).to_not be_blank
+      end
+    end
+
+    context 'with an unsupported scope' do
+      let(:scopes) { 'hoge' }
+
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(422)
+      end
     end
 
-    it 'creates an OAuth app' do
-      expect(Doorkeeper::Application.find_by(name: 'Test app')).to_not be nil
+    context 'with many duplicate scopes' do
+      let(:scopes) { (%w(read) * 40).join(' ') }
+
+      it 'returns http success' do
+        expect(response).to have_http_status(200)
+      end
+
+      it 'only saves the scope once' do
+        expect(Doorkeeper::Application.find_by(name: client_name).scopes.to_s).to eq 'read'
+      end
+    end
+
+    context 'with a too-long name' do
+      let(:client_name) { 'hoge' * 20 }
+
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(422)
+      end
+    end
+
+    context 'with a too-long website' do
+      let(:website) { 'https://foo.bar/' + ('hoge' * 2_000) }
+
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(422)
+      end
     end
 
-    it 'returns client ID and client secret' do
-      json = body_as_json
+    context 'with a too-long redirect_uris' do
+      let(:redirect_uris) { 'https://foo.bar/' + ('hoge' * 2_000) }
 
-      expect(json[:client_id]).to_not be_blank
-      expect(json[:client_secret]).to_not be_blank
+      it 'returns http unprocessable entity' do
+        expect(response).to have_http_status(422)
+      end
     end
   end
 end
diff --git a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
index 01146294f..534d02879 100644
--- a/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
+++ b/spec/controllers/api/v1/push/subscriptions_controller_spec.rb
@@ -27,20 +27,27 @@ describe Api::V1::Push::SubscriptionsController do
   let(:alerts_payload) do
     {
       data: {
+        policy: 'all',
+
         alerts: {
           follow: true,
+          follow_request: true,
           favourite: false,
           reblog: true,
           mention: false,
+          poll: true,
+          status: false,
         }
       }
     }.with_indifferent_access
   end
 
   describe 'POST #create' do
-    it 'saves push subscriptions' do
+    before do
       post :create, params: create_payload
+    end
 
+    it 'saves push subscriptions' do
       push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
 
       expect(push_subscription.endpoint).to eq(create_payload[:subscription][:endpoint])
@@ -52,31 +59,34 @@ describe Api::V1::Push::SubscriptionsController do
 
     it 'replaces old subscription on repeat calls' do
       post :create, params: create_payload
-      post :create, params: create_payload
-
       expect(Web::PushSubscription.where(endpoint: create_payload[:subscription][:endpoint]).count).to eq 1
     end
   end
 
   describe 'PUT #update' do
-    it 'changes alert settings' do
+    before do
       post :create, params: create_payload
       put :update, params: alerts_payload
+    end
 
+    it 'changes alert settings' do
       push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
 
-      expect(push_subscription.data.dig('alerts', 'follow')).to eq(alerts_payload[:data][:alerts][:follow].to_s)
-      expect(push_subscription.data.dig('alerts', 'favourite')).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
-      expect(push_subscription.data.dig('alerts', 'reblog')).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
-      expect(push_subscription.data.dig('alerts', 'mention')).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+      expect(push_subscription.data['policy']).to eq(alerts_payload[:data][:policy])
+
+      %w(follow follow_request favourite reblog mention poll status).each do |type|
+        expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+      end
     end
   end
 
   describe 'DELETE #destroy' do
-    it 'removes the subscription' do
+    before do
       post :create, params: create_payload
       delete :destroy
+    end
 
+    it 'removes the subscription' do
       expect(Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])).to be_nil
     end
   end
diff --git a/spec/controllers/api/web/push_subscriptions_controller_spec.rb b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
index 381cdeab9..bda4a7661 100644
--- a/spec/controllers/api/web/push_subscriptions_controller_spec.rb
+++ b/spec/controllers/api/web/push_subscriptions_controller_spec.rb
@@ -22,11 +22,16 @@ describe Api::Web::PushSubscriptionsController do
   let(:alerts_payload) do
     {
       data: {
+        policy: 'all',
+
         alerts: {
           follow: true,
+          follow_request: false,
           favourite: false,
           reblog: true,
           mention: false,
+          poll: true,
+          status: false,
         }
       }
     }
@@ -59,10 +64,11 @@ describe Api::Web::PushSubscriptionsController do
 
         push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
 
-        expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s)
-        expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
-        expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
-        expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+        expect(push_subscription.data['policy']).to eq 'all'
+
+        %w(follow follow_request favourite reblog mention poll status).each do |type|
+          expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+        end
       end
     end
   end
@@ -81,10 +87,11 @@ describe Api::Web::PushSubscriptionsController do
 
       push_subscription = Web::PushSubscription.find_by(endpoint: create_payload[:subscription][:endpoint])
 
-      expect(push_subscription.data['alerts']['follow']).to eq(alerts_payload[:data][:alerts][:follow].to_s)
-      expect(push_subscription.data['alerts']['favourite']).to eq(alerts_payload[:data][:alerts][:favourite].to_s)
-      expect(push_subscription.data['alerts']['reblog']).to eq(alerts_payload[:data][:alerts][:reblog].to_s)
-      expect(push_subscription.data['alerts']['mention']).to eq(alerts_payload[:data][:alerts][:mention].to_s)
+      expect(push_subscription.data['policy']).to eq 'all'
+
+      %w(follow follow_request favourite reblog mention poll status).each do |type|
+        expect(push_subscription.data['alerts'][type]).to eq(alerts_payload[:data][:alerts][type.to_sym].to_s)
+      end
     end
   end
 end
diff --git a/spec/fabricators/canonical_email_block_fabricator.rb b/spec/fabricators/canonical_email_block_fabricator.rb
new file mode 100644
index 000000000..a0b6e0d22
--- /dev/null
+++ b/spec/fabricators/canonical_email_block_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:canonical_email_block) do
+  email "test@example.com"
+  reference_account { Fabricate(:account) }
+end
diff --git a/spec/fabricators/follow_recommendation_suppression_fabricator.rb b/spec/fabricators/follow_recommendation_suppression_fabricator.rb
new file mode 100644
index 000000000..4a6a07a66
--- /dev/null
+++ b/spec/fabricators/follow_recommendation_suppression_fabricator.rb
@@ -0,0 +1,3 @@
+Fabricator(:follow_recommendation_suppression) do
+  account
+end
diff --git a/spec/lib/spam_check_spec.rb b/spec/lib/spam_check_spec.rb
deleted file mode 100644
index 159d83257..000000000
--- a/spec/lib/spam_check_spec.rb
+++ /dev/null
@@ -1,192 +0,0 @@
-# frozen_string_literal: true
-
-require 'rails_helper'
-
-RSpec.describe SpamCheck do
-  let!(:sender) { Fabricate(:account) }
-  let!(:alice) { Fabricate(:account, username: 'alice') }
-  let!(:bob) { Fabricate(:account, username: 'bob') }
-
-  def status_with_html(text, options = {})
-    status = PostStatusService.new.call(sender, { text: text }.merge(options))
-    status.update_columns(text: Formatter.instance.format(status), local: false)
-    status
-  end
-
-  describe '#hashable_text' do
-    it 'removes mentions from HTML for remote statuses' do
-      status = status_with_html('@alice Hello')
-      expect(described_class.new(status).hashable_text).to eq 'hello'
-    end
-
-    it 'removes mentions from text for local statuses' do
-      status = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
-      expect(described_class.new(status).hashable_text).to eq 'hey , how are you?'
-    end
-  end
-
-  describe '#insufficient_data?' do
-    it 'returns true when there is no text' do
-      status = status_with_html('@alice')
-      expect(described_class.new(status).insufficient_data?).to be true
-    end
-
-    it 'returns false when there is text' do
-      status = status_with_html('@alice h')
-      expect(described_class.new(status).insufficient_data?).to be false
-    end
-  end
-
-  describe '#digest' do
-    it 'returns a string' do
-      status = status_with_html('@alice Hello world')
-      expect(described_class.new(status).digest).to be_a String
-    end
-  end
-
-  describe '#spam?' do
-    it 'returns false for a unique status' do
-      status = status_with_html('@alice Hello')
-      expect(described_class.new(status).spam?).to be false
-    end
-
-    it 'returns false for different statuses to the same recipient' do
-      status1 = status_with_html('@alice Hello')
-      described_class.new(status1).remember!
-      status2 = status_with_html('@alice Are you available to talk?')
-      expect(described_class.new(status2).spam?).to be false
-    end
-
-    it 'returns false for statuses with different content warnings' do
-      status1 = status_with_html('@alice Are you available to talk?')
-      described_class.new(status1).remember!
-      status2 = status_with_html('@alice Are you available to talk?', spoiler_text: 'This is a completely different matter than what I was talking about previously, I swear!')
-      expect(described_class.new(status2).spam?).to be false
-    end
-
-    it 'returns false for different statuses to different recipients' do
-      status1 = status_with_html('@alice How is it going?')
-      described_class.new(status1).remember!
-      status2 = status_with_html('@bob Are you okay?')
-      expect(described_class.new(status2).spam?).to be false
-    end
-
-    it 'returns false for very short different statuses to different recipients' do
-      status1 = status_with_html('@alice 🙄')
-      described_class.new(status1).remember!
-      status2 = status_with_html('@bob Huh?')
-      expect(described_class.new(status2).spam?).to be false
-    end
-
-    it 'returns false for statuses with no text' do
-      status1 = status_with_html('@alice')
-      described_class.new(status1).remember!
-      status2 = status_with_html('@bob')
-      expect(described_class.new(status2).spam?).to be false
-    end
-
-    it 'returns true for duplicate statuses to the same recipient' do
-      described_class::THRESHOLD.times do
-        status1 = status_with_html('@alice Hello')
-        described_class.new(status1).remember!
-      end
-
-      status2 = status_with_html('@alice Hello')
-      expect(described_class.new(status2).spam?).to be true
-    end
-
-    it 'returns true for duplicate statuses to different recipients' do
-      described_class::THRESHOLD.times do
-        status1 = status_with_html('@alice Hello')
-        described_class.new(status1).remember!
-      end
-
-      status2 = status_with_html('@bob Hello')
-      expect(described_class.new(status2).spam?).to be true
-    end
-
-    it 'returns true for nearly identical statuses with random numbers' do
-      source_text = 'Sodium, atomic number 11, was first isolated by Humphry Davy in 1807. A chemical component of salt, he named it Na in honor of the saltiest region on earth, North America.'
-
-      described_class::THRESHOLD.times do
-        status1 = status_with_html('@alice ' + source_text + ' 1234')
-        described_class.new(status1).remember!
-      end
-
-      status2 = status_with_html('@bob ' + source_text + ' 9568')
-      expect(described_class.new(status2).spam?).to be true
-    end
-  end
-
-  describe '#skip?' do
-    it 'returns true when the sender is already silenced' do
-      status = status_with_html('@alice Hello')
-      sender.silence!
-      expect(described_class.new(status).skip?).to be true
-    end
-
-    it 'returns true when the mentioned person follows the sender' do
-      status = status_with_html('@alice Hello')
-      alice.follow!(sender)
-      expect(described_class.new(status).skip?).to be true
-    end
-
-    it 'returns false when even one mentioned person doesn\'t follow the sender' do
-      status = status_with_html('@alice @bob Hello')
-      alice.follow!(sender)
-      expect(described_class.new(status).skip?).to be false
-    end
-
-    it 'returns true when the sender is replying to a status that mentions the sender' do
-      parent = PostStatusService.new.call(alice, text: "Hey @#{sender.username}, how are you?")
-      status = status_with_html('@alice @bob Hello', thread: parent)
-      expect(described_class.new(status).skip?).to be true
-    end
-  end
-
-  describe '#remember!' do
-    let(:status) { status_with_html('@alice') }
-    let(:spam_check) { described_class.new(status) }
-    let(:redis_key) { spam_check.send(:redis_key) }
-
-    it 'remembers' do
-      expect(Redis.current.exists?(redis_key)).to be true
-      spam_check.remember!
-      expect(Redis.current.exists?(redis_key)).to be true
-    end
-  end
-
-  describe '#reset!' do
-    let(:status) { status_with_html('@alice') }
-    let(:spam_check) { described_class.new(status) }
-    let(:redis_key) { spam_check.send(:redis_key) }
-
-    before do
-      spam_check.remember!
-    end
-
-    it 'resets' do
-      expect(Redis.current.exists?(redis_key)).to be true
-      spam_check.reset!
-      expect(Redis.current.exists?(redis_key)).to be false
-    end
-  end
-
-  describe '#flag!' do
-    let!(:status1) { status_with_html('@alice General Kenobi you are a bold one') }
-    let!(:status2) { status_with_html('@alice @bob General Kenobi, you are a bold one') }
-
-    before do
-      described_class.new(status1).remember!
-      described_class.new(status2).flag!
-    end
-
-    it 'creates a report about the account' do
-      expect(sender.targeted_reports.unresolved.count).to eq 1
-    end
-
-    it 'attaches both matching statuses to the report' do
-      expect(sender.targeted_reports.first.status_ids).to include(status1.id, status2.id)
-    end
-  end
-end
diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb
index e9a7aa934..2230f9710 100644
--- a/spec/lib/tag_manager_spec.rb
+++ b/spec/lib/tag_manager_spec.rb
@@ -83,40 +83,4 @@ RSpec.describe TagManager do
       expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
     end
   end
-
-  describe '#same_acct?' do
-    # The following comparisons MUST be case-insensitive.
-
-    it 'returns true if the needle has a correct username and domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@DoMaIn.Test')).to eq true
-    end
-
-    it 'returns false if the needle is missing a domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe')).to eq false
-    end
-
-    it 'returns false if the needle has an incorrect domain for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain.test', 'UsErNaMe@incorrect.test')).to eq false
-    end
-
-    it 'returns false if the needle has an incorrect username for remote user' do
-      expect(TagManager.instance.same_acct?('username@domain.test', 'incorrect@DoMaIn.test')).to eq false
-    end
-
-    it 'returns true if the needle has a correct username and domain for local user' do
-      expect(TagManager.instance.same_acct?('username', 'UsErNaMe@Cb6E6126.nGrOk.Io')).to eq true
-    end
-
-    it 'returns true if the needle is missing a domain for local user' do
-      expect(TagManager.instance.same_acct?('username', 'UsErNaMe')).to eq true
-    end
-
-    it 'returns false if the needle has an incorrect username for local user' do
-      expect(TagManager.instance.same_acct?('username', 'UsErNaM@Cb6E6126.nGrOk.Io')).to eq false
-    end
-
-    it 'returns false if the needle has an incorrect domain for local user' do
-      expect(TagManager.instance.same_acct?('username', 'incorrect@Cb6E6126.nGrOk.Io')).to eq false
-    end
-  end
 end
diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb
new file mode 100644
index 000000000..8e0050d65
--- /dev/null
+++ b/spec/models/canonical_email_block_spec.rb
@@ -0,0 +1,47 @@
+require 'rails_helper'
+
+RSpec.describe CanonicalEmailBlock, type: :model do
+  describe '#email=' do
+    let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' }
+
+    it 'sets canonical_email_hash' do
+      subject.email = 'test@example.com'
+      expect(subject.canonical_email_hash).to eq target_hash
+    end
+
+    it 'sets the same hash even with dot permutations' do
+      subject.email = 't.e.s.t@example.com'
+      expect(subject.canonical_email_hash).to eq target_hash
+    end
+
+    it 'sets the same hash even with extensions' do
+      subject.email = 'test+mastodon1@example.com'
+      expect(subject.canonical_email_hash).to eq target_hash
+    end
+
+    it 'sets the same hash with different casing' do
+      subject.email = 'Test@EXAMPLE.com'
+      expect(subject.canonical_email_hash).to eq target_hash
+    end
+  end
+
+  describe '.block?' do
+    let!(:canonical_email_block) { Fabricate(:canonical_email_block, email: 'foo@bar.com') }
+
+    it 'returns true for the same email' do
+      expect(described_class.block?('foo@bar.com')).to be true
+    end
+
+    it 'returns true for the same email with dots' do
+      expect(described_class.block?('f.oo@bar.com')).to be true
+    end
+
+    it 'returns true for the same email with extensions' do
+      expect(described_class.block?('foo+spam@bar.com')).to be true
+    end
+
+    it 'returns false for different email' do
+      expect(described_class.block?('hoge@bar.com')).to be false
+    end
+  end
+end
diff --git a/spec/models/follow_recommendation_suppression_spec.rb b/spec/models/follow_recommendation_suppression_spec.rb
new file mode 100644
index 000000000..39107a2b0
--- /dev/null
+++ b/spec/models/follow_recommendation_suppression_spec.rb
@@ -0,0 +1,4 @@
+require 'rails_helper'
+
+RSpec.describe FollowRecommendationSuppression, type: :model do
+end
diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb
index c6665611c..b44904369 100644
--- a/spec/models/web/push_subscription_spec.rb
+++ b/spec/models/web/push_subscription_spec.rb
@@ -1,16 +1,94 @@
 require 'rails_helper'
 
 RSpec.describe Web::PushSubscription, type: :model do
-  let(:alerts) { { mention: true, reblog: false, follow: true, follow_request: false, favourite: true } }
-  let(:push_subscription) { Web::PushSubscription.new(data: { alerts: alerts }) }
+  let(:account) { Fabricate(:account) }
+
+  let(:policy) { 'all' }
+
+  let(:data) do
+    {
+      policy: policy,
+
+      alerts: {
+        mention: true,
+        reblog: false,
+        follow: true,
+        follow_request: false,
+        favourite: true,
+      },
+    }
+  end
+
+  subject { described_class.new(data: data) }
 
   describe '#pushable?' do
-    it 'obeys alert settings' do
-      expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Mention'))).to eq true
-      expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Status'))).to eq false
-      expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Follow'))).to eq true
-      expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'FollowRequest'))).to eq false
-      expect(push_subscription.send(:pushable?, Notification.new(activity_type: 'Favourite'))).to eq true
+    let(:notification_type) { :mention }
+    let(:notification) { Fabricate(:notification, account: account, type: notification_type) }
+
+    %i(mention reblog follow follow_request favourite).each do |type|
+      context "when notification is a #{type}" do
+        let(:notification_type) { type }
+
+        it "returns boolean corresonding to alert setting" do
+          expect(subject.pushable?(notification)).to eq data[:alerts][type]
+        end
+      end
+    end
+
+    context 'when policy is all' do
+      let(:policy) { 'all' }
+
+      it 'returns true' do
+        expect(subject.pushable?(notification)).to eq true
+      end
+    end
+
+    context 'when policy is none' do
+      let(:policy) { 'none' }
+
+      it 'returns false' do
+        expect(subject.pushable?(notification)).to eq false
+      end
+    end
+
+    context 'when policy is followed' do
+      let(:policy) { 'followed' }
+
+      context 'and notification is from someone you follow' do
+        before do
+          account.follow!(notification.from_account)
+        end
+
+        it 'returns true' do
+          expect(subject.pushable?(notification)).to eq true
+        end
+      end
+
+      context 'and notification is not from someone you follow' do
+        it 'returns false' do
+          expect(subject.pushable?(notification)).to eq false
+        end
+      end
+    end
+
+    context 'when policy is follower' do
+      let(:policy) { 'follower' }
+
+      context 'and notification is from someone who follows you' do
+        before do
+          notification.from_account.follow!(account)
+        end
+
+        it 'returns true' do
+          expect(subject.pushable?(notification)).to eq true
+        end
+      end
+
+      context 'and notification is not from someone who follows you' do
+        it 'returns false' do
+          expect(subject.pushable?(notification)).to eq false
+        end
+      end
     end
   end
 end
diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb
index 53b355a57..f7d5e01bc 100644
--- a/spec/validators/blacklisted_email_validator_spec.rb
+++ b/spec/validators/blacklisted_email_validator_spec.rb
@@ -9,23 +9,36 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do
 
     before do
       allow(user).to receive(:valid_invitation?) { false }
-      allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email }
-      described_class.new.validate(user)
+      allow_any_instance_of(described_class).to receive(:blocked_email_provider?) { blocked_email }
     end
 
-    context 'blocked_email?' do
+    subject { described_class.new.validate(user); errors }
+
+    context 'when e-mail provider is blocked' do
       let(:blocked_email) { true }
 
-      it 'calls errors.add' do
-        expect(errors).to have_received(:add).with(:email, :blocked)
+      it 'adds error' do
+        expect(subject).to have_received(:add).with(:email, :blocked)
       end
     end
 
-    context '!blocked_email?' do
+    context 'when e-mail provider is not blocked' do
       let(:blocked_email) { false }
 
-      it 'not calls errors.add' do
-        expect(errors).not_to have_received(:add).with(:email, :blocked)
+      it 'does not add errors' do
+        expect(subject).not_to have_received(:add).with(:email, :blocked)
+      end
+
+      context 'when canonical e-mail is blocked' do
+        let(:other_user) { Fabricate(:user, email: 'i.n.f.o@mail.com') }
+
+        before do
+          other_user.account.suspend!
+        end
+
+        it 'adds error' do
+          expect(subject).to have_received(:add).with(:email, :taken)
+        end
       end
     end
   end
diff --git a/spec/workers/web/push_notification_worker_spec.rb b/spec/workers/web/push_notification_worker_spec.rb
new file mode 100644
index 000000000..5bc24f888
--- /dev/null
+++ b/spec/workers/web/push_notification_worker_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Web::PushNotificationWorker do
+  subject { described_class.new }
+
+  let(:p256dh) { 'BN4GvZtEZiZuqFxSKVZfSfluwKBD7UxHNBmWkfiZfCtgDE8Bwh-_MtLXbBxTBAWH9r7IPKL0lhdcaqtL1dfxU5E=' }
+  let(:auth) { 'Q2BoAjC09xH3ywDLNJr-dA==' }
+  let(:endpoint) { 'https://updates.push.services.mozilla.com/push/v1/subscription-id' }
+  let(:user) { Fabricate(:user) }
+  let(:notification) { Fabricate(:notification) }
+  let(:subscription) { Fabricate(:web_push_subscription, user_id: user.id, key_p256dh: p256dh, key_auth: auth, endpoint: endpoint, data: { alerts: { notification.type => true } }) }
+  let(:vapid_public_key) { 'BB37UCyc8LLX4PNQSe-04vSFvpUWGrENubUaslVFM_l5TxcGVMY0C3RXPeUJAQHKYlcOM2P4vTYmkoo0VZGZTM4=' }
+  let(:vapid_private_key) { 'OPrw1Sum3gRoL4-DXfSCC266r-qfFSRZrnj8MgIhRHg=' }
+  let(:vapid_key) { Webpush::VapidKey.from_keys(vapid_public_key, vapid_private_key) }
+  let(:contact_email) { 'sender@example.com' }
+  let(:ciphertext) { "+\xB8\xDBT}\x13\xB6\xDD.\xF9\xB0\xA7\xC8\xD2\x80\xFD\x99#\xF7\xAC\x83\xA4\xDB,\x1F\xB5\xB9w\x85>\xF7\xADr" }
+  let(:salt) { "X\x97\x953\xE4X\xF8_w\xE7T\x95\xC51q\xFE" }
+  let(:server_public_key) { "\x04\b-RK9w\xDD$\x16lFz\xF9=\xB4~\xC6\x12k\xF3\xF40t\xA9\xC1\fR\xC3\x81\x80\xAC\f\x7F\xE4\xCC\x8E\xC2\x88 n\x8BB\xF1\x9C\x14\a\xFA\x8D\xC9\x80\xA1\xDDyU\\&c\x01\x88#\x118Ua" }
+  let(:shared_secret) { "\t\xA7&\x85\t\xC5m\b\xA8\xA7\xF8B{1\xADk\xE1y'm\xEDE\xEC\xDD\xEDj\xB3$s\xA9\xDA\xF0" }
+  let(:payload) { { ciphertext: ciphertext, salt: salt, server_public_key: server_public_key, shared_secret: shared_secret } }
+
+  describe 'perform' do
+    before do
+      allow_any_instance_of(subscription.class).to receive(:contact_email).and_return(contact_email)
+      allow_any_instance_of(subscription.class).to receive(:vapid_key).and_return(vapid_key)
+      allow(Webpush::Encryption).to receive(:encrypt).and_return(payload)
+      allow(JWT).to receive(:encode).and_return('jwt.encoded.payload')
+
+      stub_request(:post, endpoint).to_return(status: 201, body: '')
+
+      subject.perform(subscription.id, notification.id)
+    end
+
+    it 'calls the relevant service with the correct headers' do
+      expect(a_request(:post, endpoint).with(headers: {
+        'Content-Encoding' => 'aesgcm',
+        'Content-Type' => 'application/octet-stream',
+        'Crypto-Key' => 'dh=BAgtUks5d90kFmxGevk9tH7GEmvz9DB0qcEMUsOBgKwMf-TMjsKIIG6LQvGcFAf6jcmAod15VVwmYwGIIxE4VWE;p256ecdsa=' + vapid_public_key.delete('='),
+        'Encryption' => 'salt=WJeVM-RY-F9351SVxTFx_g',
+        'Ttl' => '172800',
+        'Urgency' => 'normal',
+        'Authorization' => 'WebPush jwt.encoded.payload',
+      }, body: "+\xB8\xDBT}\u0013\xB6\xDD.\xF9\xB0\xA7\xC8Ҁ\xFD\x99#\xF7\xAC\x83\xA4\xDB,\u001F\xB5\xB9w\x85>\xF7\xADr")).to have_been_made
+    end
+  end
+end