diff --git a/app/controllers/admin/action_logs_controller.rb b/app/controllers/admin/action_logs_controller.rb
index e273dfeae..2d77620df 100644
--- a/app/controllers/admin/action_logs_controller.rb
+++ b/app/controllers/admin/action_logs_controller.rb
@@ -2,8 +2,18 @@
module Admin
class ActionLogsController < BaseController
- def index
- @action_logs = Admin::ActionLog.page(params[:page])
+ before_action :set_action_logs
+
+ def index; end
+
+ private
+
+ def set_action_logs
+ @action_logs = Admin::ActionLogFilter.new(filter_params).results.page(params[:page])
+ end
+
+ def filter_params
+ params.slice(:page, *Admin::ActionLogFilter::KEYS).permit(:page, *Admin::ActionLogFilter::KEYS)
end
end
end
diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb
index 6bc75aa56..88d6e4580 100644
--- a/app/helpers/admin/action_logs_helper.rb
+++ b/app/helpers/admin/action_logs_helper.rb
@@ -9,79 +9,8 @@ module Admin::ActionLogsHelper
end
end
- def relevant_log_changes(log)
- if log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)
- log.recorded_changes.slice('domain')
- elsif log.target_type == 'CustomEmoji' && log.action == :update
- log.recorded_changes.slice('domain', 'visible_in_picker')
- elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
- log.recorded_changes.slice('moderator', 'admin')
- elsif log.target_type == 'User' && [:change_email].include?(log.action)
- log.recorded_changes.slice('email', 'unconfirmed_email')
- elsif log.target_type == 'DomainBlock'
- log.recorded_changes.slice('severity', 'reject_media')
- elsif log.target_type == 'Status' && log.action == :update
- log.recorded_changes.slice('sensitive')
- elsif log.target_type == 'Announcement' && log.action == :update
- log.recorded_changes.slice('text', 'starts_at', 'ends_at', 'all_day')
- end
- end
-
- def log_extra_attributes(hash)
- safe_join(hash.to_a.map { |key, value| safe_join([content_tag(:span, key, class: 'diff-key'), '=', log_change(value)]) }, ' ')
- end
-
- def log_change(val)
- return content_tag(:span, val, class: 'diff-neutral') unless val.is_a?(Array)
- safe_join([content_tag(:span, val.first, class: 'diff-old'), content_tag(:span, val.last, class: 'diff-new')], '→')
- end
-
- def icon_for_log(log)
- case log.target_type
- when 'Account', 'User'
- 'user'
- when 'CustomEmoji'
- 'file'
- when 'Report'
- 'flag'
- when 'DomainBlock'
- 'lock'
- when 'DomainAllow'
- 'plus-circle'
- when 'EmailDomainBlock'
- 'envelope'
- when 'Status'
- 'pencil'
- when 'AccountWarning'
- 'warning'
- when 'Announcement'
- 'bullhorn'
- end
- end
-
- def class_for_log_icon(log)
- case log.action
- when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
- 'positive'
- when :create
- opposite_verbs?(log) ? 'negative' : 'positive'
- when :update, :reset_password, :disable_2fa, :memorialize, :change_email
- 'neutral'
- when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
- 'negative'
- when :destroy
- opposite_verbs?(log) ? 'positive' : 'negative'
- else
- ''
- end
- end
-
private
- def opposite_verbs?(log)
- %w(DomainBlock EmailDomainBlock AccountWarning).include?(log.target_type)
- end
-
def linkable_log_target(record)
case record.class.name
when 'Account'
@@ -99,7 +28,7 @@ module Admin::ActionLogsHelper
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
when 'Announcement'
- link_to "##{record.id}", edit_admin_announcement_path(record.id)
+ link_to truncate(record.text), edit_admin_announcement_path(record.id)
end
end
@@ -118,7 +47,7 @@ module Admin::ActionLogsHelper
I18n.t('admin.action_logs.deleted_status')
end
when 'Announcement'
- "##{attributes['id']}"
+ truncate(attributes['text'])
end
end
end
diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb
index 6ab92939d..ba0ca9638 100644
--- a/app/helpers/admin/filter_helper.rb
+++ b/app/helpers/admin/filter_helper.rb
@@ -10,6 +10,7 @@ module Admin::FilterHelper
InviteFilter::KEYS,
RelationshipFilter::KEYS,
AnnouncementFilter::KEYS,
+ Admin::ActionLogFilter::KEYS,
].flatten.freeze
def filter_link_to(text, link_to_params, link_class_params = link_to_params)
diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js
index 061287f89..51f92de8a 100644
--- a/app/javascript/packs/admin.js
+++ b/app/javascript/packs/admin.js
@@ -30,6 +30,10 @@ delegate(document, '.media-spoiler-hide-button', 'click', () => {
});
});
+delegate(document, '.filter-subset--with-select select', 'change', ({ target }) => {
+ target.form.submit();
+});
+
const onDomainBlockSeverityChange = (target) => {
const rejectMediaDiv = document.querySelector('.input.with_label.domain_block_reject_media');
const rejectReportsDiv = document.querySelector('.input.with_label.domain_block_reject_reports');
diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss
index b0307fb71..cee44f436 100644
--- a/app/javascript/styles/mastodon/admin.scss
+++ b/app/javascript/styles/mastodon/admin.scss
@@ -418,6 +418,11 @@ body,
}
}
+ &--with-select strong {
+ display: block;
+ margin-bottom: 10px;
+ }
+
a {
display: inline-block;
color: $darker-text-color;
@@ -551,19 +556,22 @@ body,
}
.log-entry {
- margin-bottom: 20px;
line-height: 20px;
+ padding: 15px 0;
+ background: $ui-base-color;
+ border-bottom: 1px solid lighten($ui-base-color, 4%);
+
+ &:last-child {
+ border-bottom: 0;
+ }
&__header {
display: flex;
justify-content: flex-start;
align-items: center;
- padding: 10px;
- background: $ui-base-color;
color: $darker-text-color;
- border-radius: 4px 4px 0 0;
font-size: 14px;
- position: relative;
+ padding: 0 10px;
}
&__avatar {
@@ -590,44 +598,6 @@ body,
color: $dark-text-color;
}
- &__extras {
- background: lighten($ui-base-color, 6%);
- border-radius: 0 0 4px 4px;
- padding: 10px;
- color: $darker-text-color;
- font-family: $font-monospace, monospace;
- font-size: 12px;
- word-wrap: break-word;
- min-height: 20px;
- }
-
- &__icon {
- font-size: 28px;
- margin-right: 10px;
- color: $dark-text-color;
- }
-
- &__icon__overlay {
- position: absolute;
- top: 10px;
- right: 10px;
- width: 10px;
- height: 10px;
- border-radius: 50%;
-
- &.positive {
- background: $success-green;
- }
-
- &.negative {
- background: lighten($error-red, 12%);
- }
-
- &.neutral {
- background: $ui-highlight-color;
- }
- }
-
a,
.username,
.target {
@@ -635,18 +605,6 @@ body,
text-decoration: none;
font-weight: 500;
}
-
- .diff-old {
- color: lighten($error-red, 12%);
- }
-
- .diff-neutral {
- color: $secondary-text-color;
- }
-
- .diff-new {
- color: $success-green;
- }
}
a.name-tag,
diff --git a/app/models/admin/action_log_filter.rb b/app/models/admin/action_log_filter.rb
new file mode 100644
index 000000000..0ba7e1609
--- /dev/null
+++ b/app/models/admin/action_log_filter.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+class Admin::ActionLogFilter
+ KEYS = %i(
+ action_type
+ account_id
+ target_account_id
+ ).freeze
+
+ ACTION_TYPE_MAP = {
+ assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
+ change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
+ confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
+ create_account_warning: { target_type: 'AccountWarning', action: 'create' }.freeze,
+ create_announcement: { target_type: 'Announcement', action: 'create' }.freeze,
+ create_custom_emoji: { target_type: 'CustomEmoji', action: 'create' }.freeze,
+ create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
+ create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
+ create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
+ demote_user: { target_type: 'User', action: 'demote' }.freeze,
+ destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
+ destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
+ destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
+ destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
+ destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
+ destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
+ disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
+ disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
+ disable_user: { target_type: 'User', action: 'disable' }.freeze,
+ enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
+ enable_user: { target_type: 'User', action: 'enable' }.freeze,
+ memorialize_account: { target_type: 'Account', action: 'memorialize' }.freeze,
+ promote_user: { target_type: 'User', action: 'promote' }.freeze,
+ remove_avatar_user: { target_type: 'User', action: 'remove_avatar' }.freeze,
+ reopen_report: { target_type: 'Report', action: 'reopen' }.freeze,
+ reset_password_user: { target_type: 'User', action: 'reset_password' }.freeze,
+ resolve_report: { target_type: 'Report', action: 'resolve' }.freeze,
+ silence_account: { target_type: 'Account', action: 'silence' }.freeze,
+ suspend_account: { target_type: 'Account', action: 'suspend' }.freeze,
+ unassigned_report: { target_type: 'Report', action: 'unassigned' }.freeze,
+ unsilence_account: { target_type: 'Account', action: 'unsilence' }.freeze,
+ unsuspend_account: { target_type: 'Account', action: 'unsuspend' }.freeze,
+ update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
+ update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
+ update_status: { target_type: 'Status', action: 'update' }.freeze,
+ }.freeze
+
+ attr_reader :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ def results
+ scope = Admin::ActionLog.includes(:target)
+
+ params.each do |key, value|
+ next if key.to_s == 'page'
+
+ scope.merge!(scope_for(key.to_s, value.to_s.strip)) if value.present?
+ end
+
+ scope
+ end
+
+ private
+
+ def scope_for(key, value)
+ case key
+ when 'action_type'
+ Admin::ActionLog.where(ACTION_TYPE_MAP[value.to_sym])
+ when 'account_id'
+ Admin::ActionLog.where(account_id: value)
+ when 'target_account_id'
+ account = Account.find(value)
+ Admin::ActionLog.where(target: [account, account.user].compact)
+ else
+ raise "Unknown filter: #{key}"
+ end
+ end
+end
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 744d17d1f..965fd6fb6 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -53,7 +53,7 @@
.dashboard__counters__num= number_with_delimiter @account.targeted_reports.count
.dashboard__counters__label= t '.targeted_reports'
%div
- %div
+ = link_to admin_action_logs_path(target_account_id: @account.id) do
.dashboard__counters__text
- if @account.local? && @account.user.nil?
%span.neutral= t('admin.accounts.deleted')
diff --git a/app/views/admin/action_logs/_action_log.html.haml b/app/views/admin/action_logs/_action_log.html.haml
index a545e189e..59905f341 100644
--- a/app/views/admin/action_logs/_action_log.html.haml
+++ b/app/views/admin/action_logs/_action_log.html.haml
@@ -7,9 +7,3 @@
= t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe
.log-entry__timestamp
%time.formatted{ datetime: action_log.created_at.iso8601 }
- .spacer
- .log-entry__icon
- = fa_icon icon_for_log(action_log)
- .log-entry__icon__overlay{ class: class_for_log_icon(action_log) }
- .log-entry__extras
- = log_extra_attributes relevant_log_changes(action_log)
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index a4d3871a9..99f756762 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -1,6 +1,28 @@
- content_for :page_title do
= t('admin.action_logs.title')
-= render @action_logs
+- content_for :header_tags do
+ = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
+
+= form_tag admin_action_logs_url, method: 'GET', class: 'simple_form' do
+ = hidden_field_tag :target_account_id, params[:target_account_id] if params[:target_account_id].present?
+
+ .filters
+ .filter-subset.filter-subset--with-select
+ %strong= t('admin.action_logs.filter_by_user')
+ .input.select.optional
+ = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
+
+ .filter-subset.filter-subset--with-select
+ %strong= t('admin.action_logs.filter_by_action')
+ .input.select.optional
+ = select_tag :action_type, options_for_select(Admin::ActionLogFilter::ACTION_TYPE_MAP.keys.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key]}, params[:action_type]), prompt: I18n.t('admin.accounts.moderation.all')
+
+- if @action_logs.empty?
+ %div.muted-hint.center-text
+ = t 'admin.action_logs.empty'
+- else
+ .announcements-list
+ = render @action_logs
= paginate @action_logs
diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb
index aa1517256..4fec4320c 100644
--- a/config/initializers/kaminari_config.rb
+++ b/config/initializers/kaminari_config.rb
@@ -2,6 +2,6 @@
Kaminari.configure do |config|
config.default_per_page = 40
- config.window = 1
+ config.window = 2
config.outer_window = 1
end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index ef21fa54a..d07c4bb99 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -195,6 +195,42 @@ en:
web: Web
whitelisted: Whitelisted
action_logs:
+ action_types:
+ assigned_to_self_report: Assign Report
+ change_email_user: Change E-mail for User
+ confirm_user: Confirm User
+ create_account_warning: Create Warning
+ create_announcement: Create Announcement
+ create_custom_emoji: Create Custom Emoji
+ create_domain_allow: Create Domain Allow
+ create_domain_block: Create Domain Block
+ create_email_domain_block: Create E-mail Domain Block
+ demote_user: Demote User
+ destroy_announcement: Delete Announcement
+ destroy_custom_emoji: Delete Custom Emoji
+ destroy_domain_allow: Delete Domain Allow
+ destroy_domain_block: Delete Domain Block
+ destroy_email_domain_block: Delete e-mail domain block
+ destroy_status: Delete Status
+ disable_2fa_user: Disable 2FA
+ disable_custom_emoji: Disable Custom Emoji
+ disable_user: Disable User
+ enable_custom_emoji: Enable Custom Emoji
+ enable_user: Enable User
+ memorialize_account: Memorialize Account
+ promote_user: Promote User
+ remove_avatar_user: Remove Avatar
+ reopen_report: Reopen Report
+ reset_password_user: Reset Password
+ resolve_report: Resolve Report
+ silence_account: Silence Account
+ suspend_account: Suspend Account
+ unassigned_report: Unassign Report
+ unsilence_account: Unsilence Account
+ unsuspend_account: Unsuspend Account
+ update_announcement: Update Announcement
+ update_custom_emoji: Update Custom Emoji
+ update_status: Update Status
actions:
assigned_to_self_report: "%{name} assigned report %{target} to themselves"
change_email_user: "%{name} changed the e-mail address of user %{target}"
@@ -232,6 +268,9 @@ en:
update_custom_emoji: "%{name} updated emoji %{target}"
update_status: "%{name} updated status by %{target}"
deleted_status: "(deleted status)"
+ empty: No logs found.
+ filter_by_action: Filter by action
+ filter_by_user: Filter by user
title: Audit log
announcements:
destroyed_msg: Announcement successfully deleted!
diff --git a/spec/helpers/admin/action_log_helper_spec.rb b/spec/helpers/admin/action_log_helper_spec.rb
index d7af6b939..60f5ecdcc 100644
--- a/spec/helpers/admin/action_log_helper_spec.rb
+++ b/spec/helpers/admin/action_log_helper_spec.rb
@@ -31,242 +31,4 @@ RSpec.describe Admin::ActionLogsHelper, type: :helper do
end
end
end
-
- describe '#relevant_log_changes' do
- let(:log) { double(target_type: target_type, action: log_action, recorded_changes: recorded_changes) }
- let(:recorded_changes) { double }
-
- after do
- hoge.relevant_log_changes(log)
- end
-
- context "log.target_type == 'CustomEmoji' && [:enable, :disable, :destroy].include?(log.action)" do
- let(:target_type) { 'CustomEmoji' }
- let(:log_action) { :enable }
-
- it "calls log.recorded_changes.slice('domain')" do
- expect(recorded_changes).to receive(:slice).with('domain')
- end
- end
-
- context "log.target_type == 'CustomEmoji' && log.action == :update" do
- let(:target_type) { 'CustomEmoji' }
- let(:log_action) { :update }
-
- it "calls log.recorded_changes.slice('domain', 'visible_in_picker')" do
- expect(recorded_changes).to receive(:slice).with('domain', 'visible_in_picker')
- end
- end
-
- context "log.target_type == 'User' && [:promote, :demote].include?(log.action)" do
- let(:target_type) { 'User' }
- let(:log_action) { :promote }
-
- it "calls log.recorded_changes.slice('moderator', 'admin')" do
- expect(recorded_changes).to receive(:slice).with('moderator', 'admin')
- end
- end
-
- context "log.target_type == 'User' && [:change_email].include?(log.action)" do
- let(:target_type) { 'User' }
- let(:log_action) { :change_email }
-
- it "calls log.recorded_changes.slice('email', 'unconfirmed_email')" do
- expect(recorded_changes).to receive(:slice).with('email', 'unconfirmed_email')
- end
- end
-
- context "log.target_type == 'DomainBlock'" do
- let(:target_type) { 'DomainBlock' }
- let(:log_action) { nil }
-
- it "calls log.recorded_changes.slice('severity', 'reject_media')" do
- expect(recorded_changes).to receive(:slice).with('severity', 'reject_media')
- end
- end
-
- context "log.target_type == 'Status' && log.action == :update" do
- let(:target_type) { 'Status' }
- let(:log_action) { :update }
-
- it "log.recorded_changes.slice('sensitive')" do
- expect(recorded_changes).to receive(:slice).with('sensitive')
- end
- end
- end
-
- describe '#log_extra_attributes' do
- after do
- hoge.log_extra_attributes(hoge: 'hoge')
- end
-
- it "calls content_tag(:span, key, class: 'diff-key')" do
- allow(hoge).to receive(:log_change).with(anything)
- expect(hoge).to receive(:content_tag).with(:span, :hoge, class: 'diff-key')
- end
-
- it 'calls safe_join twice' do
- expect(hoge).to receive(:safe_join).with(
- ['hoge',
- '=',
- 'hoge']
- )
-
- expect(hoge).to receive(:safe_join).with([nil], ' ')
- end
- end
-
- describe '#log_change' do
- after do
- hoge.log_change(val)
- end
-
- context '!val.is_a?(Array)' do
- let(:val) { 'hoge' }
-
- it "calls content_tag(:span, val, class: 'diff-neutral')" do
- expect(hoge).to receive(:content_tag).with(:span, val, class: 'diff-neutral')
- end
- end
-
- context 'val.is_a?(Array)' do
- let(:val) { %w(foo bar) }
-
- it 'calls #content_tag twice and #safe_join' do
- expect(hoge).to receive(:content_tag).with(:span, 'foo', class: 'diff-old')
- expect(hoge).to receive(:content_tag).with(:span, 'bar', class: 'diff-new')
- expect(hoge).to receive(:safe_join).with([nil, nil], '→')
- end
- end
- end
-
- describe '#icon_for_log' do
- subject { hoge.icon_for_log(log) }
-
- context "log.target_type == 'Account'" do
- let(:log) { double(target_type: 'Account') }
-
- it 'returns "user"' do
- expect(subject).to be 'user'
- end
- end
-
- context "log.target_type == 'User'" do
- let(:log) { double(target_type: 'User') }
-
- it 'returns "user"' do
- expect(subject).to be 'user'
- end
- end
-
- context "log.target_type == 'CustomEmoji'" do
- let(:log) { double(target_type: 'CustomEmoji') }
-
- it 'returns "file"' do
- expect(subject).to be 'file'
- end
- end
-
- context "log.target_type == 'Report'" do
- let(:log) { double(target_type: 'Report') }
-
- it 'returns "flag"' do
- expect(subject).to be 'flag'
- end
- end
-
- context "log.target_type == 'DomainBlock'" do
- let(:log) { double(target_type: 'DomainBlock') }
-
- it 'returns "lock"' do
- expect(subject).to be 'lock'
- end
- end
-
- context "log.target_type == 'EmailDomainBlock'" do
- let(:log) { double(target_type: 'EmailDomainBlock') }
-
- it 'returns "envelope"' do
- expect(subject).to be 'envelope'
- end
- end
-
- context "log.target_type == 'Status'" do
- let(:log) { double(target_type: 'Status') }
-
- it 'returns "pencil"' do
- expect(subject).to be 'pencil'
- end
- end
- end
-
- describe '#class_for_log_icon' do
- subject { hoge.class_for_log_icon(log) }
-
- %i(enable unsuspend unsilence confirm promote resolve).each do |action|
- context "log.action == #{action}" do
- let(:log) { double(action: action) }
-
- it 'returns "positive"' do
- expect(subject).to be 'positive'
- end
- end
- end
-
- context 'log.action == :create' do
- context 'opposite_verbs?(log)' do
- let(:log) { double(action: :create, target_type: 'DomainBlock') }
-
- it 'returns "negative"' do
- expect(subject).to be 'negative'
- end
- end
-
- context '!opposite_verbs?(log)' do
- let(:log) { double(action: :create, target_type: '') }
-
- it 'returns "positive"' do
- expect(subject).to be 'positive'
- end
- end
- end
-
- %i(update reset_password disable_2fa memorialize change_email).each do |action|
- context "log.action == #{action}" do
- let(:log) { double(action: action) }
-
- it 'returns "neutral"' do
- expect(subject).to be 'neutral'
- end
- end
- end
-
- %i(demote silence disable suspend remove_avatar remove_header reopen).each do |action|
- context "log.action == #{action}" do
- let(:log) { double(action: action) }
-
- it 'returns "negative"' do
- expect(subject).to be 'negative'
- end
- end
- end
-
- context 'log.action == :destroy' do
- context 'opposite_verbs?(log)' do
- let(:log) { double(action: :destroy, target_type: 'DomainBlock') }
-
- it 'returns "positive"' do
- expect(subject).to be 'positive'
- end
- end
-
- context '!opposite_verbs?(log)' do
- let(:log) { double(action: :destroy, target_type: '') }
-
- it 'returns "negative"' do
- expect(subject).to be 'negative'
- end
- end
- end
- end
end