Implement relationship logic as use case

This commit is contained in:
Karina Kwiatek 2022-01-03 00:31:55 +01:00 committed by Karina Kwiatek
parent d20e07ee19
commit 3962671135
16 changed files with 347 additions and 222 deletions

View File

@ -1,39 +0,0 @@
class Ajax::FriendController < AjaxController
def create
params.require :screen_name
target_user = User.find_by_screen_name(params[:screen_name])
begin
current_user.follow target_user
rescue => e
Sentry.capture_exception(e)
@response[:status] = :fail
@response[:message] = I18n.t('messages.friend.create.fail')
return
end
@response[:status] = :okay
@response[:message] = I18n.t('messages.friend.create.okay')
@response[:success] = true
end
def destroy
params.require :screen_name
target_user = User.find_by_screen_name(params[:screen_name])
begin
current_user.unfollow target_user
rescue => e
Sentry.capture_exception(e)
@response[:status] = :fail
@response[:message] = I18n.t('messages.friend.destroy.fail')
return
end
@response[:status] = :okay
@response[:message] = I18n.t('messages.friend.destroy.okay')
@response[:success] = true
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require "use_case/relationship/create"
require "use_case/relationship/destroy"
require "errors"
class Ajax::RelationshipController < AjaxController
before_action :authenticate_user!
def create
UseCase::Relationship::Create.call(
current_user_id: current_user.id,
target_user: params[:target_user],
type: params[:type]
)
@response[:success] = true
@response[:message] = t(".success")
rescue Errors::Base => e
@response[:message] = t(e.locale_tag)
ensure
return_response
end
def destroy
UseCase::Relationship::Destroy.call(
current_user_id: current_user.id,
target_user: params[:target_user],
type: params[:type]
)
@response[:success] = true
@response[:message] = t(".success")
rescue Errors::Base => e
@response[:message] = t(e.locale_tag)
ensure
return_response
end
end

View File

@ -7,14 +7,15 @@ export function userActionHandler(event: Event): void {
const target = button.dataset.target; const target = button.dataset.target;
const action = button.dataset.action; const action = button.dataset.action;
const targetURL = action === 'follow' ? '/ajax/create_friend' : '/ajax/destroy_friend'; const targetURL = action === 'follow' ? '/ajax/create_relationship' : '/ajax/destroy_relationship';
let success = false; let success = false;
Rails.ajax({ Rails.ajax({
url: targetURL, url: targetURL,
type: 'POST', type: 'POST',
data: new URLSearchParams({ data: new URLSearchParams({
screen_name: target screen_name: target,
type: "follow"
}).toString(), }).toString(),
success: (data) => { success: (data) => {
success = data.success; success = data.success;

View File

@ -95,16 +95,6 @@ class User < ApplicationRecord
end end
end end
# follows an user.
def follow(target_user)
active_relationships.create(target: target_user)
end
# unfollows an user
def unfollow(target_user)
active_relationships.find_by(target: target_user).destroy
end
# @param list [List] # @param list [List]
# @return [Boolean] true if +self+ is a member of +list+ # @return [Boolean] true if +self+ is a member of +list+
def member_of?(list) def member_of?(list)

View File

@ -20,9 +20,7 @@ class User
# Follow an user # Follow an user
def follow(target_user) def follow(target_user)
raise Justask::Errors::FollowingOtherBlockedSelf if target_user.blocking?(self) raise Errors::FollowingSelf if target_user == self
raise Justask::Errors::FollowingSelfBlockedOther if blocking?(target_user)
raise Justask::Errors::FollowingSelf if target_user == self
create_relationship(active_follow_relationships, target_user) create_relationship(active_follow_relationships, target_user)
end end

View File

@ -457,3 +457,24 @@ en:
invalid_code: "The code you entered was invalid." invalid_code: "The code you entered was invalid."
setup: setup:
success: "Two factor authentication has been enabled for your account." success: "Two factor authentication has been enabled for your account."
errors:
base: "An error occurred"
bad_request: "Bad Request"
param_is_missing: "param is missing"
forbidden: "This is illegal, you know"
blocked: "You have been blocked from performing this request"
other_blocked_self: "You have been blocked by this user"
asking_other_blocked_self: "You have been blocked from asking this user questions"
following_other_blocked_self: "You have been blocked from following this user"
self_blocked_other: "You cannot do this while blocking this user"
asking_self_blocked_other: "You cannot ask an user who you are currently blocking"
following_self_blocked_other: "You cannot follow an user who you are currently blocking"
self_action: "You cannot do this to yourself"
following_self: "You cannot follow yourself"
not_found: "That does not exist"
user_not_found: "User not found"
conflict: "This already exists"

View File

@ -102,8 +102,8 @@ Rails.application.routes.draw do
match '/delete_all_inbox/:author', to: 'inbox#remove_all_author', via: :post, as: :delete_all_author match '/delete_all_inbox/:author', to: 'inbox#remove_all_author', via: :post, as: :delete_all_author
match '/answer', to: 'answer#create', via: :post, as: :answer match '/answer', to: 'answer#create', via: :post, as: :answer
match '/destroy_answer', to: 'answer#destroy', via: :post, as: :destroy_answer match '/destroy_answer', to: 'answer#destroy', via: :post, as: :destroy_answer
match '/create_friend', to: 'friend#create', via: :post, as: :create_friend match '/create_relationship', to: 'relationship#create', via: :post, as: :create_relationship
match '/destroy_friend', to: 'friend#destroy', via: :post, as: :destroy_friend match '/destroy_relationship', to: 'relationship#destroy', via: :post, as: :destroy_relationship
match '/create_smile', to: 'smile#create', via: :post, as: :create_smile match '/create_smile', to: 'smile#create', via: :post, as: :create_smile
match '/destroy_smile', to: 'smile#destroy', via: :post, as: :destroy_smile match '/destroy_smile', to: 'smile#destroy', via: :post, as: :destroy_smile
match '/create_comment_smile', to: 'smile#create_comment', via: :post, as: :create_comment_smile match '/create_comment_smile', to: 'smile#create_comment', via: :post, as: :create_comment_smile

View File

@ -122,7 +122,6 @@ ActiveRecord::Schema.define(version: 2022_01_05_171216) do
t.boolean "new" t.boolean "new"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "state"
t.index ["new"], name: "index_notifications_on_new" t.index ["new"], name: "index_notifications_on_new"
t.index ["recipient_id"], name: "index_notifications_on_recipient_id" t.index ["recipient_id"], name: "index_notifications_on_recipient_id"
end end
@ -278,12 +277,7 @@ ActiveRecord::Schema.define(version: 2022_01_05_171216) do
t.integer "asked_count", default: 0, null: false t.integer "asked_count", default: 0, null: false
t.integer "answered_count", default: 0, null: false t.integer "answered_count", default: 0, null: false
t.integer "commented_count", default: 0, null: false t.integer "commented_count", default: 0, null: false
t.string "display_name"
t.integer "smiled_count", default: 0, null: false t.integer "smiled_count", default: 0, null: false
t.string "motivation_header", default: "", null: false
t.string "website", default: "", null: false
t.string "location", default: "", null: false
t.text "bio", default: "", null: false
t.string "profile_picture_file_name" t.string "profile_picture_file_name"
t.boolean "profile_picture_processing" t.boolean "profile_picture_processing"
t.integer "profile_picture_x" t.integer "profile_picture_x"
@ -329,6 +323,5 @@ ActiveRecord::Schema.define(version: 2022_01_05_171216) do
t.index ["user_id"], name: "index_users_roles_on_user_id" t.index ["user_id"], name: "index_users_roles_on_user_id"
end end
add_foreign_key "mute_rules", "users"
add_foreign_key "profiles", "users" add_foreign_key "profiles", "users"
end end

View File

@ -7,6 +7,10 @@ module Errors
def code def code
@code ||= self.class.name.sub('Errors::', '').underscore @code ||= self.class.name.sub('Errors::', '').underscore
end end
def locale_tag
@locale_tag ||= "errors.#{code}"
end
end end
class BadRequest < Base class BadRequest < Base
@ -15,6 +19,9 @@ module Errors
end end
end end
class ParamIsMissing < BadRequest
end
class InvalidBanDuration < BadRequest class InvalidBanDuration < BadRequest
end end
@ -23,4 +30,19 @@ module Errors
403 403
end end
end end
class SelfAction < Forbidden
end
class FollowingSelf < SelfAction
end
class NotFound < Base
def status
404
end
end
class UserNotFound < NotFound
end
end end

View File

@ -15,5 +15,13 @@ module UseCase
def call def call
raise NotImplementedError raise NotImplementedError
end end
private
def not_blank!(*args)
args.each do |arg|
raise Errors::ParamIsMissing if instance_variable_get("@#{arg}").blank?
end
end
end end
end end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require "use_case/base"
require "errors"
module UseCase
module Relationship
class Create < UseCase::Base
option :current_user_id, type: Types::Coercible::Integer
option :target_user, type: Types::Coercible::String
option :type, type: Types::Coercible::String
def call
not_blank! :current_user_id, :target_user, :type
type = @type.downcase
ensure_type(type)
source_user = find_source_user
target_user = find_target_user
source_user.public_send(type, target_user)
{
status: 201,
resource: true,
extra: {
target_user: target_user
}
}
end
private
def ensure_type(type)
raise Errors::BadRequest unless type == 'follow'
end
def find_source_user
user_id = @current_user_id
User.find(user_id)
rescue ActiveRecord::RecordNotFound
raise Errors::UserNotFound
end
def find_target_user
target_user = @target_user
return target_user if target_user.is_a?(User)
User.find_by!(screen_name: target_user)
rescue ActiveRecord::RecordNotFound
raise Errors::UserNotFound
end
end
end
end

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require "use_case/base"
require "errors"
module UseCase
module Relationship
class Destroy < UseCase::Base
option :current_user_id, type: Types::Coercible::Integer
option :target_user, type: Types::Coercible::String
option :type, type: Types::Coercible::String
def call
not_blank! :current_user_id, :target_user, :type
type = @type.downcase
ensure_type(type)
source_user = find_source_user
target_user = find_target_user
source_user.public_send("un#{type}", target_user)
{
status: 204,
resource: nil,
extra: {
target_user: target_user
}
}
end
private
def ensure_type(type)
raise Errors::BadRequest unless type == 'follow'
end
def find_source_user
user_id = @current_user_id
User.find(user_id)
rescue ActiveRecord::RecordNotFound
raise Errors::UserNotFound
end
def find_target_user
target_user = @target_user
return target_user if target_user.is_a?(User)
User.find_by!(screen_name: target_user)
rescue ActiveRecord::RecordNotFound
raise Errors::UserNotFound
end
end
end
end

View File

@ -1,159 +0,0 @@
# frozen_string_literal: true
require "rails_helper"
describe Ajax::FriendController, :ajax_controller, type: :controller do
describe "#create" do
let(:params) do
{
screen_name: screen_name
}
end
subject { post(:create, params: params) }
context "when user is signed in" do
before(:each) { sign_in(user) }
context "when target user exists" do
let(:target_user) { FactoryBot.create(:user) }
let(:screen_name) { target_user.screen_name }
let(:expected_response) do
{
"success" => true,
"status" => "okay",
"message" => anything
}
end
it "creates a follow relationship" do
expect(user.followings.ids).not_to include(target_user.id)
expect { subject }.to(change { user.followings.count }.by(1))
expect(user.followings.ids).to include(target_user.id)
end
include_examples "returns the expected response"
end
context "when target user does not exist" do
let(:screen_name) { "tripmeister_eder" }
let(:expected_response) do
{
"success" => true,
"status" => "okay",
"message" => anything
}
end
it "does not create a follow relationship" do
expect { subject }.not_to(change { user.followings.count })
end
include_examples "returns the expected response"
end
end
context "when user is not signed in" do
let(:screen_name) { "tutenchamun" }
let(:expected_response) do
{
"success" => false,
"status" => "fail",
"message" => anything
}
end
include_examples "returns the expected response"
end
end
describe "#destroy" do
let(:params) do
{
screen_name: screen_name
}
end
subject { delete(:destroy, params: params) }
context "when user is signed in" do
before(:each) { sign_in(user) }
context "when target user exists" do
let(:target_user) { FactoryBot.create(:user) }
let(:screen_name) { target_user.screen_name }
before(:each) { target_user }
context "when user follows target" do
let(:expected_response) do
{
"success" => true,
"status" => "okay",
"message" => anything
}
end
before(:each) { user.follow target_user }
it "destroys a follow relationship" do
expect(user.followings.ids).to include(target_user.id)
expect { subject }.to(change { user.followings.count }.by(-1))
expect(user.followings.ids).not_to include(target_user.id)
end
include_examples "returns the expected response"
end
context "when user does not already follow target" do
let(:expected_response) do
{
"success" => false,
"status" => "fail",
"message" => anything
}
end
it "does not destroy a follow relationship" do
expect { subject }.not_to(change { user.followings.count })
end
include_examples "returns the expected response"
end
end
context "when target user does not exist" do
let(:screen_name) { "tripmeister_eder" }
let(:expected_response) do
{
"success" => false,
"status" => "fail",
"message" => anything
}
end
it "does not destroy a follow relationship" do
expect { subject }.not_to(change { user.followings.count })
end
include_examples "returns the expected response"
end
end
context "when user is not signed in" do
let(:screen_name) { "tutenchamun" }
let(:expected_response) do
{
"success" => false,
"status" => "fail",
"message" => anything
}
end
include_examples "returns the expected response"
end
end
end

View File

@ -0,0 +1,122 @@
# coding: utf-8
# frozen_string_literal: true
require "rails_helper"
describe Ajax::RelationshipController, type: :controller do
shared_examples_for "params is empty" do
let(:params) { { type: type } } # type is still required as it's part of the route
include_examples "ajax does not succeed", "param is missing"
end
let!(:user) { FactoryBot.create(:user) }
let!(:user2) { FactoryBot.create(:user) }
describe "#create" do
shared_examples_for "valid relationship type" do
it_behaves_like "params is empty"
context "target_user does not exist" do
let(:target_user) { "peter-witzig" }
include_examples "ajax does not succeed", "not found"
end
context "target_user is current user" do
let(:target_user) { user.screen_name }
include_examples "ajax does not succeed", "yourself"
end
context "target_user is different from current_user" do
let(:target_user) { user2.screen_name }
it "creates the relationship" do
expect { subject }.to change { Relationship.count }.by(1)
expect(Relationship.last.target.screen_name).to eq(target_user)
end
end
end
let(:type) { "Sauerkraut" }
let(:target_user) { user2.screen_name }
let(:params) { { type: type, target_user: target_user } }
subject { post(:create, params: params) }
it_behaves_like "requires login"
context "user signed in" do
before(:each) { sign_in(user) }
context "type = 'follow'" do
let(:type) { "follow" }
include_examples "valid relationship type"
end
context "type = 'dick'" do
let(:type) { "dick" }
it_behaves_like "params is empty"
include_examples "ajax does not succeed", "Bad Request"
end
end
end
describe "#destroy" do
shared_examples_for "valid relationship type" do
let(:target_user) { user2.screen_name }
context "relationship exists" do
before do
user.public_send(type, user2)
end
it "destroys a relationship" do
expect { subject }.to change { Relationship.count }.by(-1)
end
end
context "relationship does not exist" do
it "does not change anything at all" do
expect { subject }.to change { Relationship.count }.by(0)
end
end
it_behaves_like "params is empty"
context "target_user does not exist" do
let(:target_user) { "peter-witzig" }
include_examples "ajax does not succeed", "not found"
end
end
let(:type) { "Sauerkraut" }
let(:target_user) { user2.screen_name }
let(:params) { { type: type, target_user: target_user } }
subject { delete(:destroy, params: params) }
it_behaves_like "requires login"
context "user signed in" do
before(:each) { sign_in(user) }
context "type = 'follow'" do
let(:type) { "follow" }
include_examples "valid relationship type"
end
context "type = 'dick'" do
let(:type) { "dick" }
it_behaves_like "params is empty"
include_examples "ajax does not succeed", "Bad Request"
end
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
RSpec.shared_examples_for "ajax does not succeed" do |part_of_error_message|
it "ajax does not succeed" do
subject
expect(assigns(:response)[:success]).to be false
expect(assigns(:response)[:message]).to include(part_of_error_message)
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
RSpec.shared_examples_for "requires login" do
it "redirects to the login page for guests" do
subject
expect(response).to redirect_to(new_user_session_path)
end
it "does not redirect to the login page for users" do
sign_in user
subject
expect(response).to_not redirect_to(new_user_session_path)
end
end