Allow admins to delete an organization (#19699)

* feat: update the layout for organizations

* feat: update the layout for an organization

* feat: organization logos are square

* feat: change the text on the org visit button

* feat: initialize the dropdown menu

* feat: trigger the  modal

* feat: update the text in the model

* feat: tweak wording

* WIP/feat: hook up a route with an action that calls an organization delete worker.

* feat: redirect after showing the notice

* feat: update the authorization required to be super admin

* feat: add the notice to localization

* spec: integration test

* spec: fix

* spec: delete worker

* chore: update the link

* feat: add safe navigation

* feat: make sure that we show an error if the organization has credits

* fix: error message for modal unspent credits

* feat add the spec for the helper

* fix: remove rspec preface

* fix: small fixes to tests and I18n

* fix: updated the number of orgs

* feat: update the message

* feat: update the message

* chore: remove inclusion of helper

* feat: change the name of the parameter

* chore: newline
This commit is contained in:
Ridhwana 2023-07-17 12:09:24 +02:00 committed by GitHub
parent 20c282df80
commit 272aea6127
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 415 additions and 93 deletions

View file

@ -29,6 +29,18 @@ module Admin
redirect_to admin_organization_path(org)
end
def destroy
organization = Organization.find_by(id: params[:id])
Organizations::DeleteWorker.perform_async(organization.id, current_user.id, false)
flash[:settings_notice] =
I18n.t("admin.organizations_controller.deletion_scheduled", organization_name: organization.name)
redirect_to admin_organization_url(params[:id])
rescue StandardError => e
flash[:error] = I18n.t("admin.organizations_controller.error", organization_name: organization.name, error: e)
redirect_to user_settings_path(:organization, id: organization.id)
end
private
def add_note(org)

View file

@ -37,6 +37,6 @@ class BillboardsController < ApplicationController
def user_tags
return unless feed_targeted_tag_placement?(placement_area)
current_user.cached_followed_tag_names
current_user&.cached_followed_tag_names
end
end

View file

@ -76,7 +76,7 @@ class OrganizationsController < ApplicationController
organization = Organization.find_by(id: params[:id])
authorize organization
Organizations::DeleteWorker.perform_async(organization.id, current_user.id)
Organizations::DeleteWorker.perform_async(organization.id, current_user.id, true)
flash[:settings_notice] =
I18n.t("organizations_controller.deletion_scheduled", organization_name: organization.name)

View file

@ -0,0 +1,16 @@
module Admin
module OrganizationsHelper
def deletion_modal_error_message(organization)
error_message = nil
unless current_user.super_admin?
error_message = I18n.t("views.admin.organizations.delete.role_notice")
end
if organization.credits.length.positive?
error_message = "#{error_message} #{I18n.t('views.admin.organizations.delete.credits_notice')}"
end
error_message&.strip
end
end
end

View file

@ -0,0 +1,9 @@
import { showOrganizationModal } from '././organizations/modals';
import { initializeDropdown } from '@utilities/dropdownUtils';
initializeDropdown({
triggerElementId: 'options-dropdown-trigger',
dropdownContentId: 'options-dropdown',
});
document.body.addEventListener('click', showOrganizationModal);

View file

@ -0,0 +1,45 @@
import { showWindowModal } from '@utilities/showModal';
const modalContents = new Map();
/**
* Helper function to handle finding and caching modal content. Since our Preact modal helper works by duplicating HTML content,
* and our user modals rely on IDs to label form controls, we remove the original hidden content from the DOM to avoid ID conflicts.
*
* @param {string} modalContentSelector The CSS selector used to identify the correct modal content
*/
const getModalContents = (modalContentSelector) => {
if (!modalContents.has(modalContentSelector)) {
const modalContentElement = document.querySelector(modalContentSelector);
const modalContent = modalContentElement.innerHTML;
modalContentElement.remove();
modalContents.set(modalContentSelector, modalContent);
}
return modalContents.get(modalContentSelector);
};
/**
* Helper function for views which use admin user modals. May be attached as an event listener, and its actions will only be triggered
* if the target of the event is a recognised user modal trigger.
*
* @param {Object} event
*/
export const showOrganizationModal = (event) => {
const { dataset } = event.target;
if (!Object.prototype.hasOwnProperty.call(dataset, 'modalContentSelector')) {
// We're not trying to trigger a modal.
return;
}
event.preventDefault();
const { modalTitle, modalSize, modalContentSelector } = dataset;
showWindowModal({
modalContent: getModalContents(modalContentSelector),
title: modalTitle,
size: modalSize,
});
};

View file

@ -1,48 +1,86 @@
<header class="flex items-center mb-6">
<div>
<h1 class="crayons-title"><%= @organization.name %></h1>
<p class="color-base-60">Created <%= @organization.created_at.strftime("%b %e '%y") %></p>
</div>
<div class="ml-auto">
<%= link_to "View @#{@organization.name}", "/#{@organization.slug}", class: "crayons-btn", target: "_blank", rel: "noopener" %>
</div>
<%= javascript_packs_with_chunks_tag "admin/organizations", defer: true %>
<header>
<section class="crayons-card flex flex-col l:flex-row gap-2 m:gap-4 l:gap-6 mb-2 m:mb-3 p-3 s:p-4 m:p-7">
<span class="crayons-logo crayons-logo--3xl">
<img src="<%= @organization.profile_image %>" width="100%" height="100%" alt="<%= @organization.name %> profile picture">
</span>
<div class="grid gap-2 flex-1">
<div class="s:flex items-center gap-5">
<div class="flex flex-1 items-center">
<h1 class="crayons-title lh-tight">
<%= @organization.name %>
</h1>
</div>
<div class="flex relative justify-between s:justify-end gap-2 my-2 s:my-0">
<%= link_to t("views.admin.organizations.profile.visit"), "/#{@organization.slug}", class: "c-cta", target: "_blank", rel: "noopener" %>
<div class="dropdown-trigger-container">
<button type="button" class="c-btn c-btn--icon-alone dropdown-trigger" id="options-dropdown-trigger" aria-haspopup="true" aria-expanded="false" aria-controls="options-dropdown">
<%= crayons_icon_tag("overflow-vertical", title: t("views.admin.users.profile.options.icon")) %>
</button>
<div class="crayons-dropdown right-0 left-0 s:left-auto" id="options-dropdown">
<ul class="p-0">
<li>
<button
type="button" class="c-btn c-btn--destructive w-100 align-left"
data-modal-title="<%= t("views.admin.organizations.delete.heading", organization: @organization.name) %>"
data-modal-size="small"
data-modal-content-selector="#delete-organization">
<%= t("views.admin.organizations.options.delete") %>
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="fs-s mb-1 color-secondary">
<%= @organization.slug %> <span class="opacity-50">&bull;</span> <%= t("views.admin.organizations.profile.id", organizationid: @organization.id) %> <span class="opacity-50">&bull;</span>
<%= t("views.admin.organizations.profile.since_html", time: tag.time(l(@organization.created_at, format: :short_with_yy), datetime: @organization.created_at.strftime("%Y-%m-%dT%H:%M:%S%z"), title: l(@organization.created_at, format: :admin_user))) %>
<span class="opacity-50">&bull;</span> <%= t("views.admin.organizations.profile.total_members", count: @organization.organization_memberships.size) %>
</div>
<div>
<ul class="flex flex-col s:flex-row gap-3 s:gap-6 flex-wrap">
<li>
<% if @organization.email %>
<a href="mailto:<%= @organization.email %>" class="c-link c-link--icon-left inline-block" target="_blank" rel="noopener">
<%= crayons_icon_tag(:email, title: t("views.admin.organizations.profile.email"), class: "c-link__icon") %>
<%= @organization.email %>
</a>
<% else %>
<%= crayons_icon_tag(:email, title: t("views.admin.organizations.profile.email"), class: "c-link__icon") %>
N/A
<% end %>
</li>
<% if @organization.github_username %>
<li>
<a href="https://github.com/<%= @organization.github_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
<%= crayons_icon_tag(:github, title: t("views.admin.users.profile.github")) %>
<%= @organization.github_username %>
</a>
</li>
<% end %>
<% if @organization.twitter_username %>
<li>
<a href="https://twitter.com/<%= @organization.twitter_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
<%= crayons_icon_tag(:twitter, title: t("views.admin.users.profile.twitter")) %>
<%= @organization.twitter_username %>
</a>
</li>
<% end %>
</ul>
</div>
</div>
</section>
</header>
<div class="crayons-card p-6 mb-6">
<h3 class="crayons-subtitle-2 mb-4">General Info</h3>
<dl>
<dt>ID:</dt>
<dd><%= @organization.id %></dd>
<dt>Name:</dt>
<dd><%= @organization.name %></dd>
<dt>Membership Count:</dt>
<dd><%= @organization.organization_memberships.size %></dd>
<dt>Email:</dt>
<dd><%= @organization.email || "N/A" %></dd>
<dt>Twitter:</dt>
<% if @organization.twitter_username %>
<dd><%= link_to @organization.twitter_username, "https://twitter.com/#{@organization.twitter_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
<dt>GitHub:</dt>
<% if @organization.github_username %>
<dd><%= link_to @organization.github_username, "https://github.com/#{@organization.github_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
</dl>
</div>
<div class="crayons-card p-6 mb-6">
<% current_credits = @organization.unspent_credits_count %>
<h3 class="crayons-subtitle-2 mb-4">Credits (current: <%= current_credits %>)</h3>
<%= form_tag update_org_credits_admin_organization_path(@organization), method: :patch, class: "flex justify-between mb-2" do %>
<div>
<%= hidden_field_tag :credit_action, :add %>
<%= number_field_tag :credits, nil, in: 1...100_000, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" } %>
<%= text_field_tag :note, "", placeholder: "Why are you adding these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" } %>
<%= number_field_tag :credits, nil, in: 1...100_000, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" }, autocomplete: "off" %>
<%= text_field_tag :note, "", placeholder: "Why are you adding these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" }, autocomplete: "off" %>
</div>
<%= submit_tag "Add Org Credits", class: "crayons-btn" %>
<% end %>
@ -50,8 +88,8 @@
<%= form_tag update_org_credits_admin_organization_path(@organization), method: :patch, class: "flex justify-between mb-2" do %>
<div>
<%= hidden_field_tag :credit_action, :remove %>
<%= number_field_tag :credits, nil, in: 1..current_credits, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" } %>
<%= text_field_tag :note, "", placeholder: "Why are you removing these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" } %>
<%= number_field_tag :credits, nil, in: 1..current_credits, required: true, class: "crayons-textfield w-auto mr-3", size: 5, aria: { label: "Credits" }, autocomplete: "off" %>
<%= text_field_tag :note, "", placeholder: "Why are you removing these credits?", size: 50, required: true, class: "crayons-textfield w-auto mr-3", aria: { label: "Reason" }, autocomplete: "off" %>
</div>
<%= submit_tag "Remove Org Credits", class: "crayons-btn crayons-btn--danger" %>
<% end %>
@ -59,3 +97,19 @@
</div>
<%= render "activity" %>
<% error_message = deletion_modal_error_message(@organization) %>
<div class="hidden">
<div id="delete-organization">
<% if error_message %>
<div class="crayons-notice crayons-notice--danger"><%= error_message %></div>
<% else %>
<%= form_for(@organization, url: admin_organization_path(@organization), html: { class: "flex flex-col gap-6", method: :delete, onsubmit: "return confirm('#{j t('views.admin.organizations.delete.onsubmit')}')", id: nil }) do |f| %>
<%= t("views.admin.organizations.delete.desc2", organization: @organization.name) %>
<div>
<button class="c-btn c-btn--primary c-btn--destructive"><%= t("views.admin.organizations.delete.submit") %></button>
</div>
<% end %>
<% end %>
</div>
</div>

View file

@ -5,7 +5,9 @@ module Organizations
sidekiq_options queue: :high_priority, retry: 10
# operator_id - the user who deletes the organization
def perform(organization_id, operator_id)
# deleted_by_org_admin - the organization can be deleted by either
# the admin of an organization or by the Forem Admin.
def perform(organization_id, operator_id, deleted_by_org_admin)
org = Organization.find_by(id: organization_id)
return unless org
@ -14,11 +16,12 @@ module Organizations
Organizations::Delete.call(org)
user.touch(:organization_info_updated_at)
EdgeCache::BustUser.call(user)
if deleted_by_org_admin
user.touch(:organization_info_updated_at)
EdgeCache::BustUser.call(user)
# notify user that the org was deleted
NotifyMailer.with(name: user.name, org_name: org.name, email: user.email).organization_deleted_email.deliver_now
NotifyMailer.with(name: user.name, org_name: org.name, email: user.email).organization_deleted_email.deliver_now
end
audit_log(org, user)
rescue StandardError => e

View file

@ -66,6 +66,8 @@ en:
updated: User was successfully updated to %{type}
organizations_controller:
credit_updated: Successfully updated credits
deletion_scheduled: 'Organization, "%{organization_name}", deletion is scheduled.'
error: 'Your organization, %{organization_name}, could not be deleted: %{error}.'
pages_controller:
updated: Page has been successfully updated.
created: Page has been successfully created.

View file

@ -66,6 +66,8 @@ fr:
updated: L'utilisateur a été mis à jour avec succès vers %{type}
organizations_controller:
credit_updated: Crédits actualisés avec succès
deletion_scheduled: 'Organization, "%{organization_name}", deletion is scheduled.'
error: 'Your organization, %{organization_name}, could not be deleted: %{error}.'
pages_controller:
updated: La page a été mise à jour avec succès.
created: La page a été créée avec succès.

View file

@ -147,7 +147,7 @@ en:
submit_html: Banish %{user}
delete:
heading: Delete %{user}'s account
onsubmit: Are you sure? This actions is irreversible.
onsubmit: Are you sure? This action is irreversible.
desc2: Once deleted, all data relating to %{user} will be removed from our database.
notice: Only Super Admins are allowed to delete users.
submit: Delete now
@ -389,3 +389,22 @@ en:
facebook: Facebook
github: GitHub
twitter: Twitter
organizations:
profile:
id: ID %{organizationid}
since_html: Organization since %{time}
email: Email
github: GitHub
twitter: Twitter
visit: Visit organization
total_members: "%{count} members"
delete:
heading: Delete %{organization}
onsubmit: Are you sure? This action is irreversible.
desc2: "Note: Deleting organization, %{organization}, will dissasociate the existing members and articles from the organization but will not delete them."
role_notice: Only Super Admins are allowed to delete organizations.
credits_notice: You cannot delete an organization that has associated credits.
submit: Delete organizion now
options:
delete: Delete organization
icon: Options

View file

@ -17,7 +17,7 @@ fr:
description: Toutes les actions du modérateur affectent le score. Le score global est une combinaison des actions des modérateurs et des réactions du public.
no_flags: Le commentaire n'a pas de drapeaux.
no_quality_reactions: Le commentaire n'a pas de réactions de qualité par des utilisateurs de confiance.
title: Actions du modérateur
title: Actions du modérateur
shared:
flags:
actions:
@ -147,7 +147,7 @@ fr:
submit_html: Banish %{user}
delete:
heading: Delete %{user}'s account
onsubmit: Are you sure? This actions is irreversible.
onsubmit: Are you sure? This action is irreversible.
desc2: Once deleted, all data relating to %{user} will be removed from our database.
notice: Only Super Admins are allowed to delete users.
submit: Delete now
@ -389,3 +389,22 @@ fr:
facebook: Facebook
github: GitHub
twitter: Twitter
organizations:
profile:
id: ID %{organizationid}
since_html: Organization since %{time}
email: Email
github: GitHub
twitter: Twitter
visit: Visit organization
total_members: "%{count} members"
delete:
heading: Delete %{organization}
onsubmit: Are you sure? This action is irreversible.
desc2: "Note: Deleting organization, %{organization}, will dissasociate the existing members and articles from the organization but will not delete them."
role_notice: Only Super Admins are allowed to delete organizations.
credits_notice: You cannot delete an organization that has associated credits.
submit: Delete organizion now
options:
delete: Delete organization
icon: Options

View file

@ -83,7 +83,7 @@ namespace :admin do
get "/badge_achievements/award_badges", to: "badge_achievements#award"
post "/badge_achievements/award_badges", to: "badge_achievements#award_badges"
resources :comments, only: %i[index show]
resources :organizations, only: %i[index show] do
resources :organizations, only: %i[index show destroy] do
member do
patch "update_org_credits"
end

View file

@ -0,0 +1,70 @@
import { verifyAndDismissFlashMessage } from '../shared/adminUtilities';
function openOrganizationOptions(callback) {
cy.findByRole('button', { name: 'Options' })
.should('have.attr', 'aria-haspopup', 'true')
.should('have.attr', 'aria-expanded', 'false')
.click()
.then(([button]) => {
expect(button.getAttribute('aria-expanded')).to.equal('true');
const dropdownId = button.getAttribute('aria-controls');
cy.get(`#${dropdownId}`).within(callback);
});
}
describe('Manage Organization Options', () => {
describe('As a super admin', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/admin/content_manager/organizations');
});
cy.findByRole('table').within(() => {
cy.findAllByRole('link', { name: '@Awesome Org' }).first().click();
});
});
it('should show a notice about the scheduled job', () => {
openOrganizationOptions(() => {
cy.findByRole('button', { name: 'Delete organization' }).click();
});
cy.getModal().within(() => {
cy.findByRole('button', { name: 'Delete organizion now' }).click();
});
verifyAndDismissFlashMessage(
`Organization, "Awesome Org", deletion is scheduled.`,
);
});
});
describe('As an organization that has existing credits', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/admin/content_manager/organizations');
});
cy.findByRole('table').within(() => {
cy.findAllByRole('link', { name: '@Credits Org' }).first().click();
});
});
it('should show an error', () => {
openOrganizationOptions(() => {
cy.findByRole('button', { name: 'Delete organization' }).click();
});
cy.getModal().within(() => {
cy.findByText(
'You cannot delete an organization that has associated credits.',
).should('exist');
});
});
});
});

View file

@ -0,0 +1,20 @@
/**
* E2E helper function for user admin tests that validates the correct flash notice message appears
*
* @param {string} [message] The expected flash message text
*/
export function verifyAndDismissFlashMessage(message) {
cy.findByTestId('flash-settings_notice')
.as('notice')
.then((element) => {
expect(element.text().trim()).equal(message);
});
cy.get('@notice').within(() => {
cy.findByRole('button', { name: 'Dismiss message' })
.should('have.focus')
.click();
});
cy.findByTestId('flash-settings_notice').should('not.exist');
}

View file

@ -37,7 +37,7 @@ describe('User index view', () => {
cy.findAllByText('Good standing').should('exist');
cy.findByText('Last activity').should('exist');
cy.findByText('Joined on').should('exist');
cy.findByRole('figure').findByText('+ 1').should('exist');
cy.findByRole('figure').findByText('+ 2').should('exist');
});
});
@ -160,7 +160,7 @@ describe('User index view', () => {
);
cy.findByAltText('Many orgs user').should('exist');
cy.findAllByText('Good standing').should('exist');
cy.findByRole('figure').findByText('+ 1').should('exist');
cy.findByRole('figure').findByText('+ 2').should('exist');
});
});

View file

@ -0,0 +1,28 @@
require "rails_helper"
describe Admin::OrganizationsHelper do
describe "#deletion_modal_error_message" do
let(:organization) { create(:organization) }
let(:super_admin) { create(:user, :super_admin) }
let(:admin) { create(:user, :admin) }
context "when the current user is not a super admin" do
before { allow(helper).to receive(:current_user).and_return(admin) }
it "returns the appropriate error messsage" do
error_message = helper.deletion_modal_error_message(organization)
expect(error_message).to eq("Only Super Admins are allowed to delete organizations.")
end
end
context "when there are credits associated to an organization" do
before { allow(helper).to receive(:current_user).and_return(super_admin) }
it "returns the appropriate error messsage" do
Credit.add_to(organization, 10)
error_message = helper.deletion_modal_error_message(organization)
expect(error_message).to eq("You cannot delete an organization that has associated credits.")
end
end
end
end

View file

@ -180,7 +180,7 @@ RSpec.describe "UserOrganization" do
end
it "deletes the organization" do
sidekiq_assert_enqueued_with(job: Organizations::DeleteWorker, args: [org_id, org_admin.id]) do
sidekiq_assert_enqueued_with(job: Organizations::DeleteWorker, args: [org_id, org_admin.id, true]) do
delete organization_path(org_id)
end
end

View file

@ -303,6 +303,24 @@ seeder.create_if_doesnt_exist(Organization, "slug", "org4") do
)
end
seeder.create_if_doesnt_exist(Organization, "slug", "creditsorg") do
organization = Organization.create!(
name: "Credits Org",
summary: Faker::Company.bs,
profile_image: Rails.root.join("app/assets/images/#{rand(1..40)}.png").open,
url: Faker::Internet.url,
slug: "creditsorg",
)
OrganizationMembership.create!(
user_id: many_orgs_user.id,
organization_id: organization.id,
type_of_user: "member",
)
Credit.add_to(organization, 100)
end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "change-password-user@forem.com") do

View file

@ -10,82 +10,87 @@ RSpec.describe Organizations::DeleteWorker, type: :worker do
let(:mailer_class) { NotifyMailer }
let(:mailer) { double }
let(:message_delivery) { double }
let(:update_and_notify_user) { true }
context "when org and user are found" do
it "destroys the org" do
worker.perform(org.id, user.id)
worker.perform(org.id, user.id, update_and_notify_user)
expect(Organization.exists?(id: org.id)).to be(false)
end
it "calls the service" do
allow(delete).to receive(:call)
worker.perform(org.id, user.id)
worker.perform(org.id, user.id, update_and_notify_user)
expect(delete).to have_received(:call).with(org)
end
it "touches the user" do
allow(User).to receive(:find_by) { user }
allow(user).to receive(:touch)
worker.perform(org.id, user.id)
expect(user).to have_received(:touch).with(:organization_info_updated_at)
end
it "busts user's cache" do
bust_cache = EdgeCache::BustUser
allow(bust_cache).to receive(:call)
worker.perform(org.id, user.id)
expect(bust_cache).to have_received(:call).with(user)
end
it "sends the notification" do
allow(ForemInstance).to receive(:smtp_enabled?).and_return(true)
expect do
worker.perform(org.id, user.id)
end.to change(ActionMailer::Base.deliveries, :count).by(1)
end
it "sends the correct notification" do
allow(mailer_class).to receive(:with).and_return(mailer)
allow(mailer).to receive(:organization_deleted_email).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_now)
worker.perform(org.id, user.id)
expect(mailer_class).to have_received(:with).with(org_name: org.name, name: user.name, email: user.email)
expect(mailer).to have_received(:organization_deleted_email)
expect(message_delivery).to have_received(:deliver_now)
end
it "creates an audit_log record" do
expect do
worker.perform(org.id, user.id)
worker.perform(org.id, user.id, update_and_notify_user)
end.to change(AuditLog, :count).by(1)
end
it "creates a correct AuditLog record" do
worker.perform(org.id, user.id)
worker.perform(org.id, user.id, update_and_notify_user)
audit_log = AuditLog.find_by(category: "user.organization.delete", slug: "organization_delete",
user_id: user.id)
expect(audit_log).to be_present
expect(audit_log.data["organization_id"]).to eq(org.id)
expect(audit_log.data["organization_slug"]).to eq(org.slug)
end
# rubocop:disable RSpec/NestedGroups
context "when update_and_notify_user is true" do
it "touches the user" do
allow(User).to receive(:find_by) { user }
allow(user).to receive(:touch)
worker.perform(org.id, user.id, update_and_notify_user)
expect(user).to have_received(:touch).with(:organization_info_updated_at)
end
it "busts user's cache" do
bust_cache = EdgeCache::BustUser
allow(bust_cache).to receive(:call)
worker.perform(org.id, user.id, update_and_notify_user)
expect(bust_cache).to have_received(:call).with(user)
end
it "sends the notification" do
allow(ForemInstance).to receive(:smtp_enabled?).and_return(true)
expect do
worker.perform(org.id, user.id, update_and_notify_user)
end.to change(ActionMailer::Base.deliveries, :count).by(1)
end
it "sends the correct notification" do
allow(mailer_class).to receive(:with).and_return(mailer)
allow(mailer).to receive(:organization_deleted_email).and_return(message_delivery)
allow(message_delivery).to receive(:deliver_now)
worker.perform(org.id, user.id, update_and_notify_user)
expect(mailer_class).to have_received(:with).with(org_name: org.name, name: user.name, email: user.email)
expect(mailer).to have_received(:organization_deleted_email)
expect(message_delivery).to have_received(:deliver_now)
end
end
# rubocop:enable RSpec/NestedGroups
end
context "when an org or a user is not found" do
it "doesn't fail" do
worker.perform(-1, -1)
worker.perform(-1, -1, update_and_notify_user)
end
it "doesn't call org delete when a user was not found" do
allow(delete).to receive(:call)
worker.perform(org.id, -1)
worker.perform(org.id, -1, update_and_notify_user)
expect(delete).not_to have_received(:call)
end
it "doesn't call org delete when an org was not found" do
allow(delete).to receive(:call)
worker.perform(-1, org.id)
worker.perform(-1, org.id, update_and_notify_user)
expect(delete).not_to have_received(:call)
end
end