spec/models/post_spec.rb
# 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 Post, type: :model do
  describe "scopes" do
    describe ".owned_or_visible_by_user" do
      before do
        @you = bob
        @public_post = FactoryBot.create(:status_message, public: true)
        @your_post = FactoryBot.create(:status_message, author: @you.person)
        @post_from_contact = eve.post(:status_message, text: "wooo", to: eve.aspects.where(name: "generic").first)
        @post_from_stranger = FactoryBot.create(:status_message, public: false)
      end

      it "returns post from your contacts" do
        expect(StatusMessage.owned_or_visible_by_user(@you)).to include(@post_from_contact)
      end

      it "returns your posts" do
        expect(StatusMessage.owned_or_visible_by_user(@you)).to include(@your_post)
      end

      it "returns public posts" do
        expect(StatusMessage.owned_or_visible_by_user(@you)).to include(@public_post)
      end

      it "returns public post from your contact" do
        sm = FactoryBot.create(:status_message, author: eve.person, public: true)

        expect(StatusMessage.owned_or_visible_by_user(@you)).to include(sm)
      end

      it "does not return non contacts, non-public post" do
        expect(StatusMessage.owned_or_visible_by_user(@you)).not_to include(@post_from_stranger)
      end

      it "should return the three visible posts" do
        expect(StatusMessage.owned_or_visible_by_user(@you).count(:all)).to eq(3)
      end
    end

    describe ".all_public" do
      it "includes all public posts" do
        post1 = FactoryBot.create(:status_message, author: alice.person, public: true)
        post2 = FactoryBot.create(:status_message, author: bob.person, public: true)
        post3 = FactoryBot.create(:status_message, author: eve.person, public: true)
        expect(Post.all_public.ids).to match_array([post1.id, post2.id, post3.id])
      end

      it "doesn't include any private posts" do
        FactoryBot.create(:status_message, author: alice.person, public: false)
        FactoryBot.create(:status_message, author: bob.person, public: false)
        FactoryBot.create(:status_message, author: eve.person, public: false)
        expect(Post.all_public.ids).to eq([])
      end
    end

    describe ".all_local_public" do
      it "includes all public posts from local" do
        post1 = FactoryBot.create(:status_message, author: alice.person, public: true)
        post2 = FactoryBot.create(:status_message, author: bob.person, public: true)
        expect(Post.all_local_public.ids).to match_array([post1.id, post2.id])
      end

      it "doesn't include any posts from other pods" do
        pod = FactoryBot.create(:pod)
        external_person = FactoryBot.create(:person, pod: pod)
        FactoryBot.create(:status_message, author: alice.person, public: true)
        FactoryBot.create(:status_message, author: bob.person, public: true)
        post_from_extern = FactoryBot.create(:status_message, author: external_person, public: true)
        expect(Post.all_local_public.ids).not_to match_array([post_from_extern.id])
      end
    end

    describe ".for_a_stream" do
      it "calls #for_visible_shareable_sql" do
        time = double
        order = double
        expect(Post).to receive(:for_visible_shareable_sql).with(time, order).and_return(Post)
        Post.for_a_stream(time, order)
      end

      it "calls includes_for_a_stream" do
        expect(Post).to receive(:includes_for_a_stream)
        Post.for_a_stream(Time.zone.now, "created_at")
      end

      it "calls excluding_blocks if a user is present" do
        expect(Post).to receive(:excluding_blocks).with(alice).and_return(Post)
        Post.for_a_stream(Time.zone.now, "created_at", alice)
      end
    end

    describe ".excluding_blocks" do
      before do
        @post = FactoryBot.create(:status_message, author: alice.person)
        @other_post = FactoryBot.create(:status_message, author: eve.person)

        bob.blocks.create(person: alice.person)
      end

      it "does not included blocked users posts" do
        expect(Post.excluding_blocks(bob)).not_to include(@post)
      end

      it "includes not blocked users posts" do
        expect(Post.excluding_blocks(bob)).to include(@other_post)
      end

      it "returns posts if you dont have any blocks" do
        expect(Post.excluding_blocks(alice).count).to eq(2)
      end
    end

    describe ".excluding_hidden_shareables" do
      before do
        @post = FactoryBot.create(:status_message, author: alice.person)
        @other_post = FactoryBot.create(:status_message, author: eve.person)
        bob.toggle_hidden_shareable(@post)
      end
      it "excludes posts the user has hidden" do
        expect(Post.excluding_hidden_shareables(bob)).not_to include(@post)
      end
      it "includes posts the user has not hidden" do
        expect(Post.excluding_hidden_shareables(bob)).to include(@other_post)
      end
    end

    describe ".excluding_hidden_content" do
      it "calls excluding_blocks and excluding_hidden_shareables" do
        expect(Post).to receive(:excluding_blocks).and_return(Post)
        expect(Post).to receive(:excluding_hidden_shareables)
        Post.excluding_hidden_content(bob)
      end
    end

    context "having some posts" do
      before do
        time_interval = 1000
        time_past = 1_000_000
        @posts = (1..5).map do |n|
          aspect_to_post = alice.aspects.where(name: "generic").first
          post = alice.post :status_message, text: "#{alice.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
          post
        end
      end

      describe ".by_max_time" do
        it "returns the posts ordered and limited by unix time" do
          expect(Post.for_a_stream(Time.zone.now + 1, "created_at")).to eq(@posts)
          expect(Post.for_a_stream(Time.zone.now + 1, "updated_at")).to eq(@posts.reverse)
        end
      end

      describe ".for_visible_shareable_sql" do
        it "calls max_time" do
          time = Time.zone.now + 1
          expect(Post).to receive(:by_max_time).with(time, "created_at").and_return(Post)
          Post.for_visible_shareable_sql(time, "created_at")
        end

        it "defaults to 15 posts" do
          chain = double.as_null_object

          allow(Post).to receive(:by_max_time).and_return(chain)
          expect(chain).to receive(:limit).with(15).and_return(Post)
          Post.for_visible_shareable_sql(Time.zone.now + 1, "created_at")
        end

        context "with two posts with the same timestamp" do
          before do
            aspect_id = alice.aspects.where(name: "generic").first.id
            Timecop.freeze Time.zone.now do
              alice.post(:status_message, text: "first", to: aspect_id)
              alice.post(:status_message, text: "second", to: aspect_id)
            end
          end

          it "returns them in reverse creation order" do
            posts = Post.for_visible_shareable_sql(Time.zone.now + 1, "created_at")
            expect(posts.first.text).to eq("second")
            expect(posts.second.text).to eq("first")
            expect(posts.last.text).to eq("alice - 5")
          end
        end
      end
    end

    describe ".subscribed_by" do
      let(:user) { FactoryBot.create(:user) }

      context "when the user has a participation on a post" do
        let(:post) { FactoryBot.create(:status_message_with_participations, participants: [user]) }

        it "includes the post to the result set" do
          expect(Post.subscribed_by(user)).to eq([post])
        end
      end

      context "when the user doens't have a participation on a post" do
        before do
          FactoryBot.create(:status_message)
        end

        it "returns empty result set" do
          expect(Post.subscribed_by(user)).to be_empty
        end
      end
    end

    describe ".reshared_by" do
      let(:person) { FactoryBot.create(:person) }

      context "when the person has a reshare for a post" do
        let(:post) { FactoryBot.create(:reshare, author: person).root }

        it "includes the post to the result set" do
          expect(Post.reshared_by(person)).to eq([post])
        end
      end

      context "when the person has no reshare for a post" do
        before do
          FactoryBot.create(:status_message)
        end

        it "returns empty result set" do
          expect(Post.reshared_by(person)).to be_empty
        end
      end
    end
  end

  describe "validations" do
    it "validates uniqueness of guid and does not throw a db error" do
      message = FactoryBot.create(:status_message)
      expect(FactoryBot.build(:status_message, guid: message.guid)).not_to be_valid
    end
  end

  describe "post_type" do
    it "returns the class constant" do
      status_message = FactoryBot.create(:status_message)
      expect(status_message.post_type).to eq("StatusMessage")
    end
  end

  describe "deletion" do
    it "should delete a posts comments on delete" do
      post = FactoryBot.create(:status_message, author: alice.person)
      alice.comment!(post, "hey")
      post.destroy
      expect(Post.where(id: post.id).empty?).to eq(true)
      expect(Comment.where(text: "hey").empty?).to eq(true)
    end
  end

  describe ".diaspora_initialize" do
    it "takes provider_display_name" do
      sm = FactoryBot.create(:status_message, provider_display_name: "mobile")
      expect(StatusMessage.diaspora_initialize(sm.attributes.merge(author: bob.person))
        .provider_display_name).to eq("mobile")
    end
  end

  describe "#subscribers" do
    let(:user) { FactoryBot.create(:user_with_aspect) }

    before do
      user.share_with(alice.person, user.aspects.first)
    end

    context "private" do
      it "returns the people contained in the aspects the post appears in" do
        post = user.post(:status_message, text: "hello", to: user.aspects.first.id)

        expect(post.subscribers).to eq([alice.person])
      end

      it "returns empty if posted to an empty aspect" do
        empty_aspect = user.aspects.create(name: "empty")

        post = user.post(:status_message, text: "hello", to: empty_aspect.id)

        expect(post.subscribers).to eq([])
      end
    end

    context "public" do
      let(:post) { user.post(:status_message, text: "hello", public: true) }

      it "returns the author to ensure local delivery" do
        lonely_user = FactoryBot.create(:user)
        lonely_post = lonely_user.post(:status_message, text: "anyone?", public: true)
        expect(lonely_post.subscribers).to match_array([lonely_user.person])
      end

      it "returns all a users contacts if the post is public" do
        second_aspect = user.aspects.create(name: "winners")
        user.share_with(bob.person, second_aspect)

        expect(post.subscribers).to match_array([alice.person, bob.person, user.person])
      end

      it "adds resharers to subscribers" do
        FactoryBot.create(:reshare, root: post, author: eve.person)

        expect(post.subscribers).to match_array([alice.person, eve.person, user.person])
      end

      it "adds participants to subscribers" do
        eve.participate!(post)

        expect(post.subscribers).to match_array([alice.person, eve.person, user.person])
      end
    end
  end

  describe "Likeable#update_likes_counter" do
    before do
      @post = bob.post(:status_message, text: "hello", public: true)
      bob.like!(@post)
    end

    it "does not update updated_at" do
      old_time = Time.zone.now - 100
      Post.where(id: @post.id).update_all(updated_at: old_time) # rubocop:disable Rails/SkipsModelValidations
      expect(@post.reload.updated_at.to_i).to eq(old_time.to_i)
      @post.update_likes_counter
      expect(@post.reload.updated_at.to_i).to eq(old_time.to_i)
    end
  end

  describe "#receive" do
    it "creates a share visibility for the user" do
      user_ids = [alice.id, eve.id]
      post = FactoryBot.create(:status_message, author: bob.person)
      expect(ShareVisibility).to receive(:batch_import).with(user_ids, post)
      post.receive(user_ids)
    end

    it "does nothing for public post" do
      post = FactoryBot.create(:status_message, author: bob.person, public: true)
      expect(ShareVisibility).not_to receive(:batch_import)
      post.receive([alice.id])
    end

    it "does nothing if no recipients provided" do
      post = FactoryBot.create(:status_message, author: bob.person)
      expect(ShareVisibility).not_to receive(:batch_import)
      post.receive([])
    end
  end

  describe "#reshares_count" do
    before :each do
      @post = alice.post(:status_message, text: "hello", public: true)
      expect(@post.reshares.size).to eq(0)
    end

    describe "when post has not been reshared" do
      it "returns zero" do
        expect(@post.reshares_count).to eq(0)
      end
    end

    describe "when post has been reshared exactly 1 time" do
      before :each do
        expect(@post.reshares.size).to eq(0)
        @reshare = FactoryBot.create(:reshare, root: @post)
        @post.reload
        expect(@post.reshares.size).to eq(1)
      end

      it "returns 1" do
        expect(@post.reshares_count).to eq(1)
      end
    end

    describe "when post has been reshared more than once" do
      before :each do
        expect(@post.reshares.size).to eq(0)
        FactoryBot.create(:reshare, root: @post)
        FactoryBot.create(:reshare, root: @post)
        FactoryBot.create(:reshare, root: @post)
        @post.reload
        expect(@post.reshares.size).to eq(3)
      end

      it "returns the number of reshares" do
        expect(@post.reshares_count).to eq(3)
      end
    end
  end

  describe "#after_create" do
    it "sets #interacted_at" do
      post = FactoryBot.create(:status_message)
      expect(post.interacted_at).not_to be_blank
    end
  end

  describe "#before_destroy" do
    it "removes root_guid from reshares" do
      post = FactoryBot.create(:status_message, author: alice.person, public: true)
      reshare = FactoryBot.create(:reshare, author: bob.person, root: post)
      post.destroy!
      expect(reshare.reload.root_guid).to be_nil
    end
  end
end
