diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 89ffd07..bd9b8d5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -59,9 +59,11 @@ def after_sign_out_path_for(_resource_or_scope) new_user_session_path end + # If getting started is active AND the users has not completed the getting_started page def current_user_redirect_path - # If getting started is active AND the users has not completed the getting_started page - if current_user.getting_started? && !current_user.basic_profile_present? + user_profile = UserServices::Profile.new(current_user) + + if user_profile.onboard_user? getting_started_path else stream_path diff --git a/app/controllers/likes_controller.rb b/app/controllers/likes_controller.rb index d316492..fd1db07 100644 --- a/app/controllers/likes_controller.rb +++ b/app/controllers/likes_controller.rb @@ -4,7 +4,7 @@ class LikesController < ApplicationController include ApplicationHelper include PostInteractionRender - before_action :authenticate_user!, except: :index + before_action :authenticate_user! rescue_from Diaspora::Exceptions::NonPublic do authenticate_user! diff --git a/app/jobs/workers/application_job.rb b/app/jobs/workers/application_job.rb index 4afadf3..9b27ad6 100644 --- a/app/jobs/workers/application_job.rb +++ b/app/jobs/workers/application_job.rb @@ -6,7 +6,7 @@ class ApplicationJob < ActiveJob::Base retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available - discard_on ActiveJob::DeserializationError + discard_on ActiveJob::DeserializationError, ActiveRecord::RecordNotUnique sidekiq_options retry: 5 end diff --git a/app/jobs/workers/gather_o_embed_data.rb b/app/jobs/workers/gather_o_embed_data.rb index c70aa09..2bf694f 100644 --- a/app/jobs/workers/gather_o_embed_data.rb +++ b/app/jobs/workers/gather_o_embed_data.rb @@ -13,7 +13,7 @@ def perform(post_id, url, retry_count=1) # we had a chance to run the job. # On the other hand sometimes the job runs before the Post is # fully persisted. So we just reduce the amount of retries. - GatherOEmbedData.perform_in(1.minute, post_id, url, retry_count + 1) unless retry_count > 3 + Workers::GatherOEmbedData.perform_in(1.minute, post_id, url, retry_count + 1) unless retry_count > 3 end end end diff --git a/app/models/user.rb b/app/models/user.rb index 6e3f42b..1ff422c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,11 +1,6 @@ # frozen_string_literal: true class User < ApplicationRecord - include Connecting - include Querying - include SocialActions - include Profile - # attr_accessor :plain_otp_secret # encrypts :otp_secret @@ -170,6 +165,82 @@ def seed_aspects aq end + def share_with(person, aspect) + UserServices::Connecting.new(self).share_with(person, aspect) + end + + def disconnect(contact) + UserServices::Connecting.new(self).disconnect(contact) + end + + def disconnected_by(person) + UserServices::Connecting.new(self).disconnected_by(person) + end + + def comment!(target, text, opts={}) + UserServices::SocialActions.new(self).comment!(target, text, opts) + end + + def participate!(target, opts={}) + UserServices::SocialActions.new(self).participate!(target, opts) + end + + def participate_in_poll!(target, answer, opts={}) + UserServices::SocialActions.new(self).participate_in_poll!(target, answer, opts) + end + + def like!(target, opts={}) + UserServices::SocialActions.new(self).like!(target, opts) + end + + def like_comment!(target, opts={}) + UserServices::SocialActions.new(self).like_comment!(target, opts) + end + + def reshare!(target, opts={}) + UserServices::SocialActions.new(self).reshare!(target, opts) + end + + def find_visible_shareable_by_id(klass, id, opts={}) + UserServices::Querying.new(self).find_visible_shareable_by_id(klass, id, opts) + end + + def visible_shareables(klass, opts={}) + UserServices::Querying.new(self).visible_shareables(klass, opts) + end + + def posts_from(person, with_order: true) + UserServices::Querying.new(self).posts_from(person, with_order: with_order) + end + + def photos_from(_person, opts={}) + UserServices::Querying.new(self).photos_from(person, opts) + end + + def contact_for(person) + UserServices::Querying.new(self).contact_for(person) + end + + def block_for(person) + UserServices::Querying.new(self).block_for(person) + end + + def aspects_with_shareable(base_class_name_or_class, shareable_id) + UserServices::Querying.new(self).aspects_with_shareable(base_class_name_or_class, shareable_id) + end + + def contact_for_person_id(person_id) + UserServices::Querying.new(self).contact_for_person_id(person_id) + end + + def people_in_aspects(requested_aspects, opts={}) + UserServices::Querying.new(self).people_in_aspects(requested_aspects, opts) + end + + def aspects_with_person(person) + UserServices::Querying.new(self).aspects_with_person(person) + end + def send_welcome_message return unless AppConfig.settings.welcome_message.enabled? && AppConfig.admins.account? @@ -242,7 +313,7 @@ def has_hidden_shareables_of_type?(t=Post) alias send_reset_password_instructions! send_reset_password_instructions def send_reset_password_instructions - ResetPasswordJob.perform_later(self) + Workers::ResetPasswordJob.perform_later(self) end def strip_and_downcase_username diff --git a/app/models/user/profile.rb b/app/models/user/profile.rb deleted file mode 100644 index 4544829..0000000 --- a/app/models/user/profile.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -class User - module Profile - def update_profile(params) - if photo = params.delete(:photo) - photo.update(pending: false) if photo.pending - params[:image_url] = photo.url(:thumb_large) - params[:image_url_medium] = photo.url(:thumb_medium) - params[:image_url_small] = photo.url(:thumb_small) - end - - params.stringify_keys! - params.slice!(*(Profile.column_names + %w[tag_string date])) - if profile.update(params) - deliver_profile_update - true - else - false - end - end - - def update_profile_with_omniauth(user_info) - update_profile(profile.from_omniauth_hash(user_info)) - end - - def deliver_profile_update(opts={}) - Diaspora::Federation::Dispatcher.defer_dispatch(self, profile, opts) - end - - def basic_profile_present? - tag_followings.any? || profile[:image_url] - end - end -end diff --git a/app/models/user/connecting.rb b/app/models/user_services/connecting.rb similarity index 61% rename from app/models/user/connecting.rb rename to app/models/user_services/connecting.rb index 6da155a..9b109b2 100644 --- a/app/models/user/connecting.rb +++ b/app/models/user_services/connecting.rb @@ -1,15 +1,19 @@ # frozen_string_literal: true -class User - module Connecting +module UserServices + class Connecting + def initialize(user) + @user = user + end + # This will create a contact on the side of the sharer and the sharee. # @param [Person] person The person to start sharing with. # @param [Aspect] aspect The aspect to add them to. # @return [Contact] The newly made contact for the passed in person. def share_with(person, aspect) - return if blocks.exists?(person_id: person.id) + return if user.blocks.exists?(person_id: person.id) - contact = contacts.find_or_initialize_by(person_id: person.id) + contact = user.contacts.find_or_initialize_by(person_id: person.id) return nil unless contact.valid? needs_dispatch = !contact.receiving? @@ -17,26 +21,23 @@ def share_with(person, aspect) contact.aspects << aspect contact.save - if needs_dispatch - Diaspora::Federation::Dispatcher.defer_dispatch(self, contact) - deliver_profile_update(subscriber_ids: [person.id]) unless person.local? - end + dispatch_contact(contact, person) if needs_dispatch - Notifications::StartedSharing.where(recipient_id: id, target: person.id, unread: true) + # rubocop: disable Rails::SkipsModelValidations + Notifications::StartedSharing.where(recipient_id: user.id, target: person.id, unread: true) .update_all(unread: false) + # rubocop: enable Rails::SkipsModelValidations contact end def disconnect(contact) - logger.info "event=disconnect user=#{diaspora_handle} target=#{contact.person.diaspora_handle}" - if contact.person.local? raise "FATAL: user entry is missing from the DB. Aborting" if contact.person.owner.nil? contact.person.owner.disconnected_by(contact.user.person) else - Diaspora::Federated::ContactRetraction.for(contact).defer_dispatch(self) + Diaspora::Federated::ContactRetraction.for(contact).defer_dispatch(user) end contact.aspect_memberships.delete_all @@ -45,14 +46,24 @@ def disconnect(contact) end def disconnected_by(person) - logger.info "event=disconnected_by user=#{diaspora_handle} target=#{person.diaspora_handle}" - contact_for(person).try {|contact| + user.contact_for(person).try {|contact| disconnect_contact(contact, direction: :sharing, destroy: !contact.receiving) } end private + attr_reader :user + + def dispatch_contact(contact, person) + Diaspora::Federation::Dispatcher.defer_dispatch(user, contact) + deliver_profile_update(subscriber_ids: [person.id]) unless person.local? + end + + def deliver_profile_update(opts) + Profile.new(user).deliver_profile_update(opts) + end + def disconnect_contact(contact, direction:, destroy:) if destroy contact.destroy diff --git a/app/models/user_services/profile.rb b/app/models/user_services/profile.rb new file mode 100644 index 0000000..4066e1e --- /dev/null +++ b/app/models/user_services/profile.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module UserServices + class Profile + # @param [User] user up profiles for the user + def initialize(user) + @user = user + end + + def onboard_user? + user.getting_started? && !basic_profile_present? + end + + def update_profile_with_omniauth(user_info) + update_profile(user.profile.from_omniauth_hash(user_info)) + end + + def update_profile(params) + if photo = params.delete(:photo) + photo.update(pending: false) if photo.pending + params[:image_url] = photo.url(:thumb_large) + params[:image_url_medium] = photo.url(:thumb_medium) + params[:image_url_small] = photo.url(:thumb_small) + end + + params.stringify_keys! + params.slice!(*(Profile.column_names + %w[tag_string date])) + if user.profile.update(params) + deliver_profile_update + true + else + false + end + end + + def deliver_profile_update(opts={}) + Diaspora::Federation::Dispatcher.defer_dispatch(user, user.profile, opts) + end + + # A user should follow at least one person or should have a profile image + def basic_profile_present? + user.tag_followings.any? || user.profile[:image_url] + end + + private + + attr_reader :user + end +end diff --git a/app/models/user/querying.rb b/app/models/user_services/querying.rb similarity index 72% rename from app/models/user/querying.rb rename to app/models/user_services/querying.rb index 339b86c..b0fbf5e 100644 --- a/app/models/user/querying.rb +++ b/app/models/user_services/querying.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true -class User - module Querying +module UserServices + class Querying + # @param [User] user + def initialize(user) + @user = user + end + def find_visible_shareable_by_id(klass, id, opts={}) key = (opts.delete(:key) || :id) - find_visible_shareable_by_id = EvilQuery::VisibleShareableById.new(self, klass, key, id, opts) + find_visible_shareable_by_id = EvilQuery::VisibleShareableById.new(user, klass, key, id, opts) find_visible_shareable_by_id.post! end @@ -15,52 +20,48 @@ def visible_shareables(klass, opts={}) .limit(opts[:limit]).order(opts[:order_with_table]) end - # @param [TrueClass] with_order - def posts_from(person, with_order: true) - base_query = Post.from_person_visible_by_user(self, person) - return base_query.order("posts.created_at desc") if with_order - - base_query + def visible_shareable_ids(klass, opts={}) + visible_ids_from_sql(klass, prep_opts(klass, opts)) end def photos_from(person, opts={}) opts = prep_opts(Photo, opts) - Photo.from_person_visible_by_user(self, person) + Photo.from_person_visible_by_user(user, person) .limit(opts[:limit]) end def contact_for(person) return nil unless person - contact_for_person_id(person.id) + user.contact_for_person_id(person.id) end def block_for(person) return nil unless person - blocks.find_by(person_id: person.id) + user.blocks.find_by(person_id: person.id) end def aspects_with_shareable(base_class_name_or_class, shareable_id) base_class_name = base_class_name_or_class base_class_name = base_class_name_or_class.base_class.to_s if base_class_name_or_class.is_a?(Class) - aspects.joins(:aspect_visibilities).where(aspect_visibilities: {shareable_id: shareable_id, - shareable_type: base_class_name}) + user.aspects.joins(:aspect_visibilities).where(aspect_visibilities: {shareable_id: shareable_id, + shareable_type: base_class_name}) end def contact_for_person_id(person_id) Contact.includes(person: :profile) - .find_by(user_id: id, person_id: person_id) + .find_by(user_id: user.id, person_id: person_id) end # @param [Person] person # @return [Boolean] whether person is a contact of this user def has_contact_for?(person) - Contact.exists?(user_id: id, person_id: person.id) + Contact.exists?(user_id: user.id, person_id: person.id) end def people_in_aspects(requested_aspects, opts={}) - allowed_aspects = aspects & requested_aspects + allowed_aspects = user.aspects & requested_aspects aspect_ids = allowed_aspects.map(&:id) people = Person.in_aspects(aspect_ids) @@ -77,20 +78,15 @@ def aspects_with_person(person) contact_for(person).aspects end - def posts_from(person, with_order=true) - base_query = Post.from_person_visible_by_user(self, person) + # @param [Person] person + # @param [TrueClass] with_order + def posts_from(person, with_order: true) + base_query = Post.from_person_visible_by_user(user, person) return base_query.order("posts.created_at desc") if with_order base_query end - def photos_from(person, opts={}) - opts = prep_opts(Photo, opts) - Photo.from_person_visible_by_user(self, person) - .by_max_time(opts[:max_time]) - .limit(opts[:limit]) - end - protected # @return [Hash] @@ -111,5 +107,9 @@ def prep_opts(klass, opts) opts[:max_time] ||= Time.zone.now + 1 opts end + + private + + attr_reader :user end end diff --git a/app/models/user/social_actions.rb b/app/models/user_services/social_actions.rb similarity index 60% rename from app/models/user/social_actions.rb rename to app/models/user_services/social_actions.rb index cdf5dcc..5e7e010 100644 --- a/app/models/user/social_actions.rb +++ b/app/models/user_services/social_actions.rb @@ -1,51 +1,55 @@ # frozen_string_literal: true -class User - module SocialActions +module UserServices + class SocialActions + def initialize(user) + @user = user + end + def comment!(target, text, opts={}) - Comment::Generator.new(self, target, text).create!(opts).tap do + Comment::Generator.new(user, target, text).create!(opts).tap do update_or_create_participation!(target) end end def participate!(target, opts={}) - Participation::Generator.new(self, target).create!(opts) + Participation::Generator.new(user, target).create!(opts) end - def like!(target, opts={}) - Like::Generator.new(self, target).create!(opts).tap do + def participate_in_poll!(target, answer, opts={}) + PollParticipation::Generator.new(user, target, answer).create!(opts).tap do update_or_create_participation!(target) end end - def like_comment!(target, opts={}) - Like::Generator.new(self, target).create!(opts) - end - - def participate_in_poll!(target, answer, opts={}) - PollParticipation::Generator.new(self, target, answer).create!(opts).tap do + def like!(target, opts={}) + Like::Generator.new(user, target).create!(opts).tap do update_or_create_participation!(target) end end + def like_comment!(target, opts={}) + Like::Generator.new(user, target).create!(opts) + end + def reshare!(target, opts={}) - raise I18n.t("reshares.create.error") if target.author.guid == guid + raise I18n.t("reshares.create.error") if target.author.guid == user.guid - build_post(:reshare, root_guid: target.guid).tap do |reshare| + user.build_post(:reshare, root_guid: target.guid).tap do |reshare| reshare.text = opts[:text] reshare.save! update_or_create_participation!(target) - Diaspora::Federation::Dispatcher.defer_dispatch(self, reshare) + Diaspora::Federation::Dispatcher.defer_dispatch(user, reshare) end end def build_conversation(opts={}) Conversation.new do |c| - c.author = person + c.author = user.person c.subject = opts[:subject] c.participant_ids = [*opts[:participant_ids]] | [person_id] c.messages_attributes = [ - {author: person, text: opts[:message][:text]} + {author: user.person, text: opts[:message][:text]} ] end end @@ -53,19 +57,23 @@ def build_conversation(opts={}) def build_message(conversation, opts={}) conversation.messages.build( text: opts[:text], - author: person + author: user.person ) end def update_or_create_participation!(target) - return if target.author == person + return if target.author == user.person - participation = participations.find_by(target_id: target) + participation = user.participations.find_by(target_id: target) if participation.present? participation.update!(count: participation.count.next) else participate!(target) end end + + private + + attr_reader :user end end diff --git a/config/initializers/diaspora_federation.rb b/config/initializers/diaspora_federation.rb index 325c0b0..d66229b 100644 --- a/config/initializers/diaspora_federation.rb +++ b/config/initializers/diaspora_federation.rb @@ -93,14 +93,14 @@ end on :queue_public_receive do |xml, legacy=false| - ReceivePublicJob.perform_later(xml, legacy) + Workers::ReceivePublicJob.perform_later(xml, legacy) end on :queue_private_receive do |guid, xml, legacy=false| person = Person.find_by(guid: guid) (person.present? && person.owner_id.present?).tap do |user_found| - ReceivePrivateJob.perform_later(person.owner.id, xml, legacy) if user_found + Workers::ReceivePrivateJob.perform_later(person.owner.id, xml, legacy) if user_found end end diff --git a/config/locales/de.yml b/config/locales/de.yml index 82451e9..54b0977 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -12,4 +12,7 @@ de: post-status: you_like_it: "Dir gefällt das" you_and_others_like_it: "Dir und %{count} anderen gefällt das" - likes: "%{count} gefällt das" \ No newline at end of file + likes: "%{count} gefällt das" + likes: + create: + error: "Ein Like konnte nicht erstellt werden" \ No newline at end of file diff --git a/lib/diaspora/federated/retraction.rb b/lib/diaspora/federated/retraction.rb index 959646d..c9bf004 100644 --- a/lib/diaspora/federated/retraction.rb +++ b/lib/diaspora/federated/retraction.rb @@ -34,11 +34,11 @@ def self.for(target) def defer_dispatch(user, include_target_author=true) subscribers = dispatch_subscribers(include_target_author) - DeferredRetractionJob.perform_later(user.id, - self.class.to_s, - data, - subscribers.map(&:id), - service_opts(user)) + Workers::DeferredRetractionJob.perform_later(user.id, + self.class.to_s, + data, + subscribers.map(&:id), + service_opts(user)) end def perform diff --git a/lib/diaspora/federation/dispatcher.rb b/lib/diaspora/federation/dispatcher.rb index 4aee82a..a5a7037 100644 --- a/lib/diaspora/federation/dispatcher.rb +++ b/lib/diaspora/federation/dispatcher.rb @@ -21,7 +21,7 @@ def self.build(sender, object, opts={}) end def self.defer_dispatch(sender, object, opts={}) - DeferredDispatchJob.perform_later(sender.id, object.class.to_s, object.id, opts) + Workers::DeferredDispatchJob.perform_later(sender.id, object.class.to_s, object.id, opts) end def dispatch @@ -69,9 +69,9 @@ def deliver_to_remote(_people) def deliver_to_user_services case object when StatusMessage - each_service {|service| PostToServiceJob.perform_later(service.id, object.id, opts[:url]) } + each_service {|service| Workers::PostToServiceJob.perform_later(service.id, object.id, opts[:url]) } when Retraction - each_service {|service| DeletePostFromServiceJob.perform_later(service.id, opts) } + each_service {|service| Workers::DeletePostFromServiceJob.perform_later(service.id, opts) } end end diff --git a/lib/diaspora/federation/dispatcher/private.rb b/lib/diaspora/federation/dispatcher/private.rb index 7a7c0b1..404d99d 100644 --- a/lib/diaspora/federation/dispatcher/private.rb +++ b/lib/diaspora/federation/dispatcher/private.rb @@ -9,7 +9,7 @@ class Private < Dispatcher def deliver_to_remote(people) return if people.empty? - SendPrivateJob.perform_later(sender.id, entity.to_s, targets(people)) + Workers::SendPrivateJob.perform_later(sender.id, entity.to_s, targets(people)) end def targets(people) diff --git a/lib/diaspora/federation/dispatcher/public.rb b/lib/diaspora/federation/dispatcher/public.rb index fb1420b..11cfc23 100644 --- a/lib/diaspora/federation/dispatcher/public.rb +++ b/lib/diaspora/federation/dispatcher/public.rb @@ -16,7 +16,7 @@ def deliver_to_remote(people) return if targets.empty? - SendPublicJob.perform_later(sender.id, entity.to_s, targets, magic_envelope.to_xml) + ::Workers::SendPublicJob.perform_later(sender.id, entity.to_s, targets, magic_envelope.to_xml) end def target_urls(people) diff --git a/lib/diaspora/fetcher/public_posts_from_pod.rb b/lib/diaspora/fetcher/public_posts_from_pod.rb index 81c851c..21b88b5 100644 --- a/lib/diaspora/fetcher/public_posts_from_pod.rb +++ b/lib/diaspora/fetcher/public_posts_from_pod.rb @@ -12,8 +12,7 @@ def self.queue_for(pod) return if pod&.blocked return if pod&.status != Pod.no_errors - # TODO: Create a job for this - FetchPublicPostsJob.perform_later(pod) + Workers::FetchPublicPostsJob.perform_later(pod) end def fetch!(pod) diff --git a/lib/diaspora/shareable.rb b/lib/diaspora/shareable.rb index 7437c01..cdf3762 100644 --- a/lib/diaspora/shareable.rb +++ b/lib/diaspora/shareable.rb @@ -22,13 +22,13 @@ def self.included(model) # scopes scope :with_visibility, lambda { - joins("LEFT OUTER JOIN share_visibilities ON share_visibilities.shareable_id = #{table_name}.id AND "\ + joins("LEFT OUTER JOIN share_visibilities ON share_visibilities.shareable_id = #{table_name}.id AND " \ "share_visibilities.shareable_type = '#{base_class}'") } scope :with_aspects, lambda { - joins("LEFT OUTER JOIN aspect_visibilities ON aspect_visibilities.shareable_id = #{table_name}.id AND "\ - " aspect_visibilities.shareable_type = '#{base_class}'") + joins("LEFT OUTER JOIN aspect_visibilities ON aspect_visibilities.shareable_id = #{table_name}.id AND " \ + "aspect_visibilities.shareable_type = '#{base_class}'") } end model.extend Diaspora::Shareable::QueryMethods @@ -68,6 +68,8 @@ def owned_or_visible_by_user(user) ).select("DISTINCT #{table_name}.*") end + # @param [User] user + # @param [Person] person def from_person_visible_by_user(user, person) return owned_by_user(user) if person == user.person diff --git a/spec/controllers/likes_controller_spec.rb b/spec/controllers/likes_controller_spec.rb new file mode 100644 index 0000000..6f2d9cb --- /dev/null +++ b/spec/controllers/likes_controller_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +describe LikesController do + before do + @alices_aspect = alice.aspects.where(name: "generic").first + @bobs_aspect = bob.aspects.where(name: "generic").first + + sign_in(alice, scope: :user) + end + + describe "#create" do + let(:like_hash) { + {post_id: @target.id} + } + + context "on my own post" do + it "succeeds" do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + post :create, params: like_hash, format: :json + expect(response).to have_http_status(:created) + end + end + + context "on a post from a contact" do + before do + @target = bob.post(:status_message, text: "AWESOME", to: @bobs_aspect.id) + end + + it "likes" do + post :create, params: like_hash + expect(response).to have_http_status(:created) + end + + it "doesn't post multiple times" do + alice.like!(@target) + post :create, params: like_hash + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context "on a post from a stranger" do + before do + @target = eve.post :status_message, text: "AWESOME", to: eve.aspects.first.id + end + + it "doesn't post" do + expect(alice).not_to receive(:like!) + post :create, params: like_hash + expect(response).to have_http_status(:unprocessable_entity) + end + end + + context "when an the exception is raised" do + before do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + end + + it "is caught when it means that the target is not found" do + post :create, params: {post_id: -1}, format: :json + expect(response).to have_http_status(:unprocessable_entity) + end + + it "is not caught when it is unexpected" do + @target = alice.post :status_message, text: "AWESOME", to: @alices_aspect.id + allow(alice).to receive(:like!).and_raise("something") + allow(@controller).to receive(:current_user).and_return(alice) + expect { post :create, params: like_hash, format: :json }.to raise_error("something") + end + end + end + + describe "#index" do + before do + @message = alice.post(:status_message, text: "hey", to: @alices_aspect.id) + end + + it "returns a 404 for a post not visible to the user" do + sign_in eve + expect { + get :index, params: {post_id: @message.id} + }.to raise_error(ActiveRecord::RecordNotFound) + end + + it "returns an array of likes for a post" do + bob.like!(@message) + get :index, params: {post_id: @message.id} + expect(JSON.parse(response.body).map {|h| h["id"] }).to match_array(@message.likes.map(&:id)) + end + + it "returns an empty array for a post with no likes" do + get :index, params: {post_id: @message.id} + expect(JSON.parse(response.body)).to eq([]) + end + + it "returns likes for a public post without login" do + post = alice.post(:status_message, text: "hey", public: true) + bob.like!(post) + sign_out :user + get :index, params: {post_id: post.id}, format: :json + expect(JSON.parse(response.body).map {|h| h["id"] }).to match_array(post.likes.map(&:id)) + end + + it "returns a unauthorized status for a private post when logged out" do + bob.like!(@message) + sign_out :user + get :index, params: {post_id: @message.id}, format: :json + expect(response).to have_http_status(:unauthorized) + end + end + + describe "#destroy" do + before do + @message = bob.post(:status_message, text: "hey", to: @alices_aspect.id) + @like = alice.like!(@message) + end + + it "lets a user destroy their like" do + current_user = controller.send(:current_user) + expect(current_user).to receive(:retract).with(@like) + + delete :destroy, params: {post_id: @message.id, id: @like.id}, format: :json + expect(response).to have_http_status(:no_content) + end + + it "does not let a user destroy other likes" do + like2 = eve.like!(@message) + like_count = Like.count + + delete :destroy, params: {post_id: @message.id, id: like2.id}, format: :json + expect(response).to have_http_status(:not_found) + expect(response.body).to eq(I18n.t("likes.destroy.error")) + expect(Like.count).to eq(like_count) + end + end +end diff --git a/spec/models/user/querying_spec.rb b/spec/models/user/querying_spec.rb deleted file mode 100644 index f5dc1e4..0000000 --- a/spec/models/user/querying_spec.rb +++ /dev/null @@ -1,332 +0,0 @@ -# frozen_string_literal: true - -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - -require 'rails_helper' - -describe User::Querying, type: :model do - before do - @alices_aspect = alice.aspects.where(:name => "generic").first - @eves_aspect = eve.aspects.where(:name => "generic").first - @bobs_aspect = bob.aspects.where(:name => "generic").first - end - - describe "#visible_shareable_ids" do - it "contains your public posts" do - public_post = alice.post(:status_message, :text => "hi", :to => @alices_aspect.id, :public => true) - expect(alice.visible_shareable_ids(Post)).to include(public_post.id) - end - - it "contains your non-public posts" do - private_post = alice.post(:status_message, :text => "hi", :to => @alices_aspect.id, :public => false) - expect(alice.visible_shareable_ids(Post)).to include(private_post.id) - end - - it "contains public posts from people you're following" do - # Alice follows Eve, but Eve does not follow Alice - alice.share_with(eve.person, @alices_aspect) - - # Eve posts a public status message - eves_public_post = eve.post(:status_message, :text => "hello", :to => 'all', :public => true) - - # Alice should see it - expect(alice.visible_shareable_ids(Post)).to include(eves_public_post.id) - end - - it "does not contain non-public posts from people who are following you" do - eve.share_with(alice.person, @eves_aspect) - eves_post = eve.post(:status_message, :text => "hello", :to => @eves_aspect.id) - expect(alice.visible_shareable_ids(Post)).not_to include(eves_post.id) - end - - it "does not contain non-public posts from aspects you're not in" do - dogs = bob.aspects.create(:name => "dogs") - invisible_post = bob.post(:status_message, :text => "foobar", :to => dogs.id) - expect(alice.visible_shareable_ids(Post)).not_to include(invisible_post.id) - end - - it "respects the :type option" do - post = bob.post(:status_message, text: "hey", public: true, to: @bobs_aspect.id) - reshare = bob.post(:reshare, root_guid: post.guid, to: @bobs_aspect) - expect(alice.visible_shareable_ids(Post, type: "Reshare")).to include(reshare.id) - expect(alice.visible_shareable_ids(Post, type: "StatusMessage")).not_to include(reshare.id) - end - - it "does not contain duplicate posts" do - bobs_other_aspect = bob.aspects.create(:name => "cat people") - bob.add_contact_to_aspect(bob.contact_for(alice.person), bobs_other_aspect) - expect(bob.aspects_with_person(alice.person)).to match_array [@bobs_aspect, bobs_other_aspect] - - bobs_post = bob.post(:status_message, :text => "hai to all my people", :to => [@bobs_aspect.id, bobs_other_aspect.id]) - - expect(alice.visible_shareable_ids(Post).length).to eq(1) - expect(alice.visible_shareable_ids(Post)).to include(bobs_post.id) - end - - describe 'hidden posts' do - before do - aspect_to_post = bob.aspects.where(:name => "generic").first - @status = bob.post(:status_message, :text => "hello", :to => aspect_to_post) - end - - it "pulls back non hidden posts" do - expect(alice.visible_shareable_ids(Post).include?(@status.id)).to be true - end - - it "does not pull back hidden posts" do - @status.share_visibilities.where(user_id: alice.id).first.update_attributes(hidden: true) - expect(alice.visible_shareable_ids(Post).include?(@status.id)).to be false - end - end - end - - describe "#prep_opts" do - it "defaults the opts" do - time = Time.now - allow(Time).to receive(:now).and_return(time) - expect(alice.send(:prep_opts, Post, {})).to eq({ - :type => Stream::Base::TYPES_OF_POST_IN_STREAM, - :order => 'created_at DESC', - :limit => 15, - :hidden => false, - :order_field => :created_at, - :order_with_table => "posts.created_at DESC", - :max_time => time + 1 - }) - end - end - - describe "#visible_shareables" do - it 'never contains posts from people not in your aspects' do - FactoryBot.create(:status_message, public: true) - expect(bob.visible_shareables(Post).count(:all)).to eq(0) - end - - context 'with many posts' do - before do - time_interval = 1000 - time_past = 1000000 - (1..25).each do |n| - [alice, bob, eve].each do |u| - aspect_to_post = u.aspects.where(:name => "generic").first - post = u.post :status_message, :text => "#{u.username} - #{n}", :to => aspect_to_post.id - post.created_at = (post.created_at - time_past) - time_interval - post.updated_at = (post.updated_at - time_past) + time_interval - post.save - time_interval += 1000 - end - end - end - - it 'works' do - # The set up takes a looong time, so to save time we do several tests in one - expect(bob.visible_shareables(Post).length).to eq(15) #it returns 15 by default - expect(bob.visible_shareables(Post).map(&:id)).to eq(bob.visible_shareables(Post, :by_members_of => bob.aspects.map { |a| a.id }).map(&:id)) # it is the same when joining through aspects - - # checks the default sort order - expect(bob.visible_shareables(Post).sort_by { |p| p.created_at }.map { |p| p.id }).to eq(bob.visible_shareables(Post).map { |p| p.id }.reverse) #it is sorted updated_at desc by default - - # It should respect the order option - opts = { :order => 'created_at DESC' } - expect(bob.visible_shareables(Post, opts).first.created_at).to be > bob.visible_shareables(Post, opts).last.created_at - - # It should respect the order option - opts = { :order => 'updated_at DESC' } - expect(bob.visible_shareables(Post, opts).first.updated_at).to be > bob.visible_shareables(Post, opts).last.updated_at - - # It should respect the limit option - opts = { :limit => 40 } - expect(bob.visible_shareables(Post, opts).length).to eq(40) - expect(bob.visible_shareables(Post, opts).map(&:id)).to eq(bob.visible_shareables(Post, opts.merge(:by_members_of => bob.aspects.map { |a| a.id })).map(&:id)) - expect(bob.visible_shareables(Post, opts).sort_by { |p| p.created_at }.map { |p| p.id }).to eq(bob.visible_shareables(Post, opts).map { |p| p.id }.reverse) - - # It should paginate using a datetime timestamp - last_time_of_last_page = bob.visible_shareables(Post).last.created_at - opts = { :max_time => last_time_of_last_page } - expect(bob.visible_shareables(Post, opts).length).to eq(15) - expect(bob.visible_shareables(Post, opts).map { |p| p.id }).to eq(bob.visible_shareables(Post, opts.merge(:by_members_of => bob.aspects.map { |a| a.id })).map { |p| p.id }) - expect(bob.visible_shareables(Post, opts).sort_by { |p| p.created_at }.map { |p| p.id }).to eq(bob.visible_shareables(Post, opts).map { |p| p.id }.reverse) - expect(bob.visible_shareables(Post, opts).map { |p| p.id }).to eq(bob.visible_shareables(Post, :limit => 40)[15...30].map { |p| p.id }) #pagination should return the right posts - - # It should paginate using an integer timestamp - opts = { :max_time => last_time_of_last_page.to_i } - expect(bob.visible_shareables(Post, opts).length).to eq(15) - expect(bob.visible_shareables(Post, opts).map { |p| p.id }).to eq(bob.visible_shareables(Post, opts.merge(:by_members_of => bob.aspects.map { |a| a.id })).map { |p| p.id }) - expect(bob.visible_shareables(Post, opts).sort_by { |p| p.created_at }.map { |p| p.id }).to eq(bob.visible_shareables(Post, opts).map { |p| p.id }.reverse) - expect(bob.visible_shareables(Post, opts).map { |p| p.id }).to eq(bob.visible_shareables(Post, :limit => 40)[15...30].map { |p| p.id }) #pagination should return the right posts - end - end - end - - describe '#find_visible_shareable_by_id' do - it "returns a post if you can see it" do - bobs_post = bob.post(:status_message, text: "hi", to: @bobs_aspect.id, public: false) - expect(alice.find_visible_shareable_by_id(Post, bobs_post.id)).to eq(bobs_post) - end - it "returns nil if you can't see that post" do - dogs = bob.aspects.create(:name => "dogs") - invisible_post = bob.post(:status_message, text: "foobar", to: dogs.id) - expect(alice.find_visible_shareable_by_id(Post, invisible_post.id)).to be_nil - end - end - - context 'with two users' do - describe '#people_in_aspects' do - it 'returns people objects for a users contact in each aspect' do - expect(alice.people_in_aspects([@alices_aspect])).to eq([bob.person]) - end - - it "returns local/remote people objects for a users contact in each aspect" do - local_user1 = FactoryBot.create(:user) - local_user2 = FactoryBot.create(:user) - remote_person = FactoryBot.create(:person) - - asp1 = local_user1.aspects.create(name: "lol") - asp2 = local_user2.aspects.create(name: "brb") - - connect_users(alice, @alices_aspect, local_user1, asp1) - connect_users(alice, @alices_aspect, local_user2, asp2) - alice.contacts.create!(person: remote_person, aspects: [@alices_aspect], sharing: true) - - expect(alice.people_in_aspects([@alices_aspect]).count).to eq(4) - expect(alice.people_in_aspects([@alices_aspect], type: "remote").count).to eq(1) - expect(alice.people_in_aspects([@alices_aspect], type: "local").count).to eq(3) - end - - it 'does not return people not connected to user on same pod' do - 3.times { FactoryBot.create(:user) } - expect(alice.people_in_aspects([@alices_aspect]).count).to eq(1) - end - - it "only returns non-pending contacts" do - expect(alice.people_in_aspects([@alices_aspect])).to eq([bob.person]) - end - - it "returns an empty array when passed an aspect the user doesn't own" do - expect(alice.people_in_aspects([@eves_aspect])).to eq([]) - end - end - end - - context 'contact querying' do - let(:person_one) { FactoryBot.create :person } - let(:person_two) { FactoryBot.create :person } - let(:person_three) { FactoryBot.create :person } - let(:aspect) { alice.aspects.create(:name => 'heroes') } - - describe '#contact_for_person_id' do - it 'returns a contact' do - contact = Contact.create(:user => alice, :person => person_one, :aspects => [aspect]) - alice.contacts << contact - expect(alice.contact_for_person_id(person_one.id)).to be_truthy - end - - it 'returns the correct contact' do - contact = Contact.create(:user => alice, :person => person_one, :aspects => [aspect]) - alice.contacts << contact - - contact2 = Contact.create(:user => alice, :person => person_two, :aspects => [aspect]) - alice.contacts << contact2 - - contact3 = Contact.create(:user => alice, :person => person_three, :aspects => [aspect]) - alice.contacts << contact3 - - expect(alice.contact_for_person_id(person_two.id).person).to eq(person_two) - end - - it 'returns nil for a non-contact' do - expect(alice.contact_for_person_id(person_one.id)).to be_nil - end - - it 'returns nil when someone else has contact with the target' do - contact = Contact.create(:user => alice, :person => person_one, :aspects => [aspect]) - alice.contacts << contact - expect(eve.contact_for_person_id(person_one.id)).to be_nil - end - end - - describe '#contact_for' do - it 'takes a person_id and returns a contact' do - expect(alice).to receive(:contact_for_person_id).with(person_one.id) - alice.contact_for(person_one) - end - - it 'returns nil if the input is nil' do - expect(alice.contact_for(nil)).to be_nil - end - end - - describe '#aspects_with_person' do - before do - @connected_person = bob.person - end - - it 'should return the aspects with given contact' do - expect(alice.aspects_with_person(@connected_person)).to eq([@alices_aspect]) - end - - it 'returns multiple aspects if the person is there' do - aspect2 = alice.aspects.create(:name => 'second') - contact = alice.contact_for(@connected_person) - - alice.add_contact_to_aspect(contact, aspect2) - expect(alice.aspects_with_person(@connected_person).to_set).to eq(alice.aspects.to_set) - end - end - end - - describe "#block_for" do - let(:person) { FactoryBot.create :person } - - before do - eve.blocks.create({ person: person }) - end - - it 'returns the block' do - block = eve.block_for(person) - expect(block).to be_present - expect(block.person.id).to be person.id - end - end - - describe '#posts_from' do - before do - @user3 = FactoryBot.create(:user) - @aspect3 = @user3.aspects.create(:name => "bros") - - @public_message = @user3.post(:status_message, :text => "hey there", :to => 'all', :public => true) - @private_message = @user3.post(:status_message, :text => "hey there", :to => @aspect3.id) - end - - it 'displays public posts for a non-contact' do - expect(alice.posts_from(@user3.person)).to include @public_message - end - - it 'does not display private posts for a non-contact' do - expect(alice.posts_from(@user3.person)).not_to include @private_message - end - - it 'displays private and public posts for a non-contact after connecting' do - connect_users(alice, @alices_aspect, @user3, @aspect3) - new_message = @user3.post(:status_message, :text => "hey there", :to => @aspect3.id) - - alice.reload - - expect(alice.posts_from(@user3.person)).to include @public_message - expect(alice.posts_from(@user3.person)).to include new_message - end - - it 'displays recent posts first' do - msg3 = @user3.post(:status_message, :text => "hey there", :to => 'all', :public => true) - msg4 = @user3.post(:status_message, :text => "hey there", :to => 'all', :public => true) - msg3.created_at = Time.now + 10 - msg3.save! - msg4.created_at = Time.now + 14 - msg4.save! - - expect(alice.posts_from(@user3.person).map { |p| p.id }).to eq([msg4, msg3, @public_message].map { |p| p.id }) - end - end -end diff --git a/spec/models/user/connecting_spec.rb b/spec/models/user_services/connecting_spec.rb similarity index 94% rename from spec/models/user/connecting_spec.rb rename to spec/models/user_services/connecting_spec.rb index 8575eca..1c5f608 100644 --- a/spec/models/user/connecting_spec.rb +++ b/spec/models/user_services/connecting_spec.rb @@ -4,11 +4,11 @@ # licensed under the Affero General Public License version 3 or later. See # the COPYRIGHT file. -describe User::Connecting, type: :model do - let(:aspect1) { alice.aspects.first } - let(:aspect2) { alice.aspects.create(name: "other") } +describe UserServices::Connecting do + let(:aspect_first) { alice.aspects.first } + let(:aspect_other) { alice.aspects.create(name: "other") } - let(:person) { FactoryBot.create(:person) } + let(:person) { create(:person) } describe "disconnecting" do describe "#disconnected_by" do @@ -98,7 +98,7 @@ it "removes the contact from all aspects they are in" do contact = alice.contact_for(bob.person) - alice.add_contact_to_aspect(contact, aspect2) + alice.add_contact_to_aspect(contact, aspect_other) expect { alice.disconnect(contact) @@ -175,7 +175,7 @@ allow(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch).with(alice, instance_of(Profile)) expect(Diaspora::Federation::Dispatcher).not_to receive(:defer_dispatch).with(alice, instance_of(Contact)) - alice.share_with(eve.person, aspect2) + alice.share_with(eve.person, aspect_other) end it "delivers profile for remote persons" do @@ -200,10 +200,10 @@ end it "marks the corresponding notification as 'read'" do - FactoryBot.create(:notification, target: eve.person, recipient: alice, type: "Notifications::StartedSharing") + create(:notification, target: eve.person, recipient: alice, type: "Notifications::StartedSharing") expect(Notifications::StartedSharing.find_by(recipient_id: alice.id, target: eve.person).unread).to be_truthy - alice.share_with(eve.person, aspect1) + alice.share_with(eve.person, aspect_first) expect(Notifications::StartedSharing.find_by(recipient_id: alice.id, target: eve.person).unread).to be_falsey end end diff --git a/spec/models/user/posting_spec.rb b/spec/models/user_services/posting_spec.rb similarity index 100% rename from spec/models/user/posting_spec.rb rename to spec/models/user_services/posting_spec.rb diff --git a/spec/models/user_services/querying_spec.rb b/spec/models/user_services/querying_spec.rb new file mode 100644 index 0000000..80000b0 --- /dev/null +++ b/spec/models/user_services/querying_spec.rb @@ -0,0 +1,376 @@ +# frozen_string_literal: true + +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +describe UserServices::Querying do + let(:alices_aspect) { alice.aspects.where(name: "generic").first } + let(:eves_aspect) { eve.aspects.where(name: "generic").first } + let(:bobs_aspect) { bob.aspects.where(name: "generic").first } + + describe "#visible_shareable_ids" do + it "contains your public posts" do + public_post = alice.post(:status_message, text: "hi", to: alices_aspect.id, public: true) + expect(alice.visible_shareable_ids(Post)).to include(public_post.id) + end + + it "contains your non-public posts" do + private_post = alice.post(:status_message, text: "hi", to: alices_aspect.id, public: false) + expect(alice.visible_shareable_ids(Post)).to include(private_post.id) + end + + it "contains public posts from people you're following" do + # Alice follows Eve, but Eve does not follow Alice + alice.share_with(eve.person, alices_aspect) + + # Eve posts a public status message + eves_public_post = eve.post(:status_message, text: "hello", to: "all", public: true) + + # Alice should see it + expect(alice.visible_shareable_ids(Post)).to include(eves_public_post.id) + end + + it "does not contain non-public posts from people who are following you" do + eve.share_with(alice.person, eves_aspect) + eves_post = eve.post(:status_message, text: "hello", to: eves_aspect.id) + expect(alice.visible_shareable_ids(Post)).not_to include(eves_post.id) + end + + it "does not contain non-public posts from aspects you're not in" do + dogs = bob.aspects.create(name: "dogs") + invisible_post = bob.post(:status_message, text: "foobar", to: dogs.id) + expect(alice.visible_shareable_ids(Post)).not_to include(invisible_post.id) + end + + it "includes the reshare id on type reshare" do + post = bob.post(:status_message, text: "hey", public: true, to: bobs_aspect.id) + reshare = bob.post(:reshare, root_guid: post.guid, to: bobs_aspect) + expect(alice.visible_shareable_ids(Post, type: "Reshare")).to include(reshare.id) + end + + it "does not include the reshare id on type statusMessage" do + post = bob.post(:status_message, text: "hey", public: true, to: bobs_aspect.id) + reshare = bob.post(:reshare, root_guid: post.guid, to: bobs_aspect) + expect(alice.visible_shareable_ids(Post, type: "StatusMessage")).not_to include(reshare.id) + end + + it "does not contain duplicate posts" do + bobs_other_aspect = bob.aspects.create(name: "cat people") + bob.add_contact_to_aspect(bob.contact_for(alice.person), bobs_other_aspect) + expect(bob.aspects_with_person(alice.person)).to contain_exactly(bobs_aspect, bobs_other_aspect) + + bobs_post = bob.post(:status_message, text: "hai to all my people", to: [bobs_aspect.id, bobs_other_aspect.id]) + + expect(alice.visible_shareable_ids(Post).length).to eq(1) + expect(alice.visible_shareable_ids(Post)).to include(bobs_post.id) + end + + describe "hidden posts" do + before do + aspect_to_post = bob.aspects.where(name: "generic").first + @status = bob.post(:status_message, text: "hello", to: aspect_to_post) + end + + it "pulls back non hidden posts" do + expect(alice.visible_shareable_ids(Post).include?(@status.id)).to be true + end + + it "does not pull back hidden posts" do + @status.share_visibilities.where(user_id: alice.id).first.update_attributes(hidden: true) + expect(alice.visible_shareable_ids(Post).include?(@status.id)).to be false + end + end + end + + describe "#prep_opts" do + it "defaults the opts" do + time = Time.now + allow(Time).to receive(:now).and_return(time) + expect(alice.send(:prep_opts, Post, {})).to eq({ + type: Stream::Base::TYPES_OF_POST_IN_STREAM, + order: "created_at DESC", + limit: 15, + hidden: false, + order_field: :created_at, + order_with_table: "posts.created_at DESC", + max_time: time + 1 + }) + end + end + + describe "#visible_shareables" do + it "never contains posts from people not in your aspects" do + create(:status_message, public: true) + expect(bob.visible_shareables(Post).count(:all)).to eq(0) + end + + context "with many posts" do + before do + time_interval = 1000 + time_past = 1_000_000 + (1..25).each do |n| + [alice, bob, eve].each do |u| + aspect_to_post = u.aspects.where(name: "generic").first + post = u.post :status_message, text: "#{u.username} - #{n}", to: aspect_to_post.id + post.created_at = (post.created_at - time_past) - time_interval + post.updated_at = (post.updated_at - time_past) + time_interval + post.save + time_interval += 1000 + end + end + end + + it "works" do + # The set up takes a looong time, so to save time we do several tests in one + expect(bob.visible_shareables(Post).length).to eq(15) # it returns 15 by default + expect(bob.visible_shareables(Post).map(&:id)).to eq(bob.visible_shareables(Post, by_members_of: bob.aspects.map {|a| + a.id + }).map(&:id)) # it is the same when joining through aspects + + # checks the default sort order + expect(bob.visible_shareables(Post).sort_by {|p| + p.created_at + }.map {|p| p.id }).to eq(bob.visible_shareables(Post).map {|p| + p.id + }.reverse) # it is sorted updated_at desc by default + + # It should respect the order option + opts = {order: "created_at DESC"} + expect(bob.visible_shareables(Post, + opts).first.created_at).to be > bob.visible_shareables(Post, opts).last.created_at + + # It should respect the order option + opts = {order: "updated_at DESC"} + expect(bob.visible_shareables(Post, + opts).first.updated_at).to be > bob.visible_shareables(Post, opts).last.updated_at + + # It should respect the limit option + opts = {limit: 40} + expect(bob.visible_shareables(Post, opts).length).to eq(40) + expect(bob.visible_shareables(Post, + opts).map(&:id)).to eq(bob.visible_shareables(Post, opts.merge(by_members_of: bob.aspects.map {|a| + a.id + })).map(&:id)) + expect(bob.visible_shareables(Post, opts).sort_by {|p| + p.created_at + }.map {|p| p.id }).to eq(bob.visible_shareables(Post, opts).map {|p| + p.id + }.reverse) + + # It should paginate using a datetime timestamp + last_time_of_last_page = bob.visible_shareables(Post).last.created_at + opts = {max_time: last_time_of_last_page} + expect(bob.visible_shareables(Post, opts).length).to eq(15) + expect(bob.visible_shareables(Post, opts).map {|p| + p.id + }).to eq(bob.visible_shareables(Post, opts.merge(by_members_of: bob.aspects.map {|a| + a.id + })).map {|p| + p.id + }) + expect(bob.visible_shareables(Post, opts).sort_by {|p| + p.created_at + }.map {|p| p.id }).to eq(bob.visible_shareables(Post, opts).map {|p| + p.id + }.reverse) + expect(bob.visible_shareables(Post, opts).map {|p| + p.id + }).to eq(bob.visible_shareables(Post, limit: 40)[15...30].map {|p| + p.id + }) # pagination should return the right posts + + # It should paginate using an integer timestamp + opts = {max_time: last_time_of_last_page.to_i} + expect(bob.visible_shareables(Post, opts).length).to eq(15) + expect(bob.visible_shareables(Post, opts).map {|p| + p.id + }).to eq(bob.visible_shareables(Post, opts.merge(by_members_of: bob.aspects.map {|a| + a.id + })).map {|p| + p.id + }) + expect(bob.visible_shareables(Post, opts).sort_by {|p| + p.created_at + }.map {|p| p.id }).to eq(bob.visible_shareables(Post, opts).map {|p| + p.id + }.reverse) + expect(bob.visible_shareables(Post, opts).map {|p| + p.id + }).to eq(bob.visible_shareables(Post, limit: 40)[15...30].map {|p| + p.id + }) # pagination should return the right posts + end + end + end + + describe "#find_visible_shareable_by_id" do + it "returns a post if you can see it" do + bobs_post = bob.post(:status_message, text: "hi", to: bobs_aspect.id, public: false) + expect(alice.find_visible_shareable_by_id(Post, bobs_post.id)).to eq(bobs_post) + end + + it "returns nil if you can't see that post" do + dogs = bob.aspects.create(name: "dogs") + invisible_post = bob.post(:status_message, text: "foobar", to: dogs.id) + expect(alice.find_visible_shareable_by_id(Post, invisible_post.id)).to be_nil + end + end + + context "with two users" do + describe "#people_in_aspects" do + it "returns people objects for a users contact in each aspect" do + expect(alice.people_in_aspects([alices_aspect])).to eq([bob.person]) + end + + it "returns local/remote people objects for a users contact in each aspect" do + local_user1 = create(:user) + local_user2 = create(:user) + remote_person = create(:person) + + asp1 = local_user1.aspects.create(name: "lol") + asp2 = local_user2.aspects.create(name: "brb") + + connect_users(alice, alices_aspect, local_user1, asp1) + connect_users(alice, alices_aspect, local_user2, asp2) + alice.contacts.create!(person: remote_person, aspects: [alices_aspect], sharing: true) + + expect(alice.people_in_aspects([alices_aspect]).count).to eq(4) + expect(alice.people_in_aspects([alices_aspect], type: "remote").count).to eq(1) + expect(alice.people_in_aspects([alices_aspect], type: "local").count).to eq(3) + end + + it "does not return people not connected to user on same pod" do + create_list(:user, 3) + expect(alice.people_in_aspects([alices_aspect]).count).to eq(1) + end + + it "only returns non-pending contacts" do + expect(alice.people_in_aspects([alices_aspect])).to eq([bob.person]) + end + + it "returns an empty array when passed an aspect the user doesn't own" do + expect(alice.people_in_aspects([eves_aspect])).to eq([]) + end + end + end + + context "contact querying" do + let(:person_one) { create(:person) } + let(:person_two) { create(:person) } + let(:person_three) { create(:person) } + let(:aspect) { alice.aspects.create(name: "heroes") } + + describe "#contact_for_person_id" do + it "returns a contact" do + contact = Contact.create(user: alice, person: person_one, aspects: [aspect]) + alice.contacts << contact + expect(alice.contact_for_person_id(person_one.id)).to be_truthy + end + + it "returns the correct contact" do + contact = Contact.create(user: alice, person: person_one, aspects: [aspect]) + alice.contacts << contact + + contact2 = Contact.create(user: alice, person: person_two, aspects: [aspect]) + alice.contacts << contact2 + + contact3 = Contact.create(user: alice, person: person_three, aspects: [aspect]) + alice.contacts << contact3 + + expect(alice.contact_for_person_id(person_two.id).person).to eq(person_two) + end + + it "returns nil for a non-contact" do + expect(alice.contact_for_person_id(person_one.id)).to be_nil + end + + it "returns nil when someone else has contact with the target" do + contact = Contact.create(user: alice, person: person_one, aspects: [aspect]) + alice.contacts << contact + expect(eve.contact_for_person_id(person_one.id)).to be_nil + end + end + + describe "#contact_for" do + it "takes a person_id and returns a contact" do + expect(alice).to receive(:contact_for_person_id).with(person_one.id) + alice.contact_for(person_one) + end + + it "returns nil if the input is nil" do + expect(alice.contact_for(nil)).to be_nil + end + end + + describe "#aspects_with_person" do + let(:connected_person) { bob.person } + + it "returns the aspects with given contact" do + expect(alice.aspects_with_person(connected_person)).to eq([alices_aspect]) + end + + it "returns multiple aspects if the person is there" do + aspect2 = alice.aspects.create(name: "second") + contact = alice.contact_for(connected_person) + + alice.add_contact_to_aspect(contact, aspect2) + expect(alice.aspects_with_person(connected_person).to_set).to eq(alice.aspects.to_set) + end + end + end + + describe "#block_for" do + let(:person) { create(:person) } + + before do + eve.blocks.create({person: person}) + end + + it "returns the block" do + block = eve.block_for(person) + expect(block).to be_present + expect(block.person.id).to be person.id + end + end + + describe "#posts_from" do + let(:user_charley) { create(:user) } + + before do + @aspect3 = user_charley.aspects.create(name: "bros") + + @public_message = user_charley.post(:status_message, text: "hey there", to: "all", public: true) + @private_message = user_charley.post(:status_message, text: "hey there", to: @aspect3.id) + end + + it "displays public posts for a non-contact" do + expect(alice.posts_from(user_charley.person)).to include @public_message + end + + it "does not display private posts for a non-contact" do + expect(alice.posts_from(user_charley.person)).not_to include @private_message + end + + it "displays private and public posts for a non-contact after connecting" do + connect_users(alice, @alices_aspect, user_charley, @aspect3) + new_message = user_charley.post(:status_message, text: "hey there", to: @aspect3.id) + + alice.reload + + expect(alice.posts_from(user_charley.person)).to include @public_message + expect(alice.posts_from(user_charley.person)).to include new_message + end + + it "displays recent posts first" do + msg3 = user_charley.post(:status_message, text: "hey there", to: "all", public: true) + msg4 = user_charley.post(:status_message, text: "hey there", to: "all", public: true) + msg3.created_at = Time.now + 10 + msg3.save! + msg4.created_at = Time.now + 14 + msg4.save! + + expect(alice.posts_from(user_charley.person).map {|p| p.id }).to eq([msg4, msg3, @public_message].map {|p| p.id }) + end + end +end diff --git a/spec/models/user/social_actions_spec.rb b/spec/models/user_services/social_actions_spec.rb similarity index 90% rename from spec/models/user/social_actions_spec.rb rename to spec/models/user_services/social_actions_spec.rb index 8f2227a..30ff0df 100644 --- a/spec/models/user/social_actions_spec.rb +++ b/spec/models/user_services/social_actions_spec.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -require 'rails_helper' - -describe User::SocialActions, type: :model do - let(:status) { FactoryBot.create(:status_message, public: true, author: bob.person) } +describe UserServices::SocialActions do + let(:status) { create(:status_message, public: true, author: bob.person) } describe "User#comment!" do it "sets the comment text" do @@ -51,12 +49,12 @@ alice.like!(status) end - it "should be able to like on one's own status" do + it "is able to like on one's own status" do like = bob.like!(status) expect(status.reload.likes.first).to eq(like) end - it "should be able to like on a contact's status" do + it "is able to like on a contact's status" do like = alice.like!(status) expect(status.reload.likes.first).to eq(like) end @@ -71,7 +69,7 @@ end describe "User#participate_in_poll!" do - let(:poll) { FactoryBot.create(:poll, status_message: status) } + let(:poll) { create(:poll, status_message: status) } let(:answer) { poll.poll_answers.first } it "federates" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2e10f35..299af72 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -81,7 +81,7 @@ def params describe "send password instructions" do it "sends instructions async" do user = create(:user) - assert_enqueued_with(job: ResetPasswordJob, args: [user]) do + assert_enqueued_with(job: Workers::ResetPasswordJob, args: [user]) do user.send_reset_password_instructions end end diff --git a/spec/services/post_service_spec.rb b/spec/services/post_service_spec.rb index c8c6d70..b383da8 100644 --- a/spec/services/post_service_spec.rb +++ b/spec/services/post_service_spec.rb @@ -85,7 +85,7 @@ it "NonPublic if the post is private" do expect { PostService.new.find!(post.id) - }.to raise_error D Diaspora::Exceptions::NonPublic + }.to raise_error Diaspora::Exceptions::NonPublic end it "RecordNotFound if the post cannot be found" do diff --git a/spec/support/inlined_jobs.rb b/spec/support/inlined_jobs.rb index daef7e1..fe76acb 100644 --- a/spec/support/inlined_jobs.rb +++ b/spec/support/inlined_jobs.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "sidekiq/testing" + module HelperMethods def inlined_jobs Sidekiq::Worker.clear_all diff --git a/spec/support/user_methods.rb b/spec/support/user_methods.rb index 0f0ea7a..b799ec7 100644 --- a/spec/support/user_methods.rb +++ b/spec/support/user_methods.rb @@ -53,8 +53,8 @@ def build_comment(options={}) def disable_send_workers RSpec.current_example&.example_group_instance&.instance_eval do - allow(SendPrivateJob).to receive(:perform_later) - allow(SendPublicJob).to receive(:perform_later) + allow(Workers::SendPrivateJob).to receive(:perform_later) + allow(Workers::SendPublicJob).to receive(:perform_later) end end end