diff --git a/app/assets/javascripts/discourse/app/components/invite-panel.js b/app/assets/javascripts/discourse/app/components/invite-panel.js
index 7a77a6d2e8b..dd55ebeed29 100644
--- a/app/assets/javascripts/discourse/app/components/invite-panel.js
+++ b/app/assets/javascripts/discourse/app/components/invite-panel.js
@@ -317,6 +317,11 @@ export default Component.extend({
     });
   },
 
+  @action
+  sendCloseModal() {
+    this.attrs.close();
+  },
+
   @action
   createInvite() {
     if (this.disabled) {
@@ -388,7 +393,7 @@ export default Component.extend({
   },
 
   @action
-  generateInvitelink() {
+  generateInviteLink() {
     if (this.disabled) {
       return;
     }
diff --git a/app/assets/javascripts/discourse/app/templates/components/invite-panel.hbs b/app/assets/javascripts/discourse/app/templates/components/invite-panel.hbs
index d5cfd1e3494..f78860493dc 100644
--- a/app/assets/javascripts/discourse/app/templates/components/invite-panel.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/invite-panel.hbs
@@ -87,7 +87,7 @@
   {{#if inviteModel.finished}}
     {{d-button
       class="btn-primary"
-      action=(route-action "closeModal")
+      action=(action "sendCloseModal")
       label="close"}}
   {{else}}
     {{d-button
@@ -99,7 +99,7 @@
     {{#if showCopyInviteButton}}
       {{d-button
         icon="link"
-        action=(action "generateInvitelink")
+        action=(action "generateInviteLink")
         class="btn-primary generate-invite-link"
         disabled=disabledCopyLink
         label="user.invited.generate_link"}}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js b/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js
new file mode 100644
index 00000000000..8d161d20226
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/integration/components/invite-panel-test.js
@@ -0,0 +1,56 @@
+import { set } from "@ember/object";
+import { click, fillIn } from "@ember/test-helpers";
+import User from "discourse/models/user";
+import componentTest, {
+  setupRenderingTest,
+} from "discourse/tests/helpers/component-test";
+import pretender from "discourse/tests/helpers/create-pretender";
+import {
+  discourseModule,
+  queryAll,
+} from "discourse/tests/helpers/qunit-helpers";
+import selectKit from "discourse/tests/helpers/select-kit-helper";
+import hbs from "htmlbars-inline-precompile";
+
+discourseModule("Integration | Component | invite-panel", function (hooks) {
+  setupRenderingTest(hooks);
+
+  componentTest("shows the invite link after it is generated", {
+    template: hbs`{{invite-panel panel=panel}}`,
+
+    beforeEach() {
+      pretender.get("/u/search/users", () => {
+        return [200, { "Content-Type": "application/json" }, { users: [] }];
+      });
+
+      pretender.post("/invites", () => {
+        return [
+          200,
+          { "Content-Type": "application/json" },
+          {
+            link: "http://example.com/invites/92c297e886a0ca03089a109ccd6be155",
+          },
+        ];
+      });
+
+      set(this.currentUser, "details", { can_invite_via_email: true });
+      this.set("panel", {
+        id: "invite",
+        model: { inviteModel: User.create(this.currentUser) },
+      });
+    },
+
+    async test(assert) {
+      const input = selectKit(".invite-user-input");
+      await input.expand();
+      await fillIn(".invite-user-input .filter-input", "eviltrout@example.com");
+      await input.selectRowByValue("eviltrout@example.com");
+      assert.ok(queryAll(".send-invite:disabled").length === 0);
+      await click(".generate-invite-link");
+      assert.equal(
+        find(".invite-link-input")[0].value,
+        "http://example.com/invites/92c297e886a0ca03089a109ccd6be155"
+      );
+    },
+  });
+});
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index ed4ee4d59b5..0e6a2b4f2b6 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -247,7 +247,6 @@ class InvitesController < ApplicationController
     raise Discourse::InvalidParameters.new(:email) if invite.blank?
     invite.resend_invite
     render json: success_json
-
   rescue RateLimiter::LimitExceeded
     render_json_error(I18n.t("rate_limiter.slow_down"))
   end
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 749bb8512c8..109b767c535 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -70,7 +70,7 @@ class Invite < ActiveRecord::Base
   end
 
   def redeemable?
-    !redeemed? && !expired? && !destroyed? && link_valid?
+    !redeemed? && !expired? && !deleted_at? && !destroyed? && link_valid?
   end
 
   def redeemed?
diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb
index bda281c9309..35875a3fde2 100644
--- a/spec/models/invite_spec.rb
+++ b/spec/models/invite_spec.rb
@@ -3,194 +3,90 @@
 require 'rails_helper'
 
 describe Invite do
+  fab!(:user) { Fabricate(:user) }
 
-  it { is_expected.to validate_presence_of :invited_by_id }
+  context 'validators' do
+    it { is_expected.to validate_presence_of :invited_by_id }
+    it { is_expected.to rate_limit }
 
-  it { is_expected.to rate_limit }
-
-  let(:iceking) { 'iceking@adventuretime.ooo' }
-
-  context 'user validators' do
-    fab!(:coding_horror) { Fabricate(:coding_horror) }
-    fab!(:user) { Fabricate(:user) }
-    let(:invite) { Invite.create(email: user.email, invited_by: coding_horror) }
-
-    it "should not allow an invite with the same email as an existing user" do
-      expect(invite).not_to be_valid
-    end
-
-    it "should not allow a user to invite themselves" do
-      expect(invite.email_already_exists).to eq(true)
-    end
-  end
-
-  context 'email validators' do
-    fab!(:coding_horror) { Fabricate(:coding_horror) }
-
-    it "should not allow an invite with unformatted email address" do
-      invite = Fabricate.build(:invite, email: "John Doe <john.doe@example.com>")
-      expect(invite.valid?).to eq(false)
-      expect(invite.errors.details[:email].first[:error]).to eq(I18n.t("user.email.invalid"))
-    end
-
-    it "should not allow an invite with blocklisted email" do
-      invite = Invite.create(email: "test@mailinator.com", invited_by: coding_horror)
-      expect(invite).not_to be_valid
-    end
-
-    it "should allow an invite with non-blocklisted email" do
-      invite = Fabricate(:invite, email: "test@mail.com", invited_by: coding_horror)
+    it 'allows invites with valid emails' do
+      invite = Fabricate.build(:invite, email: 'test@example.com', invited_by: user)
       expect(invite).to be_valid
     end
 
-    it "should not allow an invalid email address" do
+    it 'does not allow invites with invalid emails' do
+      invite = Fabricate.build(:invite, email: 'John Doe <john.doe@example.com>')
+      expect(invite.valid?).to eq(false)
+      expect(invite.errors.details[:email].first[:error]).to eq(I18n.t('user.email.invalid'))
+    end
+
+    it 'does not allow an invite with the same email as an existing user' do
+      invite = Fabricate.build(:invite, email: Fabricate(:user).email, invited_by: user)
+      expect(invite).not_to be_valid
+
+      invite = Fabricate.build(:invite, email: user.email, invited_by: user)
+      expect(invite).not_to be_valid
+    end
+
+    it 'does not allow an invite with blocked email' do
+      invite = Invite.create(email: 'test@mailinator.com', invited_by: user)
+      expect(invite).not_to be_valid
+    end
+
+    it 'does not allow an invalid email address' do
       invite = Fabricate.build(:invite, email: 'asjdso')
       expect(invite.valid?).to eq(false)
-      expect(invite.errors.details[:email].first[:error]).to eq(I18n.t("user.email.invalid"))
+      expect(invite.errors.details[:email].first[:error]).to eq(I18n.t('user.email.invalid'))
     end
   end
 
-  context '#create' do
-    context 'saved' do
-      subject { Fabricate(:invite) }
+  context '::generate' do
+    it 'saves an invites' do
+      invite = Invite.generate(user, email: 'TEST@EXAMPLE.COM')
+      expect(invite.invite_key).to be_present
+      expect(invite.email).to eq('test@example.com')
+    end
 
-      it "works" do
-        expect(subject.invite_key).to be_present
-        expect(subject.email_already_exists).to eq(false)
+    it 'can succeed for staged users emails' do
+      Fabricate(:staged, email: 'test@example.com')
+      invite = Invite.generate(user, email: 'test@example.com')
+      expect(invite.email).to eq('test@example.com')
+    end
+
+    it 'raises an error when inviting an existing user' do
+      expect { Invite.generate(user, email: user.email) }
+        .to raise_error(Invite::UserExists)
+    end
+
+    context 'via email' do
+      it 'enqueues a job to email the invite' do
+        invite = Invite.generate(user, email: 'test@example.com')
+        expect(invite.emailed_status).to eq(Invite.emailed_status_types[:sending])
+        expect(Jobs::InviteEmail.jobs.size).to eq(1)
       end
 
-      it 'should store a lower case version of the email' do
-        expect(subject.email).to eq(iceking)
+      it 'can skip the job to email the invite' do
+        invite = Invite.generate(user, email: 'test@example.com', skip_email: true)
+        expect(invite.emailed_status).to eq(Invite.emailed_status_types[:not_required])
+        expect(Jobs::InviteEmail.jobs.size).to eq(0)
+      end
+
+      it 'can invite the same user after their invite was destroyed' do
+        Invite.generate(user, email: 'test@example.com').destroy!
+        invite = Invite.generate(user, email: 'test@example.com')
+        expect(invite).to be_present
       end
     end
 
-    context 'to a topic' do
-      fab!(:topic) { Fabricate(:topic) }
-      let(:inviter) { topic.user }
-
-      context 'email' do
-        it 'enqueues a job to email the invite' do
-          expect do
-            Invite.generate(inviter, email: iceking, topic: topic)
-          end.to change { Jobs::InviteEmail.jobs.size }
-        end
+    context 'via link' do
+      it 'does not enqueue a job to email the invite' do
+        invite = Invite.generate(user, skip_email: true)
+        expect(invite.emailed_status).to eq(Invite.emailed_status_types[:not_required])
+        expect(Jobs::InviteEmail.jobs.size).to eq(0)
       end
 
-      context 'links' do
-        it 'does not enqueue a job to email the invite' do
-          expect { Invite.generate(inviter, email: iceking, topic: topic, skip_email: true) }
-            .not_to change { Jobs::InviteEmail.jobs.size }
-        end
-      end
-
-      context 'destroyed' do
-        it "can invite the same user after their invite was destroyed" do
-          Invite.generate(inviter, email: iceking, topic: topic).destroy!
-          invite = Invite.generate(inviter, email: iceking, topic: topic)
-          expect(invite).to be_present
-        end
-      end
-
-      context 'after created' do
-        let(:invite) { Invite.generate(inviter, email: iceking, topic: topic) }
-
-        it 'belongs to the topic' do
-          expect(topic.invites).to eq([invite])
-          expect(invite.topics).to eq([topic])
-        end
-
-        context 'when added by another user' do
-          fab!(:coding_horror) { Fabricate(:coding_horror) }
-
-          let(:new_invite) do
-            Invite.generate(coding_horror, email: iceking, topic: topic)
-          end
-
-          it 'returns a different invite' do
-            expect(new_invite).not_to eq(invite)
-            expect(new_invite.invite_key).not_to eq(invite.invite_key)
-            expect(new_invite.topics).to eq([topic])
-          end
-        end
-
-        context 'when adding a duplicate' do
-          it 'returns the original invite' do
-            %w{
-              iceking@adventuretime.ooo
-              iceking@ADVENTURETIME.ooo
-              ICEKING@adventuretime.ooo
-            }.each do |email|
-              expect(Invite.generate(inviter, email: email, topic: topic)).to eq(invite)
-            end
-          end
-
-          it 'updates timestamp of existing invite' do
-            freeze_time
-            invite.update!(created_at: 10.days.ago)
-
-            resend_invite = Invite.generate(inviter, email: 'iceking@adventuretime.ooo', topic: topic)
-
-            expect(resend_invite.created_at).to eq_time(Time.zone.now)
-          end
-
-          it 'returns a new invite if the other has expired' do
-            SiteSetting.invite_expiry_days = 1
-            invite.update!(expires_at: 2.days.ago)
-
-            new_invite = Invite.generate(inviter, email: 'iceking@adventuretime.ooo', topic: topic)
-            expect(new_invite).not_to eq(invite)
-            expect(new_invite).not_to be_expired
-          end
-        end
-
-        context 'when adding to another topic' do
-          fab!(:another_topic) { Fabricate(:topic, user: topic.user) }
-
-          it 'should be the same invite' do
-            new_invite = Invite.generate(inviter, email: iceking, topic: another_topic)
-            expect(new_invite).to eq(invite)
-            expect(another_topic.invites).to eq([invite])
-            expect(invite.topics).to match_array([topic, another_topic])
-          end
-        end
-
-        it 'resets expiry of a resent invite' do
-          SiteSetting.invite_expiry_days = 2
-          invite.update!(invalidated_at: 10.days.ago, expires_at: 10.days.ago)
-          expect(invite).to be_expired
-
-          invite.resend_invite
-          expect(invite.invalidated_at).to be_nil
-          expect(invite).not_to be_expired
-        end
-
-        it 'correctly marks invite emailed_status for email invites' do
-          expect(invite.emailed_status).to eq(Invite.emailed_status_types[:sending])
-
-          Invite.generate(inviter, email: iceking, topic: topic)
-          expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:sending])
-        end
-
-        it 'does not mark emailed_status as sending after generating invite link' do
-          expect(invite.emailed_status).to eq(Invite.emailed_status_types[:sending])
-
-          Invite.generate(inviter, email: iceking, topic: topic, emailed_status: Invite.emailed_status_types[:not_required])
-          expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
-
-          Invite.generate(inviter, email: iceking, topic: topic)
-          expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
-
-          Invite.generate(inviter, email: iceking, topic: topic, emailed_status: Invite.emailed_status_types[:not_required])
-          expect(invite.reload.emailed_status).to eq(Invite.emailed_status_types[:not_required])
-        end
-      end
-    end
-
-    context 'invite links' do
-      let(:inviter) { Fabricate(:user) }
-
-      it "can be created" do
-        invite = Invite.generate(inviter, max_redemptions_allowed: 5)
+      it 'can be created' do
+        invite = Invite.generate(user, max_redemptions_allowed: 5)
         expect(invite.max_redemptions_allowed).to eq(5)
         expect(invite.expires_at.to_date).to eq(SiteSetting.invite_expiry_days.days.from_now.to_date)
         expect(invite.emailed_status).to eq(Invite.emailed_status_types[:not_required])
@@ -198,342 +94,285 @@ describe Invite do
       end
 
       it 'checks for max_redemptions_allowed range' do
-        SiteSetting.invite_link_max_redemptions_limit = 1000
-        expect { Invite.generate(inviter, max_redemptions_allowed: 1001) }
-          .to raise_error(ActiveRecord::RecordInvalid)
+        SiteSetting.invite_link_max_redemptions_limit_users = 3
+        expect { Invite.generate(user, max_redemptions_allowed: 4) }.to raise_error(ActiveRecord::RecordInvalid)
+
+        SiteSetting.invite_link_max_redemptions_limit = 3
+        expect { Invite.generate(Fabricate(:admin), max_redemptions_allowed: 4) }.to raise_error(ActiveRecord::RecordInvalid)
+      end
+    end
+
+    context 'when sending an invite to the same user' do
+      fab!(:invite) { Invite.generate(user, email: 'test@example.com') }
+
+      it 'returns the original invite' do
+        %w{test@EXAMPLE.com TEST@example.com}.each do |email|
+          expect(Invite.generate(user, email: email)).to eq(invite)
+        end
       end
 
-      it 'does not enqueue a job to email the invite' do
-        expect { Invite.generate(inviter) }
-          .not_to change { Jobs::InviteEmail.jobs.size }
+      it 'updates timestamp of existing invite' do
+        freeze_time
+        invite.update!(created_at: 10.days.ago)
+        resend_invite = Invite.generate(user, email: 'test@example.com')
+        expect(resend_invite).to eq(invite)
+        expect(resend_invite.created_at).to eq_time(Time.zone.now)
+      end
+
+      it 'returns a new invite if the other has expired' do
+        SiteSetting.invite_expiry_days = 1
+        invite.update!(expires_at: 2.days.ago)
+
+        new_invite = Invite.generate(user, email: 'test@example.com')
+        expect(new_invite).not_to eq(invite)
+        expect(new_invite).not_to be_expired
+      end
+
+      it 'returns a new invite when invited by a different user' do
+        invite = Invite.generate(user, email: 'test@example.com')
+        expect(invite.email).to eq('test@example.com')
+
+        another_invite = Invite.generate(Fabricate(:user), email: 'test@example.com')
+        expect(another_invite.email).to eq('test@example.com')
+
+        expect(invite.invite_key).not_to eq(another_invite.invite_key)
+      end
+    end
+
+    context 'invite to a topic' do
+      fab!(:topic) { Fabricate(:topic) }
+      let(:invite) { Invite.generate(topic.user, email: 'test@example.com', topic: topic) }
+
+      it 'belongs to the topic' do
+        expect(topic.invites).to contain_exactly(invite)
+        expect(invite.topics).to contain_exactly(topic)
+      end
+
+      context 'when adding to another topic' do
+        fab!(:another_topic) { Fabricate(:topic, user: topic.user) }
+
+        it 'should be the same invite' do
+          new_invite = Invite.generate(topic.user, email: 'test@example.com', topic: another_topic)
+          expect(invite).to eq(new_invite)
+          expect(invite.topics).to contain_exactly(topic, another_topic)
+          expect(topic.invites).to contain_exactly(invite)
+          expect(another_topic.invites).to contain_exactly(invite)
+        end
       end
     end
   end
 
-  context 'an existing user' do
-    fab!(:topic) { Fabricate(:topic, category_id: nil, archetype: 'private_message') }
-    fab!(:coding_horror) { Fabricate(:coding_horror) }
-
-    it "raises the right error" do
-      expect { Invite.generate(topic.user, email: coding_horror.email, topic: topic) }
-        .to raise_error(Invite::UserExists)
-    end
-  end
-
-  context 'a staged user' do
-    it 'creates an invite for a staged user' do
-      Fabricate(:staged, email: 'staged@account.com')
-      invite = Invite.generate(Fabricate(:coding_horror), email: 'staged@account.com')
-
-      expect(invite).to be_valid
-      expect(invite.email).to eq('staged@account.com')
-    end
-  end
-
-  context '.redeem' do
-
+  context '#redeem' do
     fab!(:invite) { Fabricate(:invite) }
 
+    it 'works' do
+      user = invite.redeem
+      expect(invite.invited_users.map(&:user)).to contain_exactly(user)
+      expect(user.is_a?(User)).to eq(true)
+      expect(user.trust_level).to eq(SiteSetting.default_invitee_trust_level)
+      expect(user.send_welcome_message).to eq(true)
+
+      expect(invite.reload.redemption_count).to eq(1)
+      expect(invite.redeem).to be_blank
+    end
+
     it 'creates a notification for the invitee' do
-      expect { invite.redeem }.to change(Notification, :count)
+      expect { invite.redeem }.to change { Notification.count }
     end
 
-    it 'wont redeem an expired invite' do
-      SiteSetting.invite_expiry_days = 10
-      invite.update_column(:expires_at, 20.days.ago)
+    it 'does not work with expired invites' do
+      invite.update!(expires_at: 1.day.ago)
       expect(invite.redeem).to be_blank
     end
 
-    it 'wont redeem a deleted invite' do
-      invite.destroy
+    it 'does not work with deleted invites' do
+      invite.trash!
       expect(invite.redeem).to be_blank
     end
 
-    it "won't redeem an invalidated invite" do
-      invite.invalidated_at = 1.day.ago
+    it 'does not work with deleted invites' do
+      invite.destroy!
       expect(invite.redeem).to be_blank
     end
 
-    context "deletes duplicate invites" do
-      fab!(:another_user) { Fabricate(:user) }
-
-      it 'delete duplicate invite' do
-        another_invite = Fabricate(:invite, email: invite.email, invited_by: another_user)
-        invite.redeem
-        duplicate_invite = Invite.find_by(id: another_invite.id)
-        expect(duplicate_invite).to be_nil
-      end
-
-      it 'does not delete already redeemed invite' do
-        redeemed_invite = Fabricate(:invite, email: invite.email, invited_by: another_user)
-        Fabricate(:invited_user, invite: invite, user: Fabricate(:user))
-        invite.redeem
-        used_invite = Invite.find_by(id: redeemed_invite.id)
-        expect(used_invite).not_to be_nil
-      end
+    it 'does not work with invalidated invites' do
+      invite.update(invalidated_at: 1.day.ago)
+      expect(invite.redeem).to be_blank
     end
 
-    context "as a moderator" do
-      it "will give the user a moderator flag" do
-        invite.invited_by = Fabricate(:admin)
-        invite.moderator = true
-        invite.save
+    it 'deletes duplicate invite' do
+      another_invite = Fabricate(:invite, email: invite.email, invited_by: Fabricate(:user))
+      another_redeemed_invite = Fabricate(:invite, email: invite.email, invited_by: Fabricate(:user))
+      Fabricate(:invited_user, invite: another_redeemed_invite)
+
+      user = invite.redeem
+      expect(user).not_to eq(nil)
+      expect(Invite.find_by(id: another_invite.id)).to eq(nil)
+      expect(Invite.find_by(id: another_redeemed_invite.id)).not_to eq(nil)
+    end
+
+    context 'as a moderator' do
+      it 'will give the user a moderator flag' do
+        invite.update!(moderator: true, invited_by: Fabricate(:admin))
 
         user = invite.redeem
         expect(user).to be_moderator
       end
 
-      it "will not give the user a moderator flag if the inviter is not staff" do
-        invite.moderator = true
-        invite.save
+      it 'will not give the user a moderator flag if the inviter is not staff' do
+        invite.update!(moderator: true)
 
         user = invite.redeem
         expect(user).not_to be_moderator
       end
     end
 
-    context "when inviting to groups" do
-      it "add the user to the correct groups" do
+    context 'when inviting to groups' do
+      it 'add the user to the correct groups' do
         group = Fabricate(:group)
         group.add_owner(invite.invited_by)
-        invite.invited_groups.build(group_id: group.id)
-        invite.save
+        invite.invited_groups.create!(group_id: group.id)
 
         user = invite.redeem
-        expect(user.groups.count).to eq(1)
+        expect(user.groups).to contain_exactly(group)
       end
     end
 
-    context "invite trust levels" do
-      it "returns the trust level in default_invitee_trust_level" do
-        SiteSetting.default_invitee_trust_level = TrustLevel[3]
-        expect(invite.redeem.trust_level).to eq(TrustLevel[3])
-      end
+    it 'activates user when must_approve_users? is enabled' do
+      SiteSetting.must_approve_users = true
+      invite.invited_by = Fabricate(:admin)
+
+      user = invite.redeem
+      expect(user.approved?).to eq(true)
     end
 
-    context 'inviting when must_approve_users? is enabled' do
-      it 'correctly activates accounts' do
-        invite.invited_by = Fabricate(:admin)
-        SiteSetting.must_approve_users = true
-        user = invite.redeem
-        expect(user.approved?).to eq(true)
-      end
-    end
+    context 'invite to a topic' do
+      fab!(:topic) { Fabricate(:private_message_topic) }
+      fab!(:another_topic) { Fabricate(:private_message_topic) }
 
-    context 'simple invite' do
-      let!(:user) { invite.redeem }
-
-      it 'works correctly' do
-        expect(user.is_a?(User)).to eq(true)
-        expect(user.send_welcome_message).to eq(true)
-        expect(user.trust_level).to eq(SiteSetting.default_invitee_trust_level)
+      before do
+        invite.topic_invites.create!(topic: topic)
       end
 
-      context 'after redeeming' do
-        before do
-          invite.reload
-        end
-
-        it 'works correctly' do
-          # has set the user_id attribute
-          expect(invite.invited_users.first.user).to eq(user)
-
-          # returns true for redeemed
-          expect(invite).to be_redeemed
-        end
-
-        context 'again' do
-          it 'will not redeem twice' do
-            expect(invite.redeem).to be_blank
-          end
-        end
-      end
-    end
-
-    context 'invited to topics' do
-      fab!(:tl2_user) { Fabricate(:user, trust_level: 2) }
-      fab!(:topic) { Fabricate(:private_message_topic, user: tl2_user) }
-
-      let!(:invite) do
-        topic.invite(topic.user, 'jake@adventuretime.ooo')
-        Invite.find_by(invited_by_id: topic.user)
-      end
-
-      context 'redeem topic invite' do
-        it 'adds the user to the topic_users' do
-          user = invite.redeem
-          topic.reload
-          expect(topic.allowed_users.include?(user)).to eq(true)
-          expect(Guardian.new(user).can_see?(topic)).to eq(true)
-        end
-      end
-
-      context 'invited by another user to the same topic' do
-        fab!(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
-        let!(:another_invite) { topic.invite(another_tl2_user, 'jake@adventuretime.ooo') }
-        let!(:user) { invite.redeem }
-
-        it 'adds the user to the topic_users' do
-          topic.reload
-          expect(topic.allowed_users.include?(user)).to eq(true)
-        end
-      end
-
-      context 'invited by another user to a different topic' do
-        let!(:user) { invite.redeem }
-        fab!(:another_tl2_user) { Fabricate(:user, trust_level: 2) }
-        fab!(:another_topic) { Fabricate(:topic, user: another_tl2_user) }
-
-        it 'adds the user to the topic_users of the first topic' do
-          expect(another_topic.invite(another_tl2_user, user.username)).to be_truthy # invited via username
-          expect(topic.allowed_users.include?(user)).to eq(true)
-        end
-      end
-    end
-
-    context 'invite_link' do
-      fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) }
-
-      it 'works correctly' do
-        user = invite_link.redeem(email: 'foo@example.com')
-        expect(user.is_a?(User)).to eq(true)
-        expect(user.send_welcome_message).to eq(true)
-        expect(user.trust_level).to eq(SiteSetting.default_invitee_trust_level)
-        expect(user.active).to eq(false)
-        expect(invite_link.reload.redemption_count).to eq(1)
-      end
-
-      it 'returns error if user with that email already exists' do
-        user = Fabricate(:user)
-        expect { invite_link.redeem(email: user.email) }.to raise_error(Invite::UserExists)
+      it 'adds the user to topic_users' do
+        invited_user = invite.redeem
+        expect(invited_user).not_to eq(nil)
+        expect(topic.reload.allowed_users.include?(invited_user)).to eq(true)
+        expect(Guardian.new(invited_user).can_see?(topic)).to eq(true)
       end
     end
   end
 
-  describe '.pending' do
-    context 'with user that has invited' do
-      it 'returns invites' do
-        inviter = Fabricate(:user)
-        invite = Fabricate(:invite, invited_by: inviter)
-
-        expect(Invite.pending(inviter)).to include(invite)
-      end
-    end
-
-    context 'with user that has not invited' do
-      it 'does not return invites' do
-        user = Fabricate(:user)
-        Fabricate(:invite)
-
-        expect(Invite.pending(user)).to be_empty
-      end
-    end
-
-    it 'returns pending invites only' do
-      inviter = Fabricate(:user)
-
-      redeemed_invite = Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com')
-      redeemed_invite.redeem
-
-      pending_invite = Fabricate(:invite, invited_by: inviter, email: 'pending@example.com')
-      pending_link_invite = Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 5)
-
-      expired_invite = Fabricate(:invite, invited_by: inviter, email: 'expired@example.com', expires_at: 1.day.ago)
-
-      expect(Invite.pending(inviter)).to contain_exactly(pending_invite, pending_link_invite)
-    end
-  end
-
-  describe '.redeemed_users' do
-    it 'returns redeemed invites only' do
-      inviter = Fabricate(:user)
-
-      Fabricate(:invite, invited_by: inviter, email: 'pending@example.com')
-
-      redeemed_invite = Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com')
-      Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
-
-      expect(Invite.redeemed_users(inviter)).to contain_exactly(redeemed_invite.invited_users.first)
-    end
-
-    it 'returns redeemed invites even if trashed' do
-      inviter = Fabricate(:user)
-      redeemed_invite = Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com')
-      Fabricate(:invited_user, invite: redeemed_invite, user: Fabricate(:user))
-      redeemed_invite.trash!
-
-      expect(Invite.redeemed_users(inviter)).to contain_exactly(redeemed_invite.invited_users.first)
-    end
-
-    it 'returns redeemed invites for invite links' do
-      inviter = Fabricate(:user)
-      invite_link = Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 5)
-
-      redeemed = [
-        Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user)),
-        Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user)),
-        Fabricate(:invited_user, invite: invite_link, user: Fabricate(:user))
-      ]
-
-      expect(Invite.redeemed_users(inviter)).to match_array(redeemed)
-    end
-  end
-
-  describe '.invalidate_for_email' do
-    let(:email) { 'invite.me@example.com' }
-    subject { described_class.invalidate_for_email(email) }
-
-    it 'returns nil if there is no invite for the given email' do
-      expect(subject).to eq(nil)
-    end
-
-    it 'sets the matching invite to be invalid' do
-      invite = Fabricate(:invite, invited_by: Fabricate(:user), email: email)
-      expect(subject).to eq(invite)
-      expect(subject.link_valid?).to eq(false)
-      expect(subject).to be_valid
-    end
-
-    it 'sets the matching invite to be invalid without being case-sensitive' do
-      invite = Fabricate(:invite, invited_by: Fabricate(:user), email: 'invite.me2@Example.COM')
-      result = described_class.invalidate_for_email('invite.me2@EXAMPLE.com')
-      expect(result).to eq(invite)
-      expect(result.link_valid?).to eq(false)
-      expect(result).to be_valid
-    end
-  end
-
-  describe '.redeem_from_email' do
-    fab!(:inviter) { Fabricate(:user) }
-    fab!(:invite) { Fabricate(:invite, invited_by: inviter, email: 'test@example.com') }
+  describe '#redeem_from_email' do
+    fab!(:invite) { Fabricate(:invite, email: 'test@example.com') }
     fab!(:user) { Fabricate(:user, email: invite.email) }
 
     it 'redeems the invite from email' do
       Invite.redeem_from_email(user.email)
-      invite.reload
-      expect(invite).to be_redeemed
+      expect(invite.reload).to be_redeemed
     end
 
     it 'does not redeem the invite if email does not match' do
-      Invite.redeem_from_email('test24@example.com')
-      invite.reload
-      expect(invite).not_to be_redeemed
+      Invite.redeem_from_email('test2@example.com')
+      expect(invite.reload).not_to be_redeemed
     end
   end
 
-  describe '#emailed_status_types' do
-    context "verify enum sequence" do
-      before do
-        @emailed_status_types = Invite.emailed_status_types
+  context 'scopes' do
+    fab!(:inviter) { Fabricate(:user) }
+
+    fab!(:pending_invite) { Fabricate(:invite, invited_by: inviter, email: 'pending@example.com') }
+    fab!(:pending_link_invite) { Fabricate(:invite, invited_by: inviter, max_redemptions_allowed: 5) }
+    fab!(:pending_invite_from_another_user) { Fabricate(:invite) }
+
+    fab!(:expired_invite) { Fabricate(:invite, invited_by: inviter, email: 'expired@example.com', expires_at: 1.day.ago) }
+
+    fab!(:redeemed_invite) { Fabricate(:invite, invited_by: inviter, email: 'redeemed@example.com') }
+    let!(:redeemed_invite_user) { redeemed_invite.redeem }
+
+    fab!(:partially_redeemed_invite) { Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5) }
+    let!(:partially_redeemed_invite_user) { partially_redeemed_invite.redeem(email: 'partially_redeemed_invite@example.com') }
+
+    fab!(:redeemed_and_expired_invite) { Fabricate(:invite, invited_by: inviter, email: 'redeemed_and_expired@example.com') }
+    let!(:redeemed_and_expired_invite_user) do
+      user = redeemed_and_expired_invite.redeem
+      redeemed_and_expired_invite.update!(expires_at: 1.day.ago)
+      user
+    end
+
+    fab!(:partially_redeemed_and_expired_invite) { Fabricate(:invite, invited_by: inviter, email: nil, max_redemptions_allowed: 5) }
+    let!(:partially_redeemed_and_expired_invite_user) do
+      user = partially_redeemed_and_expired_invite.redeem(email: 'partially_redeemed_and_expired_invite@example.com')
+      partially_redeemed_and_expired_invite.update!(expires_at: 1.day.ago)
+      user
+    end
+
+    describe '#pending' do
+      it 'returns pending invites only' do
+        expect(Invite.pending(inviter)).to contain_exactly(
+          pending_invite, pending_link_invite, partially_redeemed_invite
+        )
+      end
+    end
+
+    describe '#expired' do
+      it 'returns expired invites only' do
+        expect(Invite.expired(inviter)).to contain_exactly(
+          expired_invite, partially_redeemed_and_expired_invite
+        )
+      end
+    end
+
+    describe '#redeemed_users' do
+      it 'returns redeemed users' do
+        expect(Invite.redeemed_users(inviter).map(&:user)).to contain_exactly(
+          redeemed_invite_user, partially_redeemed_invite_user, redeemed_and_expired_invite_user, partially_redeemed_and_expired_invite_user
+        )
       end
 
-      it "'not_required' should be at 0 position" do
-        expect(@emailed_status_types[:not_required]).to eq(0)
-      end
+      it 'returns redeemed users for trashed invites' do
+        [redeemed_invite, partially_redeemed_invite, redeemed_and_expired_invite, partially_redeemed_and_expired_invite].each(&:trash!)
 
-      it "'sent' should be at 4th position" do
-        expect(@emailed_status_types[:sent]).to eq(4)
+        expect(Invite.redeemed_users(inviter).map(&:user)).to contain_exactly(
+          redeemed_invite_user, partially_redeemed_invite_user, redeemed_and_expired_invite_user, partially_redeemed_and_expired_invite_user
+        )
       end
     end
   end
+
+  describe '#invalidate_for_email' do
+    it 'returns nil if there is no invite for the given email' do
+      invite = Invite.invalidate_for_email('test@example.com')
+      expect(invite).to eq(nil)
+    end
+
+    it 'sets the matching invite to be invalid' do
+      invite = Fabricate(:invite, invited_by: Fabricate(:user), email: 'test@example.com')
+      result = Invite.invalidate_for_email('test@example.com')
+
+      expect(result).to eq(invite)
+      expect(result.link_valid?).to eq(false)
+    end
+
+    it 'sets the matching invite to be invalid without being case-sensitive' do
+      invite = Fabricate(:invite, invited_by: Fabricate(:user), email: 'test@Example.COM')
+      result = Invite.invalidate_for_email('test@EXAMPLE.com')
+
+      expect(result).to eq(invite)
+      expect(result.link_valid?).to eq(false)
+    end
+  end
+
+  describe '#resend_email' do
+    fab!(:invite) { Fabricate(:invite) }
+
+    it 'resets expiry of a resent invite' do
+      invite.update!(invalidated_at: 10.days.ago, expires_at: 10.days.ago)
+      expect(invite).to be_expired
+
+      invite.resend_invite
+      expect(invite).not_to be_expired
+      expect(invite.invalidated_at).to be_nil
+    end
+  end
 end
diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb
index 8fd37b887ff..53645fb52da 100644
--- a/spec/requests/invites_controller_spec.rb
+++ b/spec/requests/invites_controller_spec.rb
@@ -4,277 +4,229 @@ require 'rails_helper'
 
 describe InvitesController do
   fab!(:admin) { Fabricate(:admin) }
-  fab!(:trust_level_4) { Fabricate(:trust_level_4) }
+  fab!(:user) { Fabricate(:user, trust_level: SiteSetting.min_trust_level_to_allow_invite) }
 
-  context 'show' do
+  context '#show' do
     fab!(:invite) { Fabricate(:invite) }
-    fab!(:user) { Fabricate(:coding_horror) }
 
-    it 'does not work for logged in users' do
-      sign_in(Fabricate(:user))
+    it 'shows the accept invite page' do
       get "/invites/#{invite.invite_key}"
-
       expect(response.status).to eq(200)
-      body = response.body
-      expect(CGI.unescapeHTML(body)).to include(I18n.t("login.already_logged_in"))
+      expect(response.body).to have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include('i*****g@a***********e.ooo')
+      expect(response.body).not_to include(invite.email)
+      expect(response.body).to_not include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
     end
 
-    it "returns error if invite not found" do
-      get "/invites/nopeNOPEnope"
-
+    it 'shows unobfuscated email if email data is present in authentication data' do
+      ActionDispatch::Request.any_instance.stubs(:session).returns(authentication: { email: invite.email })
+      get "/invites/#{invite.invite_key}"
       expect(response.status).to eq(200)
-
-      body = response.body
-      expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
-      expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.not_found', base_url: Discourse.base_url))
+      expect(response.body).to have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include(invite.email)
+      expect(response.body).not_to include('i*****g@a***********e.ooo')
     end
 
-    it "returns error if invite expired" do
+    it 'fails for logged in users' do
+      sign_in(Fabricate(:user))
+
+      get "/invites/#{invite.invite_key}"
+      expect(response.status).to eq(200)
+      expect(response.body).to_not have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include(I18n.t('login.already_logged_in'))
+    end
+
+    it 'fails if invite does not exist' do
+      get '/invites/missing'
+      expect(response.status).to eq(200)
+      expect(response.body).to_not have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include(I18n.t('invite.not_found', base_url: Discourse.base_url))
+    end
+
+    it 'fails if invite expired' do
       invite.update(expires_at: 1.day.ago)
 
       get "/invites/#{invite.invite_key}"
-
       expect(response.status).to eq(200)
-
-      body = response.body
-      expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
-      expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.expired', base_url: Discourse.base_url))
+      expect(response.body).to_not have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include(I18n.t('invite.expired', base_url: Discourse.base_url))
     end
 
-    it "renders the accept invite page if invite exists" do
-      get "/invites/#{invite.invite_key}"
-
-      expect(response.status).to eq(200)
-
-      body = response.body
-      expect(body).to have_tag(:script, with: { src: '/assets/application.js' })
-      expect(CGI.unescapeHTML(body)).to_not include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
-    end
-
-    it "stores the invite key in the secure session if invite exists" do
+    it 'stores the invite key in the secure session if invite exists' do
       get "/invites/#{invite.invite_key}"
       expect(response.status).to eq(200)
-      invite_key = read_secure_session["invite-key"]
+      invite_key = read_secure_session['invite-key']
       expect(invite_key).to eq(invite.invite_key)
     end
 
-    it "returns error if invite has already been redeemed" do
+    it 'returns error if invite has already been redeemed' do
       Fabricate(:invited_user, invite: invite, user: Fabricate(:user))
+
       get "/invites/#{invite.invite_key}"
-
       expect(response.status).to eq(200)
-
-      body = response.body
-      expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' })
-      expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
-    end
-  end
-
-  context '#destroy' do
-    it 'requires you to be logged in' do
-      delete "/invites.json",
-        params: { email: 'jake@adventuretime.ooo' }
-      expect(response.status).to eq(403)
-    end
-
-    context 'while logged in' do
-      let!(:user) { sign_in(Fabricate(:user))      }
-      let!(:invite) { Fabricate(:invite, invited_by: user) }
-      fab!(:another_invite) { Fabricate(:invite, email: 'anotheremail@address.com') }
-
-      it 'raises an error when the id is missing' do
-        delete "/invites.json"
-        expect(response.status).to eq(400)
-      end
-
-      it "raises an error when the id cannot be found" do
-        delete "/invites.json", params: { id: 848 }
-        expect(response.status).to eq(400)
-      end
-
-      it 'raises an error when the invite is not yours' do
-        delete "/invites.json", params: { id: another_invite.id }
-        expect(response.status).to eq(400)
-      end
-
-      it "destroys the invite" do
-        delete "/invites.json", params: { id: invite.id }
-
-        expect(response.status).to eq(200)
-
-        invite.reload
-        expect(invite.trashed?).to be_truthy
-      end
+      expect(response.body).to_not have_tag(:script, with: { src: '/assets/application.js' })
+      expect(response.body).to include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url))
     end
   end
 
   context '#create' do
-    it 'requires you to be logged in' do
-      post "/invites.json", params: { email: 'jake@adventuretime.ooo' }
+    it 'requires to be logged in' do
+      post '/invites.json', params: { email: 'test@example.com' }
       expect(response.status).to eq(403)
     end
 
     context 'while logged in' do
-      let(:email) { 'jake@adventuretime.ooo' }
+      before do
+        sign_in(user)
+      end
 
-      it "fails if you can't invite to the forum" do
+      it 'fails if you cannot invite to the forum' do
         sign_in(Fabricate(:user))
-        post "/invites.json", params: { email: email }
+
+        post '/invites.json', params: { email: 'test@example.com' }
         expect(response).to be_forbidden
       end
 
-      it "fails for normal user if invite email already exists" do
-        user = sign_in(trust_level_4)
-        invite = Invite.generate(user, email: "invite@example.com")
-        post "/invites.json", params: { email: invite.email }
+      it 'fails for normal user if invite email already exists' do
+        Fabricate(:invite, invited_by: user, email: 'test@example.com')
+
+        post '/invites.json', params: { email: 'test@example.com' }
         expect(response.status).to eq(422)
-        expect(response.parsed_body["failed"]).to be_present
-      end
-
-      it "allows admins to invite to groups" do
-        group = Fabricate(:group)
-        sign_in(admin)
-        post "/invites.json", params: { email: email, group_ids: [group.id] }
-        expect(response.status).to eq(200)
-        expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
-      end
-
-      it 'allows group owners to invite to groups' do
-        group = Fabricate(:group)
-        user = sign_in(Fabricate(:user))
-        user.update!(trust_level: TrustLevel[2])
-        group.add_owner(user)
-
-        post "/invites.json", params: { email: email, group_ids: [group.id] }
-
-        expect(response.status).to eq(200)
-        expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
-      end
-
-      it "does not allow admins to send multiple invites to same email" do
-        user = sign_in(admin)
-        invite = Invite.generate(user, email: "invite@example.com")
-        post "/invites.json", params: { email: invite.email }
-        expect(response.status).to eq(422)
-      end
-
-      it "responds with error message in case of validation failure" do
-        sign_in(admin)
-        post "/invites.json", params: { email: "test@mailinator.com" }
-        expect(response.status).to eq(422)
-        expect(response.parsed_body["errors"]).to be_present
+        expect(response.parsed_body['failed']).to be_present
       end
     end
 
-    describe 'single use invite link' do
-      it 'requires you to be logged in' do
-        post "/invites.json", params: {
-          email: 'jake@adventuretime.ooo', skip_email: true
-        }
+    context 'invite to topic' do
+      fab!(:topic) { Fabricate(:topic) }
+
+      it 'works' do
+        sign_in(user)
+
+        post '/invites.json', params: { email: 'test@example.com', topic_id: topic.id }
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.first['args'].first['invite_to_topic']).to be_falsey
+      end
+
+      it 'fails when topic_id is invalid' do
+        sign_in(user)
+
+        post '/invites.json', params: { email: 'test@example.com', topic_id: -9999 }
+        expect(response.status).to eq(400)
+      end
+    end
+
+    context 'invite to group' do
+      fab!(:group) { Fabricate(:group) }
+
+      it 'works for admins' do
+        sign_in(admin)
+
+        post '/invites.json', params: { email: 'test@example.com', group_ids: [group.id] }
+        expect(response.status).to eq(200)
+        expect(Invite.find_by(email: 'test@example.com').invited_groups.count).to eq(1)
+      end
+
+      it 'works for group owners' do
+        sign_in(user)
+        group.add_owner(user)
+
+        post '/invites.json', params: { email: 'test@example.com', group_ids: [group.id] }
+        expect(response.status).to eq(200)
+        expect(Invite.find_by(email: 'test@example.com').invited_groups.count).to eq(1)
+      end
+
+      it 'works with multiple groups' do
+        sign_in(admin)
+        group2 = Fabricate(:group)
+
+        post '/invites.json', params: { email: 'test@example.com', group_names: "#{group.name},#{group2.name}" }
+        expect(response.status).to eq(200)
+        expect(Invite.find_by(email: 'test@example.com').invited_groups.count).to eq(2)
+      end
+
+      it 'fails for group members' do
+        sign_in(user)
+        group.add(user)
+
+        post '/invites.json', params: { email: 'test@example.com', group_ids: [group.id] }
         expect(response.status).to eq(403)
       end
 
-      context 'while logged in' do
-        let(:email) { 'jake@adventuretime.ooo' }
+      it 'fails for other users' do
+        sign_in(user)
 
-        it "fails if you can't invite to the forum" do
-          sign_in(Fabricate(:user))
-          post "/invites.json", params: { email: email, skip_email: true }
-          expect(response.status).to eq(403)
-        end
+        post '/invites.json', params: { email: 'test@example.com', group_ids: [group.id] }
+        expect(response.status).to eq(403)
+      end
 
-        it "fails for normal user if invite email already exists" do
-          user = sign_in(trust_level_4)
-          invite = Invite.generate(user, email: "invite@example.com")
+      it 'fails to invite new user to a group-private topic' do
+        sign_in(user)
+        private_category = Fabricate(:private_category, group: group)
+        group_private_topic = Fabricate(:topic, category: private_category)
 
-          post "/invites.json", params: { email: invite.email, skip_email: true }
-          expect(response.status).to eq(422)
-        end
-
-        it "fails when topic_id is invalid" do
-          sign_in(trust_level_4)
-
-          post "/invites.json", params: { email: email, skip_email: true, topic_id: -9999 }
-          expect(response.status).to eq(400)
-        end
-
-        it "verifies that inviter is authorized to invite new user to a group-private topic" do
-          group = Fabricate(:group)
-          private_category = Fabricate(:private_category, group: group)
-          group_private_topic = Fabricate(:topic, category: private_category)
-          sign_in(trust_level_4)
-
-          post "/invites.json", params: {
-            email: email, skip_email: true, topic_id: group_private_topic.id
-          }
-
-          expect(response.status).to eq(403)
-        end
-
-        it "allows admins to invite to groups" do
-          group = Fabricate(:group)
-          sign_in(admin)
-
-          post "/invites.json", params: {
-            email: email, skip_email: true, group_ids: [group.id]
-          }
-
-          expect(response.status).to eq(200)
-          expect(Invite.find_by(email: email).invited_groups.count).to eq(1)
-        end
-
-        it "allows multiple group invite" do
-          Fabricate(:group, name: "security")
-          Fabricate(:group, name: "support")
-          sign_in(admin)
-
-          post "/invites.json", params: {
-            email: email, skip_email: true, group_names: "security,support"
-          }
-
-          expect(response.status).to eq(200)
-          expect(Invite.find_by(email: email).invited_groups.count).to eq(2)
-        end
+        post '/invites.json', params: { email: 'test@example.com', topic_id: group_private_topic.id  }
+        expect(response.status).to eq(403)
       end
     end
 
-    describe 'multiple use invite link' do
-      it 'requires you to be logged in' do
-        post "/invites.json", params: {
-          max_redemptions_allowed: 5
-        }
-        expect(response).to be_forbidden
+    context 'email invite' do
+      it 'does not allow users to send multiple invites to same email' do
+        sign_in(user)
+
+        post '/invites.json', params: { email: 'test@example.com' }
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.size).to eq(1)
+
+        post '/invites.json', params: { email: 'test@example.com' }
+        expect(response.status).to eq(422)
       end
 
-      context 'while logged in' do
-        it "allows staff to invite to groups" do
-          moderator = Fabricate(:moderator)
-          sign_in(moderator)
-          group = Fabricate(:group)
-          group.add_owner(moderator)
+      it 'accept skip_email parameter' do
+        sign_in(user)
 
-          post "/invites.json", params: {
-            max_redemptions_allowed: 5,
-            group_ids: [group.id]
-          }
+        post '/invites.json', params: { email: 'test@example.com', skip_email: true }
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.size).to eq(0)
+      end
 
-          expect(response.status).to eq(200)
-          expect(Invite.last.invited_groups.count).to eq(1)
-        end
+      it 'fails in case of validation failure' do
+        sign_in(admin)
 
-        it "allows multiple group invite" do
-          Fabricate(:group, name: "security")
-          Fabricate(:group, name: "support")
-          sign_in(admin)
+        post '/invites.json', params: { email: 'test@mailinator.com' }
+        expect(response.status).to eq(422)
+        expect(response.parsed_body['errors']).to be_present
+      end
+    end
 
-          post "/invites.json", params: {
-            max_redemptions_allowed: 5,
-            group_names: "security,support"
-          }
+    context 'link invite' do
+      it 'works' do
+        sign_in(admin)
 
-          expect(response.status).to eq(200)
-          expect(Invite.last.invited_groups.count).to eq(2)
-        end
+        post '/invites.json'
+        expect(response.status).to eq(200)
+        expect(Invite.last.email).to eq(nil)
+        expect(Invite.last.invited_by).to eq(admin)
+        expect(Invite.last.max_redemptions_allowed).to eq(1)
+      end
+
+      it 'fails if over invite_link_max_redemptions_limit' do
+        sign_in(admin)
+
+        post '/invites.json', params: { max_redemptions_allowed: SiteSetting.invite_link_max_redemptions_limit - 1 }
+        expect(response.status).to eq(200)
+
+        post '/invites.json', params: { max_redemptions_allowed: SiteSetting.invite_link_max_redemptions_limit + 1 }
+        expect(response.status).to eq(422)
+      end
+
+      it 'fails if over invite_link_max_redemptions_limit_users' do
+        sign_in(user)
+
+        post '/invites.json', params: { max_redemptions_allowed: SiteSetting.invite_link_max_redemptions_limit_users - 1 }
+        expect(response.status).to eq(200)
+
+        post '/invites.json', params: { max_redemptions_allowed: SiteSetting.invite_link_max_redemptions_limit_users + 1 }
+        expect(response.status).to eq(422)
       end
     end
   end
@@ -282,115 +234,176 @@ describe InvitesController do
   context '#update' do
     fab!(:invite) { Fabricate(:invite, invited_by: admin, email: 'test@example.com') }
 
-    before do
-      sign_in(admin)
-      RateLimiter.enable
-    end
-
-    after do
-      RateLimiter.disable
-    end
-
-    it 'updating email address resends invite email' do
+    it 'requires to be logged in' do
       put "/invites/#{invite.id}", params: { email: 'test2@example.com' }
-
-      expect(response.status).to eq(200)
-      expect(Jobs::InviteEmail.jobs.size).to eq(1)
+      expect(response.status).to eq(400)
     end
 
-    it 'updating does not resend invite email' do
-      put "/invites/#{invite.id}", params: { custom_message: "new message" }
+    context 'while logged in' do
+      before do
+        sign_in(admin)
+      end
 
-      expect(response.status).to eq(200)
-      expect(invite.reload.custom_message).to eq("new message")
-      expect(Jobs::InviteEmail.jobs.size).to eq(0)
+      it 'resends invite email if updating email address' do
+        put "/invites/#{invite.id}", params: { email: 'test2@example.com' }
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.size).to eq(1)
+      end
+
+      it 'does not resend invite email if skip_email if updating email address' do
+        put "/invites/#{invite.id}", params: { email: 'test2@example.com', skip_email: true }
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.size).to eq(0)
+      end
+
+      it 'does not resend invite email when updating other fields' do
+        put "/invites/#{invite.id}", params: { custom_message: 'new message' }
+        expect(response.status).to eq(200)
+        expect(invite.reload.custom_message).to eq('new message')
+        expect(Jobs::InviteEmail.jobs.size).to eq(0)
+      end
+
+      it 'can send invite email' do
+        sign_in(user)
+        RateLimiter.enable
+        RateLimiter.clear_all!
+
+        invite = Fabricate(:invite, invited_by: user, email: 'test@example.com')
+
+        expect { put "/invites/#{invite.id}", params: { send_email: true } }
+          .to change { RateLimiter.new(user, 'resend-invite-per-hour', 10, 1.hour).remaining }.by(-1)
+        expect(response.status).to eq(200)
+        expect(Jobs::InviteEmail.jobs.size).to eq(1)
+      ensure
+        RateLimiter.disable
+      end
+    end
+  end
+
+  context '#destroy' do
+    it 'requires to be logged in' do
+      delete '/invites.json', params: { email: 'test@example.com' }
+      expect(response.status).to eq(403)
     end
 
-    it 'can send invite email' do
-      user = Fabricate(:user, trust_level: SiteSetting.min_trust_level_to_allow_invite)
-      invite = Fabricate(:invite, invited_by: user, email: 'test@example.com')
+    context 'while logged in' do
+      fab!(:invite) { Fabricate(:invite, invited_by: user) }
 
-      sign_in(user)
-      RateLimiter.enable
+      before { sign_in(user) }
 
-      expect { put "/invites/#{invite.id}", params: { send_email: true } }
-        .to change { RateLimiter.new(user, "resend-invite-per-hour", 10, 1.hour).remaining }.by(-1)
+      it 'raises an error when id is missing' do
+        delete '/invites.json'
+        expect(response.status).to eq(400)
+      end
 
-      expect(response.status).to eq(200)
-      expect(Jobs::InviteEmail.jobs.size).to eq(1)
+      it 'raises an error when invite does not exist' do
+        delete '/invites.json', params: { id: 848 }
+        expect(response.status).to eq(400)
+      end
+
+      it 'raises an error when invite is not created by user' do
+        another_invite = Fabricate(:invite, email: 'test2@example.com')
+
+        delete '/invites.json', params: { id: another_invite.id }
+        expect(response.status).to eq(400)
+      end
+
+      it 'destroys the invite' do
+        delete '/invites.json', params: { id: invite.id }
+        expect(response.status).to eq(200)
+        expect(invite.reload.trashed?).to be_truthy
+      end
     end
   end
 
   context '#perform_accept_invitation' do
-    context 'with an invalid invite id' do
-      it "redirects to the root and doesn't change the session" do
-        put "/invites/show/doesntexist.json"
+    context 'with an invalid invite' do
+      it 'redirects to the root' do
+        put '/invites/show/doesntexist.json'
         expect(response.status).to eq(404)
-        expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
-        expect(session[:current_user_id]).to be_blank
-      end
-    end
-
-    context 'with an invalid invite record' do
-      let(:invite) { Fabricate(:invite) }
-      it "responds with error message" do
-        invite.update_attribute(:email, "John Doe <john.doe@example.com>")
-        put "/invites/show/#{invite.invite_key}.json"
-        expect(response.status).to eq(412)
-        expect(response.parsed_body["message"]).to eq(I18n.t('invite.error_message'))
+        expect(response.parsed_body['message']).to eq(I18n.t('invite.not_found_json'))
         expect(session[:current_user_id]).to be_blank
       end
     end
 
     context 'with a deleted invite' do
-      fab!(:topic) { Fabricate(:topic) }
-      let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
+      fab!(:invite) { Fabricate(:invite) }
 
       before do
-        invite.destroy!
+        invite.trash!
       end
 
-      it "redirects to the root" do
+      it 'redirects to the root' do
         put "/invites/show/#{invite.invite_key}.json"
-
         expect(response.status).to eq(404)
-        expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
+        expect(response.parsed_body['message']).to eq(I18n.t('invite.not_found_json'))
         expect(session[:current_user_id]).to be_blank
       end
     end
 
     context 'with an expired invite' do
-      fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.day.ago, emailed_status: Invite.emailed_status_types[:not_required]) }
-
-      it "response is not successful" do
-        put "/invites/show/#{invite_link.invite_key}.json", params: { email: "foobar@example.com" }
+      fab!(:invite) { Fabricate(:invite, expires_at: 1.day.ago) }
 
+      it 'response is not successful' do
+        put "/invites/show/#{invite.invite_key}.json"
         expect(response.status).to eq(404)
-        expect(response.parsed_body["message"]).to eq(I18n.t('invite.not_found_json'))
+        expect(response.parsed_body['message']).to eq(I18n.t('invite.not_found_json'))
         expect(session[:current_user_id]).to be_blank
       end
     end
 
-    context 'with a valid invite id' do
-      fab!(:topic) { Fabricate(:topic) }
-      let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
+    context 'with an email invite' do
+      let(:topic) { Fabricate(:topic) }
+      let(:invite) { Invite.generate(topic.user, email: 'iceking@adventuretime.ooo', topic: topic) }
 
       it 'redeems the invite' do
         put "/invites/show/#{invite.invite_key}.json"
-        invite.reload
-        expect(invite.redeemed?).to be_truthy
+        expect(invite.reload.redeemed?).to be_truthy
       end
 
-      it 'returns the right response when local login is disabled and no external auth is configured' do
+      it 'logs in the user' do
+        events = DiscourseEvent.track_events do
+          put "/invites/show/#{invite.invite_key}.json"
+        end
+
+        expect(events.map { |event| event[:event_name] }).to include(:user_logged_in, :user_first_logged_in)
+        expect(response.status).to eq(200)
+        expect(session[:current_user_id]).to eq(invite.invited_users.first.user_id)
+        expect(invite.reload.redeemed?).to be_truthy
+        user = User.find(invite.invited_users.first.user_id)
+        expect(user.ip_address).to be_present
+        expect(user.registration_ip_address).to be_present
+      end
+
+      it 'redirects to the first topic the user was invited to' do
+        put "/invites/show/#{invite.invite_key}.json"
+        expect(response.status).to eq(200)
+        expect(response.parsed_body['redirect_to']).to eq(topic.relative_url)
+      end
+
+      it 'sets the timezone of the user in user_options' do
+        put "/invites/show/#{invite.invite_key}.json", params: { timezone: 'Australia/Melbourne' }
+        expect(response.status).to eq(200)
+        invite.reload
+        user = User.find(invite.invited_users.first.user_id)
+        expect(user.user_option.timezone).to eq('Australia/Melbourne')
+      end
+
+      it 'does not log in the user if there are validation errors' do
+        put "/invites/show/#{invite.invite_key}.json", params: { password: 'password' }
+        expect(response.status).to eq(412)
+        expect(response.parsed_body['errors']['password']).to be_present
+      end
+
+      it 'fails when local login is disabled and no external auth is configured' do
         SiteSetting.enable_local_logins = false
 
         put "/invites/show/#{invite.invite_key}.json"
-
         expect(response.status).to eq(404)
       end
 
-      describe 'with authentication session' do
-        let(:authenticated_email) { "foobar@example.com" }
+      context 'with OmniAuth provider' do
+        fab!(:authenticated_email) { 'test@example.com' }
 
         before do
           OmniAuth.config.test_mode = true
@@ -406,204 +419,146 @@ describe InvitesController do
               raw_info: OmniAuth::AuthHash.new(
                 email_verified: true,
                 email: authenticated_email,
-                family_name: "Last",
-                given_name: "First",
-                gender: "male",
-                name: "First Last",
+                family_name: 'Last',
+                given_name: 'First',
+                gender: 'male',
+                name: 'First Last',
               )
             },
           )
 
-          Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2]
+          Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:google_oauth2]
           SiteSetting.enable_google_oauth2_logins = true
 
-          get "/auth/google_oauth2/callback.json"
+          get '/auth/google_oauth2/callback.json'
           expect(response.status).to eq(302)
         end
 
         after do
-          Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] = nil
+          Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:google_oauth2] = nil
           OmniAuth.config.test_mode = false
         end
 
         it 'should associate the invited user with authenticator records' do
-          invite.update!(email: authenticated_email)
           SiteSetting.auth_overrides_name = true
+          invite.update!(email: authenticated_email)
 
-          expect do
-            put "/invites/show/#{invite.invite_key}.json",
-              params: { name: 'somename' }
-
-            expect(response.status).to eq(200)
-          end.to change { User.with_email(authenticated_email).exists? }.to(true)
+          expect { put "/invites/show/#{invite.invite_key}.json", params: { name: 'somename' } }
+            .to change { User.with_email(authenticated_email).exists? }.to(true)
+          expect(response.status).to eq(200)
 
           user = User.find_by_email(authenticated_email)
-
           expect(user.name).to eq('First Last')
-
-          expect(user.user_associated_accounts.first.provider_name)
-            .to eq("google_oauth2")
+          expect(user.user_associated_accounts.first.provider_name).to eq('google_oauth2')
         end
 
         it 'returns the right response even if local logins has been disabled' do
           SiteSetting.enable_local_logins = false
-
           invite.update!(email: authenticated_email)
 
           put "/invites/show/#{invite.invite_key}.json"
-
           expect(response.status).to eq(200)
         end
 
         it 'returns the right response if authenticated email does not match invite email' do
           put "/invites/show/#{invite.invite_key}.json"
-
           expect(response.status).to eq(412)
         end
       end
 
-      context 'when redeem returns a user' do
-        fab!(:user) { Fabricate(:coding_horror) }
+      context '.post_process_invite' do
+        it 'sends a welcome message if set' do
+          SiteSetting.send_welcome_message = true
+          user.send_welcome_message = true
+          put "/invites/show/#{invite.invite_key}.json"
+          expect(response.status).to eq(200)
 
-        context 'success' do
-          it 'logs in the user' do
-            events = DiscourseEvent.track_events do
-              put "/invites/show/#{invite.invite_key}.json"
-            end
+          expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
+        end
 
-            expect(events.map { |event| event[:event_name] }).to include(
-              :user_logged_in, :user_first_logged_in
-            )
-            invite.reload
-            expect(response.status).to eq(200)
-            expect(session[:current_user_id]).to eq(invite.invited_users.first.user_id)
-            expect(invite.redeemed?).to be_truthy
-            user = User.find(invite.invited_users.first.user_id)
-            expect(user.ip_address).to be_present
-            expect(user.registration_ip_address).to be_present
-          end
+        it 'refreshes automatic groups if staff' do
+          topic.user.grant_admin!
+          invite.update!(moderator: true)
 
-          it 'redirects to the first topic the user was invited to' do
+          put "/invites/show/#{invite.invite_key}.json"
+          expect(response.status).to eq(200)
+
+          expect(invite.invited_users.first.user.groups.pluck(:name)).to contain_exactly('moderators', 'staff')
+        end
+
+        context 'without password' do
+          it 'sends password reset email' do
             put "/invites/show/#{invite.invite_key}.json"
             expect(response.status).to eq(200)
-            expect(response.parsed_body["redirect_to"]).to eq(topic.relative_url)
-          end
 
-          context "if a timezone guess is provided" do
-            it "sets the timezone of the user in user_options" do
-              put "/invites/show/#{invite.invite_key}.json", params: { timezone: "Australia/Melbourne" }
-              expect(response.status).to eq(200)
-              invite.reload
-              user = User.find(invite.invited_users.first.user_id)
-              expect(user.user_option.timezone).to eq("Australia/Melbourne")
-            end
+            expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(1)
+            expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
           end
         end
 
-        context 'failure' do
-          it "doesn't log in the user if there's a validation error" do
-            put "/invites/show/#{invite.invite_key}.json", params: { password: "password" }
-            expect(response.status).to eq(412)
-            expect(response.parsed_body["errors"]["password"]).to be_present
-          end
-        end
+        context 'with password' do
+          context 'user was invited via email' do
+            before { invite.update_column(:emailed_status, Invite.emailed_status_types[:pending]) }
 
-        context '.post_process_invite' do
-          it 'sends a welcome message if set' do
-            user.send_welcome_message = true
-            put "/invites/show/#{invite.invite_key}.json"
-            expect(response.status).to eq(200)
+            it 'does not send an activation email and activates the user' do
+              expect do
+                put "/invites/show/#{invite.invite_key}.json", params: { password: 'verystrongpassword' }
+              end.to change { UserAuthToken.count }.by(1)
 
-            expect(Jobs::SendSystemMessage.jobs.size).to eq(1)
-          end
-
-          it 'refreshes automatic groups if staff' do
-            topic.user.grant_admin!
-            invite.update!(moderator: true)
-
-            put "/invites/show/#{invite.invite_key}.json"
-            expect(response.status).to eq(200)
-
-            expect(invite.invited_users.first.user.groups.pluck(:name)).to contain_exactly("moderators", "staff")
-          end
-
-          context "without password" do
-            it "sends password reset email" do
-              put "/invites/show/#{invite.invite_key}.json"
               expect(response.status).to eq(200)
 
-              expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(1)
+              expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
               expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
+
+              invited_user = User.find_by_email(invite.email)
+              expect(invited_user.active).to eq(true)
+              expect(invited_user.email_confirmed?).to eq(true)
             end
           end
 
-          context "with password" do
-            context "user was invited via email" do
-              before { invite.update_column(:emailed_status, Invite.emailed_status_types[:pending]) }
+          context 'user was invited via link' do
+            before { invite.update_column(:emailed_status, Invite.emailed_status_types[:not_required]) }
 
-              it "doesn't send an activation email and activates the user" do
-                expect do
-                  put "/invites/show/#{invite.invite_key}.json", params: { password: "verystrongpassword" }
-                end.to change { UserAuthToken.count }.by(1)
+            it 'sends an activation email and does not activate the user' do
+              expect do
+                put "/invites/show/#{invite.invite_key}.json", params: { password: 'verystrongpassword' }
+              end.not_to change { UserAuthToken.count }
 
-                expect(response.status).to eq(200)
+              expect(response.status).to eq(200)
+              expect(response.parsed_body['message']).to eq(I18n.t('invite.confirm_email'))
 
-                expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
-                expect(Jobs::CriticalUserEmail.jobs.size).to eq(0)
+              invited_user = User.find_by_email(invite.email)
+              expect(invited_user.active).to eq(false)
+              expect(invited_user.email_confirmed?).to eq(false)
 
-                invited_user = User.find_by_email(invite.email)
-                expect(invited_user.active).to eq(true)
-                expect(invited_user.email_confirmed?).to eq(true)
-              end
-            end
+              expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
+              expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
 
-            context "user was invited via link" do
-              before { invite.update_column(:emailed_status, Invite.emailed_status_types[:not_required]) }
+              tokens = EmailToken.where(user_id: invited_user.id, confirmed: false, expired: false).pluck(:token)
+              expect(tokens.size).to eq(1)
 
-              it "sends an activation email and doesn't activate the user" do
-                expect do
-                  put "/invites/show/#{invite.invite_key}.json", params: { password: "verystrongpassword" }
-                end.not_to change { UserAuthToken.count }
-
-                expect(response.status).to eq(200)
-                expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
-
-                invited_user = User.find_by_email(invite.email)
-                expect(invited_user.active).to eq(false)
-                expect(invited_user.email_confirmed?).to eq(false)
-
-                expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0)
-                expect(Jobs::CriticalUserEmail.jobs.size).to eq(1)
-
-                tokens = EmailToken.where(user_id: invited_user.id, confirmed: false, expired: false).pluck(:token)
-                expect(tokens.size).to eq(1)
-
-                job_args = Jobs::CriticalUserEmail.jobs.first["args"].first
-                expect(job_args["type"]).to eq("signup")
-                expect(job_args["user_id"]).to eq(invited_user.id)
-                expect(job_args["email_token"]).to eq(tokens.first)
-              end
+              job_args = Jobs::CriticalUserEmail.jobs.first['args'].first
+              expect(job_args['type']).to eq('signup')
+              expect(job_args['user_id']).to eq(invited_user.id)
+              expect(job_args['email_token']).to eq(tokens.first)
             end
           end
         end
       end
     end
 
-    context 'with multiple use invite link' do
-      fab!(:invite_link) { Fabricate(:invite, email: nil, max_redemptions_allowed: 5, expires_at: 1.month.from_now, emailed_status: Invite.emailed_status_types[:not_required]) }
+    context 'with an invite link' do
+      fab!(:invite) { Fabricate(:invite, email: nil, emailed_status: Invite.emailed_status_types[:not_required]) }
 
-      it "sends an activation email and doesn't activate the user" do
-        expect do
-          put "/invites/show/#{invite_link.invite_key}.json", params: { email: "foobar@example.com", password: "verystrongpassword" }
-        end.not_to change { UserAuthToken.count }
+      it 'sends an activation email and does not activate the user' do
+        expect { put "/invites/show/#{invite.invite_key}.json", params: { email: 'test@example.com', password: 'verystrongpassword' } }
+          .not_to change { UserAuthToken.count }
 
         expect(response.status).to eq(200)
-        expect(response.parsed_body["message"]).to eq(I18n.t("invite.confirm_email"))
+        expect(response.parsed_body['message']).to eq(I18n.t('invite.confirm_email'))
+        expect(invite.reload.redemption_count).to eq(1)
 
-        invite_link.reload
-        expect(invite_link.redemption_count).to eq(1)
-
-        invited_user = User.find_by_email("foobar@example.com")
+        invited_user = User.find_by_email('test@example.com')
         expect(invited_user.active).to eq(false)
         expect(invited_user.email_confirmed?).to eq(false)
 
@@ -613,49 +568,44 @@ describe InvitesController do
         tokens = EmailToken.where(user_id: invited_user.id, confirmed: false, expired: false).pluck(:token)
         expect(tokens.size).to eq(1)
 
-        job_args = Jobs::CriticalUserEmail.jobs.first["args"].first
-        expect(job_args["type"]).to eq("signup")
-        expect(job_args["user_id"]).to eq(invited_user.id)
-        expect(job_args["email_token"]).to eq(tokens.first)
+        job_args = Jobs::CriticalUserEmail.jobs.first['args'].first
+        expect(job_args['type']).to eq('signup')
+        expect(job_args['user_id']).to eq(invited_user.id)
+        expect(job_args['email_token']).to eq(tokens.first)
       end
     end
 
     context 'new registrations are disabled' do
       fab!(:topic) { Fabricate(:topic) }
-
-      let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
+      fab!(:invite) { Invite.generate(topic.user, email: 'test@example.com', topic: topic) }
 
       before { SiteSetting.allow_new_registrations = false }
 
-      it "doesn't redeem the invite" do
+      it 'does not redeem the invite' do
         put "/invites/show/#{invite.invite_key}.json"
         expect(response.status).to eq(200)
-        invite.reload
-        expect(invite.invited_users).to be_blank
+        expect(invite.reload.invited_users).to be_blank
         expect(invite.redeemed?).to be_falsey
-        expect(response.body).to include(I18n.t("login.new_registrations_disabled"))
+        expect(response.body).to include(I18n.t('login.new_registrations_disabled'))
       end
     end
 
     context 'user is already logged in' do
-      fab!(:topic) { Fabricate(:topic) }
+      fab!(:invite) { Fabricate(:invite, email: 'test@example.com') }
+      fab!(:user) { sign_in(Fabricate(:user)) }
 
-      let(:invite) { Invite.generate(topic.user, email: "iceking@adventuretime.ooo", topic: topic) }
-
-      let!(:user) { sign_in(Fabricate(:user)) }
-
-      it "doesn't redeem the invite" do
+      it 'does not redeem the invite' do
         put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key }
         expect(response.status).to eq(200)
         invite.reload
         expect(invite.invited_users).to be_blank
         expect(invite.redeemed?).to be_falsey
-        expect(response.body).to include(I18n.t("login.already_logged_in", current_user: user.username))
+        expect(response.body).to include(I18n.t('login.already_logged_in', current_user: user.username))
       end
     end
   end
 
-  context "#destroy_all" do
+  context '#destroy_all_expired' do
     it 'removes all expired invites sent by a user' do
       SiteSetting.invite_expiry_days = 1
 
@@ -666,7 +616,7 @@ describe InvitesController do
       expired_invite.update!(expires_at: 2.days.ago)
 
       sign_in(user)
-      post "/invites/destroy-all-expired"
+      post '/invites/destroy-all-expired'
 
       expect(response.status).to eq(200)
       expect(invite_1.reload.deleted_at).to eq(nil)
@@ -676,33 +626,33 @@ describe InvitesController do
   end
 
   context '#resend_invite' do
-    it 'requires you to be logged in' do
-      post "/invites/reinvite.json", params: { email: 'first_name@example.com' }
+    it 'requires to be logged in' do
+      post '/invites/reinvite.json', params: { email: 'first_name@example.com' }
       expect(response.status).to eq(403)
     end
 
     context 'while logged in' do
-      let!(:user) { sign_in(Fabricate(:user)) }
-      let!(:invite) { Fabricate(:invite, invited_by: user) }
+      fab!(:user) { sign_in(Fabricate(:user)) }
+      fab!(:invite) { Fabricate(:invite, invited_by: user) }
       fab!(:another_invite) { Fabricate(:invite, email: 'last_name@example.com') }
 
       it 'raises an error when the email is missing' do
-        post "/invites/reinvite.json"
+        post '/invites/reinvite.json'
         expect(response.status).to eq(400)
       end
 
-      it "raises an error when the email cannot be found" do
-        post "/invites/reinvite.json", params: { email: 'first_name@example.com' }
+      it 'raises an error when the email cannot be found' do
+        post '/invites/reinvite.json', params: { email: 'first_name@example.com' }
         expect(response.status).to eq(400)
       end
 
       it 'raises an error when the invite is not yours' do
-        post "/invites/reinvite.json", params: { email: another_invite.email }
+        post '/invites/reinvite.json', params: { email: another_invite.email }
         expect(response.status).to eq(400)
       end
 
-      it "resends the invite" do
-        post "/invites/reinvite.json", params: { email: invite.email }
+      it 'resends the invite' do
+        post '/invites/reinvite.json', params: { email: invite.email }
         expect(response.status).to eq(200)
         expect(Jobs::InviteEmail.jobs.size).to eq(1)
       end
@@ -722,7 +672,7 @@ describe InvitesController do
       redeemed_invite.update!(expires_at: 5.days.ago)
 
       sign_in(user)
-      post "/invites/reinvite-all"
+      post '/invites/reinvite-all'
 
       expect(response.status).to eq(200)
       expect(new_invite.reload.expires_at.to_date).to eq(30.days.from_now.to_date)
@@ -732,41 +682,36 @@ describe InvitesController do
   end
 
   context '#upload_csv' do
-    it 'requires you to be logged in' do
-      post "/invites/upload_csv.json"
+    it 'requires to be logged in' do
+      post '/invites/upload_csv.json'
       expect(response.status).to eq(403)
     end
 
     context 'while logged in' do
       let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/discourse.csv") }
+      let(:file) { Rack::Test::UploadedFile.new(File.open(csv_file)) }
 
-      let(:file) do
-        Rack::Test::UploadedFile.new(File.open(csv_file))
-      end
-
-      let(:filename) { 'discourse.csv' }
-
-      it "fails if you can't bulk invite to the forum" do
+      it 'fails if you cannot bulk invite to the forum' do
         sign_in(Fabricate(:user))
-        post "/invites/upload_csv.json", params: { file: file, name: filename }
+        post '/invites/upload_csv.json', params: { file: file, name: 'discourse.csv' }
         expect(response.status).to eq(403)
       end
 
-      it "allows admin to bulk invite" do
+      it 'allows admin to bulk invite' do
         sign_in(admin)
-        post "/invites/upload_csv.json", params: { file: file, name: filename }
+        post '/invites/upload_csv.json', params: { file: file, name: 'discourse.csv' }
         expect(response.status).to eq(200)
         expect(Jobs::BulkInvite.jobs.size).to eq(1)
       end
 
-      it "sends limited invites at a time" do
+      it 'sends limited invites at a time' do
         SiteSetting.max_bulk_invites = 3
         sign_in(admin)
-        post "/invites/upload_csv.json", params: { file: file, name: filename }
+        post '/invites/upload_csv.json', params: { file: file, name: 'discourse.csv' }
 
         expect(response.status).to eq(422)
         expect(Jobs::BulkInvite.jobs.size).to eq(1)
-        expect(response.parsed_body["errors"][0]).to eq(I18n.t("bulk_invite.max_rows", max_bulk_invites: SiteSetting.max_bulk_invites))
+        expect(response.parsed_body['errors'][0]).to eq(I18n.t('bulk_invite.max_rows', max_bulk_invites: SiteSetting.max_bulk_invites))
       end
     end
   end
diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb
index 3b02ccd5eec..ae921e092fc 100644
--- a/spec/requests/topics_controller_spec.rb
+++ b/spec/requests/topics_controller_spec.rb
@@ -3377,6 +3377,7 @@ RSpec.describe TopicsController do
           end.to change { Invite.where(invited_by_id: user.id).count }.by(1)
 
           expect(response.status).to eq(200)
+          expect(Jobs::InviteEmail.jobs.first['args'].first['invite_to_topic']).to be_truthy
         end
       end
 
diff --git a/test/javascripts/integration/components/invite-panel-test.js b/test/javascripts/integration/components/invite-panel-test.js
deleted file mode 100644
index bdce109aa15..00000000000
--- a/test/javascripts/integration/components/invite-panel-test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import EmberObject, { set } from "@ember/object";
-import componentTest from "helpers/component-test";
-import { moduleForComponent } from "ember-qunit";
-import { queryAll } from "discourse/tests/helpers/qunit-helpers";
-
-moduleForComponent("invite-panel", { integration: true });
-
-componentTest("can_invite_via_email", {
-  template: "{{invite-panel panel=panel}}",
-
-  beforeEach() {
-    set(this.currentUser, "details", { can_invite_via_email: true });
-    const inviteModel = JSON.parse(JSON.stringify(this.currentUser));
-    this.set("panel", {
-      id: "invite",
-      model: { inviteModel: EmberObject.create(inviteModel) },
-    });
-  },
-
-  async test(assert) {
-    await fillIn(".invite-user-input", "eviltrout@example.com");
-    assert.ok(queryAll(".send-invite:disabled").length === 0);
-  },
-});