mirror of https://github.com/mastodon/mastodon
Add notifications of severed relationships (#27511)
parent
8a1423a474
commit
44bf7b8128
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SeveredRelationshipsController < ApplicationController
|
||||||
|
layout 'admin'
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :set_body_classes
|
||||||
|
before_action :set_cache_headers
|
||||||
|
|
||||||
|
before_action :set_event, only: [:following, :followers]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@events = AccountRelationshipSeveranceEvent.where(account: current_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
def following
|
||||||
|
respond_to do |format|
|
||||||
|
format.csv { send_data following_data, filename: "following-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers
|
||||||
|
respond_to do |format|
|
||||||
|
format.csv { send_data followers_data, filename: "followers-#{@event.target_name}-#{@event.created_at.to_date.iso8601}.csv" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_event
|
||||||
|
@event = AccountRelationshipSeveranceEvent.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def following_data
|
||||||
|
CSV.generate(headers: ['Account address', 'Show boosts', 'Notify on new posts', 'Languages'], write_headers: true) do |csv|
|
||||||
|
@event.severed_relationships.active.where(local_account: current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
|
||||||
|
csv << [acct(follow.target_account), follow.show_reblogs, follow.notify, follow.languages&.join(', ')]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def followers_data
|
||||||
|
CSV.generate(headers: ['Account address'], write_headers: true) do |csv|
|
||||||
|
@event.severed_relationships.passive.where(local_account: current_account).includes(:remote_account).reorder(id: :desc).each do |follow|
|
||||||
|
csv << [acct(follow.account)]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def acct(account)
|
||||||
|
account.local? ? account.local_username_and_domain : account.acct
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_body_classes
|
||||||
|
@body_classes = 'admin'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_cache_headers
|
||||||
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,61 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
|
||||||
|
// This needs to be kept in sync with app/models/relationship_severance_event.rb
|
||||||
|
const messages = defineMessages({
|
||||||
|
account_suspension: { id: 'relationship_severance_notification.types.account_suspension', defaultMessage: 'Account has been suspended' },
|
||||||
|
domain_block: { id: 'relationship_severance_notification.types.domain_block', defaultMessage: 'Domain has been suspended' },
|
||||||
|
user_domain_block: { id: 'relationship_severance_notification.types.user_domain_block', defaultMessage: 'You blocked this domain' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const RelationshipsSeveranceEvent = ({ event, hidden }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (hidden || !event) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='notification__report'>
|
||||||
|
<div className='notification__report__details'>
|
||||||
|
<div>
|
||||||
|
<RelativeTimestamp timestamp={event.get('created_at')} short={false} />
|
||||||
|
{' · '}
|
||||||
|
{ event.get('purged') ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='relationship_severance_notification.purged_data'
|
||||||
|
defaultMessage='purged by administrators'
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='relationship_severance_notification.relationships'
|
||||||
|
defaultMessage='{count, plural, one {# relationship} other {# relationships}}'
|
||||||
|
values={{ count: event.get('relationships_count', 0) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<br />
|
||||||
|
<strong>{intl.formatMessage(messages[event.get('type')])}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='notification__report__actions'>
|
||||||
|
<a href='/severed_relationships' className='button' target='_blank' rel='noopener noreferrer'>
|
||||||
|
<FormattedMessage id='relationship_severance_notification.view' defaultMessage='View' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
RelationshipsSeveranceEvent.propTypes = {
|
||||||
|
event: ImmutablePropTypes.map.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RelationshipsSeveranceEvent;
|
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>
|
After Width: | Height: | Size: 414 B |
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m770-302-60-62q40-11 65-42.5t25-73.5q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 57-29.5 105T770-302ZM634-440l-80-80h86v80h-6ZM792-56 56-792l56-56 736 736-56 56ZM440-280H280q-83 0-141.5-58.5T80-480q0-69 42-123t108-71l74 74h-24q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h65l79 80H320Z"/></svg>
|
After Width: | Height: | Size: 414 B |
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: account_relationship_severance_events
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# account_id :bigint(8) not null
|
||||||
|
# relationship_severance_event_id :bigint(8) not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class AccountRelationshipSeveranceEvent < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
belongs_to :relationship_severance_event
|
||||||
|
|
||||||
|
delegate :severed_relationships, :type, :target_name, :purged, to: :relationship_severance_event, prefix: false
|
||||||
|
|
||||||
|
before_create :set_relationships_count!
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_relationships_count!
|
||||||
|
self.relationships_count = severed_relationships.where(local_account: account).count
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: relationship_severance_events
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# type :integer not null
|
||||||
|
# target_name :string not null
|
||||||
|
# purged :boolean default(FALSE), not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class RelationshipSeveranceEvent < ApplicationRecord
|
||||||
|
self.inheritance_column = nil
|
||||||
|
|
||||||
|
has_many :severed_relationships, inverse_of: :relationship_severance_event, dependent: :delete_all
|
||||||
|
|
||||||
|
enum type: {
|
||||||
|
domain_block: 0,
|
||||||
|
user_domain_block: 1,
|
||||||
|
account_suspension: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
scope :about_local_account, ->(account) { where(id: SeveredRelationship.about_local_account(account).select(:relationship_severance_event_id)) }
|
||||||
|
|
||||||
|
def import_from_active_follows!(follows)
|
||||||
|
import_from_follows!(follows, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_from_passive_follows!(follows)
|
||||||
|
import_from_follows!(follows, false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def affected_local_accounts
|
||||||
|
Account.where(id: severed_relationships.select(:local_account_id))
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def import_from_follows!(follows, active)
|
||||||
|
SeveredRelationship.insert_all(
|
||||||
|
follows.pluck(:account_id, :target_account_id, :show_reblogs, :notify, :languages).map do |account_id, target_account_id, show_reblogs, notify, languages|
|
||||||
|
{
|
||||||
|
local_account_id: active ? account_id : target_account_id,
|
||||||
|
remote_account_id: active ? target_account_id : account_id,
|
||||||
|
show_reblogs: show_reblogs,
|
||||||
|
notify: notify,
|
||||||
|
languages: languages,
|
||||||
|
relationship_severance_event_id: id,
|
||||||
|
direction: active ? :active : :passive,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: severed_relationships
|
||||||
|
#
|
||||||
|
# id :bigint(8) not null, primary key
|
||||||
|
# relationship_severance_event_id :bigint(8) not null
|
||||||
|
# local_account_id :bigint(8) not null
|
||||||
|
# remote_account_id :bigint(8) not null
|
||||||
|
# direction :integer not null
|
||||||
|
# show_reblogs :boolean
|
||||||
|
# notify :boolean
|
||||||
|
# languages :string is an Array
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
#
|
||||||
|
class SeveredRelationship < ApplicationRecord
|
||||||
|
belongs_to :relationship_severance_event
|
||||||
|
belongs_to :local_account, class_name: 'Account'
|
||||||
|
belongs_to :remote_account, class_name: 'Account'
|
||||||
|
|
||||||
|
enum direction: {
|
||||||
|
passive: 0, # analogous to `local_account.passive_relationships`
|
||||||
|
active: 1, # analogous to `local_account.active_relationships`
|
||||||
|
}
|
||||||
|
|
||||||
|
scope :about_local_account, ->(account) { where(local_account: account) }
|
||||||
|
|
||||||
|
scope :active, -> { where(direction: :active) }
|
||||||
|
scope :passive, -> { where(direction: :passive) }
|
||||||
|
|
||||||
|
def account
|
||||||
|
active? ? local_account : remote_account
|
||||||
|
end
|
||||||
|
|
||||||
|
def target_account
|
||||||
|
active? ? remote_account : local_account
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,9 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class REST::AccountRelationshipSeveranceEventSerializer < ActiveModel::Serializer
|
||||||
|
attributes :id, :type, :purged, :target_name, :created_at
|
||||||
|
|
||||||
|
def id
|
||||||
|
object.id.to_s
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,34 @@
|
|||||||
|
- content_for :page_title do
|
||||||
|
= t('settings.severed_relationships')
|
||||||
|
|
||||||
|
%p.muted-hint= t('severed_relationships.preamble')
|
||||||
|
|
||||||
|
- unless @events.empty?
|
||||||
|
.table-wrapper
|
||||||
|
%table.table
|
||||||
|
%thead
|
||||||
|
%tr
|
||||||
|
%th= t('exports.archive_takeout.date')
|
||||||
|
%th= t('severed_relationships.type')
|
||||||
|
%th= t('severed_relationships.lost_follows')
|
||||||
|
%th= t('severed_relationships.lost_followers')
|
||||||
|
%tbody
|
||||||
|
- @events.each do |event|
|
||||||
|
%tr
|
||||||
|
%td= l event.created_at
|
||||||
|
%td= t("severed_relationships.event_type.#{event.type}", target_name: event.target_name)
|
||||||
|
- if event.purged?
|
||||||
|
%td{ rowspan: 2 }= t('severed_relationships.purged')
|
||||||
|
- else
|
||||||
|
%td
|
||||||
|
- count = event.severed_relationships.active.where(local_account: current_account).count
|
||||||
|
- if count.zero?
|
||||||
|
= t('generic.none')
|
||||||
|
- else
|
||||||
|
= table_link_to 'download', t('severed_relationships.download', count: count), following_severed_relationship_path(event, format: :csv)
|
||||||
|
%td
|
||||||
|
- count = event.severed_relationships.passive.where(local_account: current_account).count
|
||||||
|
- if count.zero?
|
||||||
|
= t('generic.none')
|
||||||
|
- else
|
||||||
|
= table_link_to 'download', t('severed_relationships.download', count: count), followers_severed_relationship_path(event, format: :csv)
|
@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateRelationshipSeveranceEvents < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :relationship_severance_events do |t|
|
||||||
|
t.integer :type, null: false
|
||||||
|
t.string :target_name, null: false
|
||||||
|
t.boolean :purged, null: false, default: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.index [:type, :target_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateSeveredRelationships < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :severed_relationships do |t|
|
||||||
|
# No need to have an index on this foreign key as it is covered by `index_severed_relationships_on_unique_tuples`
|
||||||
|
t.references :relationship_severance_event, null: false, foreign_key: { on_delete: :cascade }, index: false
|
||||||
|
|
||||||
|
# No need to have an index on this foregin key as it is covered by `index_severed_relationships_on_local_account_and_event`
|
||||||
|
t.references :local_account, null: false, foreign_key: { to_table: :accounts, on_delete: :cascade }, index: false
|
||||||
|
t.references :remote_account, null: false, foreign_key: { to_table: :accounts, on_delete: :cascade }
|
||||||
|
|
||||||
|
# Used to describe whether `local_account` is the active (follower) or passive (followed) part of the relationship
|
||||||
|
t.integer :direction, null: false
|
||||||
|
|
||||||
|
# Those attributes are carried over from the `follows` table
|
||||||
|
t.boolean :show_reblogs
|
||||||
|
t.boolean :notify
|
||||||
|
t.string :languages, array: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
|
||||||
|
t.index [:relationship_severance_event_id, :local_account_id, :direction, :remote_account_id], name: 'index_severed_relationships_on_unique_tuples', unique: true
|
||||||
|
t.index [:local_account_id, :relationship_severance_event_id], name: 'index_severed_relationships_on_local_account_and_event'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateAccountRelationshipSeveranceEvents < ActiveRecord::Migration[7.1]
|
||||||
|
def change
|
||||||
|
create_table :account_relationship_severance_events do |t|
|
||||||
|
t.belongs_to :account, foreign_key: { on_delete: :cascade }, null: false, index: { unique: true }
|
||||||
|
t.belongs_to :relationship_severance_event, foreign_key: { on_delete: :cascade }, null: false, index: { unique: true }
|
||||||
|
|
||||||
|
t.integer :relationships_count, default: 0, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:account_relationship_severance_event) do
|
||||||
|
account
|
||||||
|
relationship_severance_event
|
||||||
|
end
|
@ -0,0 +1,6 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:relationship_severance_event) do
|
||||||
|
type { :domain_block }
|
||||||
|
target_name { 'example.com' }
|
||||||
|
end
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
Fabricator(:severed_relationship) do
|
||||||
|
local_account { Fabricate.build(:account) }
|
||||||
|
remote_account { Fabricate.build(:account) }
|
||||||
|
relationship_severance_event { Fabricate.build(:relationship_severance_event) }
|
||||||
|
direction { :active }
|
||||||
|
end
|
@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe RelationshipSeveranceEvent do
|
||||||
|
let(:local_account) { Fabricate(:account) }
|
||||||
|
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:event) { Fabricate(:relationship_severance_event) }
|
||||||
|
|
||||||
|
describe '#import_from_active_follows!' do
|
||||||
|
before do
|
||||||
|
local_account.follow!(remote_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports the follow relationships with the expected direction' do
|
||||||
|
event.import_from_active_follows!(local_account.active_relationships)
|
||||||
|
|
||||||
|
relationships = event.severed_relationships.to_a
|
||||||
|
expect(relationships.size).to eq 1
|
||||||
|
expect(relationships[0].account).to eq local_account
|
||||||
|
expect(relationships[0].target_account).to eq remote_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#import_from_passive_follows!' do
|
||||||
|
before do
|
||||||
|
remote_account.follow!(local_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'imports the follow relationships with the expected direction' do
|
||||||
|
event.import_from_passive_follows!(local_account.passive_relationships)
|
||||||
|
|
||||||
|
relationships = event.severed_relationships.to_a
|
||||||
|
expect(relationships.size).to eq 1
|
||||||
|
expect(relationships[0].account).to eq remote_account
|
||||||
|
expect(relationships[0].target_account).to eq local_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#affected_local_accounts' do
|
||||||
|
before do
|
||||||
|
event.severed_relationships.create!(local_account: local_account, remote_account: remote_account, direction: :active)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'correctly lists local accounts' do
|
||||||
|
expect(event.affected_local_accounts.to_a).to contain_exactly(local_account)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SeveredRelationship do
|
||||||
|
let(:local_account) { Fabricate(:account) }
|
||||||
|
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:event) { Fabricate(:relationship_severance_event) }
|
||||||
|
|
||||||
|
describe '#account' do
|
||||||
|
context 'when the local account is the follower' do
|
||||||
|
let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) }
|
||||||
|
|
||||||
|
it 'returns the local account' do
|
||||||
|
expect(severed_relationship.account).to eq local_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the local account is being followed' do
|
||||||
|
let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) }
|
||||||
|
|
||||||
|
it 'returns the remote account' do
|
||||||
|
expect(severed_relationship.account).to eq remote_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#target_account' do
|
||||||
|
context 'when the local account is the follower' do
|
||||||
|
let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :active) }
|
||||||
|
|
||||||
|
it 'returns the remote account' do
|
||||||
|
expect(severed_relationship.target_account).to eq remote_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the local account is being followed' do
|
||||||
|
let(:severed_relationship) { Fabricate(:severed_relationship, relationship_severance_event: event, local_account: local_account, remote_account: remote_account, direction: :passive) }
|
||||||
|
|
||||||
|
it 'returns the local account' do
|
||||||
|
expect(severed_relationship.target_account).to eq local_account
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe 'Severed relationships page' do
|
||||||
|
include RoutingHelper
|
||||||
|
|
||||||
|
describe 'GET severed_relationships#index' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in user
|
||||||
|
|
||||||
|
Fabricate(:severed_relationship, local_account: user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
get severed_relationships_path
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue