spec/models/person_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 Person, type: :model do
  before do
    @user = bob
    @person = FactoryBot.create(:person)
  end

  it "always has a profile" do
    expect(Person.new.profile).not_to be_nil
  end

  it "does not save automatically" do
    expect(Person.new.persisted?).to be false
    expect(Person.new.profile.persisted?).to be false
  end

  context "scopes" do
    describe ".for_json" do
      it "does not select public keys" do
        expect {
          Person.for_json.first.serialized_public_key
        }.to raise_error ActiveModel::MissingAttributeError
      end
    end

    describe ".local" do
      it "returns only local people" do
        expect(Person.local.map(&:id)).to include(@user.person.id)
        expect(Person.local.map(&:id)).to_not include(@person.id)
      end
    end

    describe ".remote" do
      it "returns only remote people" do
        expect(Person.remote.map(&:id)).to include(@person.id)
        expect(Person.remote.map(&:id)).to_not include(@user.person.id)
      end
    end

    describe ".find_person_from_guid_or_username" do
      it "searchs for a person if id is passed" do
        expect(Person.find_from_guid_or_username(id: @person.guid).id).to eq(@person.id)
      end

      it "searchs a person from a user if username is passed" do
        expect(Person.find_from_guid_or_username(username: @user.username).id).to eq(@user.person.id)
      end

      it "throws active record not found exceptions if no person is found via id" do
        expect {
          Person.find_from_guid_or_username(id: "2d13123")
        }.to raise_error ActiveRecord::RecordNotFound
      end

      it "throws active record not found exceptions if no person is found via username" do
        expect {
          Person.find_from_guid_or_username(username: "michael_jackson")
        }.to raise_error ActiveRecord::RecordNotFound
      end
    end

    describe ".all_from_aspects" do
      it "pulls back the right people given all a user's aspects" do
        aspect_ids = bob.aspects.map(&:id)
        expect(Person.all_from_aspects(aspect_ids, bob).map(&:id))
          .to match_array(bob.contacts.includes(:person).map {|c| c.person.id })
      end

      it "pulls back the right people given a subset of aspects" do
        aspect_ids = bob.aspects.first.id
        expect(Person.all_from_aspects(aspect_ids, bob).map(&:id))
          .to match_array(bob.aspects.first.contacts.includes(:person).map {|c| c.person.id })
      end

      it "respects aspects given a user" do
        aspect_ids = alice.aspects.map(&:id)
        expect(Person.all_from_aspects(aspect_ids, bob).map(&:id)).to eq([])
      end
    end

    describe ".who_have_reshared a user's posts" do
      it "pulls back users who reshared the status message of a user" do
        sm = FactoryBot.create(:status_message, author: alice.person, public: true)
        reshare = FactoryBot.create(:reshare, root: sm)
        expect(Person.who_have_reshared_a_users_posts(alice)).to eq([reshare.author])
      end
    end

    # rubocop:disable Rails/DynamicFindBy
    describe ".find_by_substring" do
      it "returns \"none\" when the substring is less than 1 non-space character" do
        expect(Person.find_by_substring("R")).to eq(Person.none)
        expect(Person.find_by_substring("R  ")).to eq(Person.none)
        expect(Person.find_by_substring("")).to eq(Person.none)
        expect(Person.find_by_substring("  ")).to eq(Person.none)
      end

      it "finds a person with a profile name containing the substring" do
        substring = r_str
        person = FactoryBot.create(:person, first_name: "A#{substring}A")
        expect(Person.find_by_substring(substring)).to include(person)
      end

      it "finds a person with a diaspora ID starting with the substring" do
        substring = r_str
        person = FactoryBot.create(:person, diaspora_handle: "#{substring}A@pod.tld")
        expect(Person.find_by_substring(substring)).to include(person)
      end
    end

    describe ".allowed_to_be_mentioned_in_a_comment_to" do
      let(:status_bob) { bob.post(:status_message, text: "hello", to: bob.aspects.first.id) }

      it "returns the author and people who have commented or liked the private post" do
        kate = FactoryBot.create(:user_with_aspect, friends: [bob])
        olga = FactoryBot.create(:user_with_aspect, friends: [bob])
        alice.comment!(status_bob, "why so formal?")
        eve.comment!(status_bob, "comment text")
        kate.like!(status_bob)
        olga.participate!(status_bob)
        expect(
          Person.allowed_to_be_mentioned_in_a_comment_to(status_bob).ids
        ).to match_array([alice, bob, eve, kate].map(&:person_id))
      end

      it "selects distinct people" do
        alice.comment!(status_bob, "hi")
        alice.comment!(status_bob, "how are you?")
        expect(
          Person.allowed_to_be_mentioned_in_a_comment_to(status_bob).ids
        ).to match_array([alice, bob].map(&:person_id))
      end

      it "returns all for public posts" do
        status_bob.update(public: true) # set parent public
        expect(Person.allowed_to_be_mentioned_in_a_comment_to(status_bob).ids).to match_array(Person.ids)
      end
    end
    # rubocop:enable Rails/DynamicFindBy

    describe ".sort_for_mention_suggestion" do
      let(:status_message) { FactoryBot.create(:status_message) }

      it "returns people sorted in the order: post author > commenters > likers > contacts" do
        like = FactoryBot.create(:like, target: status_message)
        comment = FactoryBot.create(:comment, post: status_message)
        current_user = FactoryBot.create(:user_with_aspect, friends: [alice])
        result = Person.select(:id, :guid).sort_for_mention_suggestion(status_message, current_user)
        expect(result[0]).to eq(status_message.author)
        expect(result[1]).to eq(comment.author)
        expect(result[2]).to eq(like.author)
        expect(result[3]).to eq(alice.person) # a contact of the current user
      end

      it "sorts people of the same priority by profile name" do
        current_user = FactoryBot.create(:user_with_aspect)
        person1 = FactoryBot.create(:person, first_name: "x2")
        person2 = FactoryBot.create(:person, first_name: "x1")
        result = Person
                 .select(:id, :guid)
                 .where(id: [person1.id, person2.id])
                 .sort_for_mention_suggestion(status_message, current_user)
        expect(result[0].id).to eq(person2.id)
        expect(result[1].id).to eq(person1.id)
      end

      it "sorts people of the same priority and same names by diaspora ID" do
        current_user = FactoryBot.create(:user_with_aspect)
        person1 = FactoryBot.create(:person, diaspora_handle: "x2@pod.tld")
        person1.profile.update(first_name: "John", last_name: "Doe")
        person2 = FactoryBot.create(:person, diaspora_handle: "x1@pod.tld")
        person2.profile.update(first_name: "John", last_name: "Doe")
        result = Person
                 .select(:id, :guid)
                 .where(id: [person1.id, person2.id])
                 .sort_for_mention_suggestion(status_message, current_user)
        expect(result[0].id).to eq(person2.id)
        expect(result[1].id).to eq(person1.id)
      end
    end

    describe ".in_aspects" do
      it "returns person that is in the aspect" do
        aspect = FactoryBot.create(:aspect)
        contact = FactoryBot.create(:contact, user: aspect.user)
        aspect.contacts << contact
        expect(Person.in_aspects([aspect.id])).to include(contact.person)
      end

      it "returns same person in multiple aspects only once" do
        user = bob
        contact = FactoryBot.create(:contact, user: user)
        ids = Array.new(2) do
          aspect = FactoryBot.create(:aspect, user: user, name: r_str)
          aspect.contacts << contact
          aspect.id
        end

        expect(Person.in_aspects(ids)).to eq([contact.person])
      end
    end
  end

  describe "delegating" do
    it "delegates last_name to the profile" do
      expect(@person.last_name).to eq(@person.profile.last_name)
      @person.profile.update(last_name: "Heathers")
      expect(@person.reload.last_name).to eq("Heathers")
    end
  end

  describe "valid url" do
    context "https urls" do
      let(:person) { FactoryBot.build(:person, pod: Pod.find_or_create_by(url: "https://example.com")) }

      it "should add trailing slash" do
        expect(person.url).to eq("https://example.com/")
      end

      it "should return the receive url" do
        expect(person.receive_url).to eq("https://example.com/receive/users/#{person.guid}")
      end

      it "should return the atom url" do
        expect(person.atom_url).to eq("https://example.com/public/#{person.username}.atom")
      end

      it "should return the profile url" do
        expect(person.profile_url).to eq("https://example.com/u/#{person.username}")
      end
    end

    context "messed up urls" do
      let(:person) {
        FactoryBot.build(:person, pod: Pod.find_or_create_by(url: "https://example.com/a/bit/messed/up"))
      }

      it "should return the correct url" do
        expect(person.url).to eq("https://example.com/")
      end

      it "should return the correct receive url" do
        expect(person.receive_url).to eq("https://example.com/receive/users/#{person.guid}")
      end

      it "should return the correct atom url" do
        expect(person.atom_url).to eq("https://example.com/public/#{person.username}.atom")
      end

      it "should return the correct profile url" do
        expect(person.profile_url).to eq("https://example.com/u/#{person.username}")
      end
    end

    it "should allow ports in the url" do
      person = FactoryBot.build(:person, pod: Pod.find_or_create_by(url: "https://example.com:3000/"))
      expect(person.url).to eq("https://example.com:3000/")
    end

    it "should remove https port in the url" do
      person = FactoryBot.build(:person, pod: Pod.find_or_create_by(url: "https://example.com:443/"))
      expect(person.url).to eq("https://example.com/")
    end
  end

  describe "#diaspora_handle" do
    context "local people" do
      it "uses the pod config url to set the diaspora_handle" do
        new_person = User.build(username: "foo123", email: "foo123@example.com",
          password: "password", password_confirmation: "password").person
        expect(new_person.diaspora_handle).to eq("foo123#{User.diaspora_id_host}")
      end

      it "does not include www if it is set in app config" do
        allow(AppConfig).to receive(:pod_uri).and_return(Addressable::URI.parse("https://www.foobar.com/"))
        new_person = User.build(username: "foo123", email: "foo123@example.com", password: "password",
          password_confirmation: "password").person
        expect(new_person.diaspora_handle).to eq("foo123@foobar.com")
      end
    end

    context "remote people" do
      it "stores the diaspora_handle in the database" do
        expect(@person.diaspora_handle.include?(AppConfig.pod_uri.host)).to be false
      end
    end

    describe "validation" do
      it "is unique" do
        person_two = FactoryBot.build(:person, diaspora_handle: @person.diaspora_handle)
        expect(person_two).not_to be_valid
      end

      it "is case insensitive" do
        person_two = FactoryBot.build(:person, diaspora_handle: @person.diaspora_handle.upcase)
        expect(person_two).not_to be_valid
      end
    end
  end

  context ".name_from_attrs" do
    before do
      @person = alice.person
      @profile = @person.profile
    end

    context "with only first name" do
      it "should return their first name for name" do
        expect(Person.name_from_attrs(@profile.first_name, nil, @profile.diaspora_handle))
          .to eq(@profile.first_name.strip)
      end
    end

    context "with only last name" do
      it "should return their last name for name" do
        expect(Person.name_from_attrs(nil, @profile.last_name, @profile.diaspora_handle))
          .to eq(@profile.last_name.strip)
      end
    end

    context "with both first and last name" do
      it "should return their composed name for name" do
        expect(Person.name_from_attrs(@profile.first_name, @profile.last_name, @profile.diaspora_handle))
          .to eq("#{@profile.first_name.strip} #{@profile.last_name.strip}")
      end
    end

    context "without first nor last name" do
      it "should display their diaspora handle" do
        expect(Person.name_from_attrs(nil, nil, @profile.diaspora_handle)).to eq(@profile.diaspora_handle)
      end
    end
  end

  describe "#name" do
    it "calls Person.name_from_attrs" do
      profile = alice.person.profile
      expect(Person).to receive(:name_from_attrs)
        .with(profile.first_name, profile.last_name, profile.person.diaspora_handle)
      alice.name
    end

    it "strips endline whitespace" do
      profile = alice.person.profile
      profile.first_name = "maxwell "
      profile.last_name = "salzberg "
      expect(alice.name).to eq("maxwell salzberg")
    end
  end

  it "#owns? posts" do
    person_message = FactoryBot.create(:status_message, author: @person)
    person_two = FactoryBot.create(:person)

    expect(@person.owns?(person_message)).to be true
    expect(person_two.owns?(person_message)).to be false
  end

  describe "disconnecting" do
    before do
      @user2 = FactoryBot.create(:user)
      @aspect = @user.aspects.create(name: "Dudes")
      @aspect2 = @user2.aspects.create(name: "Abscence of Babes")
    end
    it "should not delete an orphaned contact" do
      @user.contacts.create(person: @person, aspects: [@aspect])

      expect { @user.disconnect(@user.contact_for(@person)) }.not_to change(Person, :count)
    end

    it "should not delete an un-orphaned contact" do
      @user.contacts.create(person: @person, aspects: [@aspect])
      @user2.contacts.create(person: @person, aspects: [@aspect2])

      expect { @user.disconnect(@user.contact_for(@person)) }.not_to change(Person, :count)
    end
  end

  describe "#first_name" do
    it "returns username if first_name is not present in profile" do
      alice.person.profile.update(first_name: "")
      expect(alice.person.first_name).to eq(alice.username)
    end

    it "returns first words in first_name if first_name is present" do
      alice.person.profile.update(first_name: "First Mid Last")
      expect(alice.person.first_name).to eq("First Mid")
    end

    it "returns first word in first_name if first_name is present" do
      alice.person.profile.update(first_name: "Alice")
      expect(alice.person.first_name).to eq("Alice")
    end
  end

  describe ".search" do
    before do
      Person.delete_all
      @user = FactoryBot.create(:user_with_aspect)
      user_profile = @user.person.profile
      user_profile.first_name = "aiofj"
      user_profile.last_name = "asdji"
      user_profile.save

      @robert_grimm = FactoryBot.build(:person)
      @eugene_weinstein = FactoryBot.build(:person)
      @yevgeniy_dodis = FactoryBot.build(:person)
      @casey_grippi = FactoryBot.build(:person)
      @invisible_person = FactoryBot.build(:person)
      @closed_account = FactoryBot.build(:person, closed_account: true)

      @robert_grimm.profile.first_name = "Robert"
      @robert_grimm.profile.last_name = "Grimm"
      @robert_grimm.profile.save
      @robert_grimm.reload

      @eugene_weinstein.profile.first_name = "Eugene"
      @eugene_weinstein.profile.last_name = "Weinstein"
      @eugene_weinstein.profile.save
      @eugene_weinstein.reload

      @yevgeniy_dodis.profile.first_name = "Yevgeniy"
      @yevgeniy_dodis.profile.last_name = "Dodis"
      @yevgeniy_dodis.profile.save
      @yevgeniy_dodis.reload

      @casey_grippi.profile.first_name = "Casey"
      @casey_grippi.profile.last_name = "Grippi"
      @casey_grippi.profile.save
      @casey_grippi.reload

      @invisible_person.profile.first_name = "Johnson"
      @invisible_person.profile.last_name = "Invisible"
      @invisible_person.profile.searchable = false
      @invisible_person.profile.save
      @invisible_person.reload

      @closed_account.profile.first_name = "Closed"
      @closed_account.profile.last_name = "Account"
      @closed_account.profile.save
      @closed_account.reload
    end

    it "orders results by last name" do
      @robert_grimm.profile.first_name = "AAA"
      @robert_grimm.profile.save!

      @eugene_weinstein.profile.first_name = "AAA"
      @eugene_weinstein.profile.save!

      @yevgeniy_dodis.profile.first_name = "AAA"
      @yevgeniy_dodis.profile.save!

      @casey_grippi.profile.first_name = "AAA"
      @casey_grippi.profile.save!

      people = Person.search("AAA", @user)
      expect(people.map(&:name)).to eq([@yevgeniy_dodis, @robert_grimm, @casey_grippi, @eugene_weinstein].map(&:name))
    end

    it "returns nothing on an empty query" do
      people = Person.search("", @user)
      expect(people).to be_empty
    end

    it "returns nothing on a one-character query" do
      people = Person.search("i", @user)
      expect(people).to be_empty
    end

    it "returns results for partial names" do
      people = Person.search("Eug", @user)
      expect(people.count).to eq(1)
      expect(people.first).to eq(@eugene_weinstein)

      people = Person.search("wEi", @user)
      expect(people.count).to eq(1)
      expect(people.first).to eq(@eugene_weinstein)

      people = Person.search("gri", @user)
      expect(people.count).to eq(2)
      expect(people.first).to eq(@robert_grimm)
      expect(people.second).to eq(@casey_grippi)
    end

    it "returns results for full names" do
      people = Person.search("Casey Grippi", @user)
      expect(people.count).to eq(1)
      expect(people.first).to eq(@casey_grippi)
    end

    it "doesn't display people that are neither searchable nor contacts" do
      expect(Person.search("Johnson", @user)).to be_empty
    end

    it "doesn't display closed accounts" do
      expect(Person.search("Closed", @user)).to be_empty
      expect(Person.search("Account", @user)).to be_empty
      expect(Person.search(@closed_account.diaspora_handle, @user)).to be_empty
    end

    it "displays contacts that are not searchable" do
      @user.contacts.create(person: @invisible_person, aspects: [@user.aspects.first])
      people = Person.search("Johnson", @user)
      expect(people.count).to eq(1)
      expect(people.first).to eq(@invisible_person)
    end

    it "returns results for Diaspora handles" do
      people = Person.search(@robert_grimm.diaspora_handle, @user)
      expect(people).to eq([@robert_grimm])
    end

    it "puts the searching user's contacts first" do
      @robert_grimm.profile.first_name = "AAA"
      @robert_grimm.profile.save!

      @eugene_weinstein.profile.first_name = "AAA"
      @eugene_weinstein.profile.save!

      @yevgeniy_dodis.profile.first_name = "AAA"
      @yevgeniy_dodis.profile.save!

      @casey_grippi.profile.first_name = "AAA"
      @casey_grippi.profile.save!

      @user.contacts.create(person: @casey_grippi, aspects: [@user.aspects.first])

      people = Person.search("AAA", @user)
      expect(people.map(&:name)).to eq([@casey_grippi, @yevgeniy_dodis, @robert_grimm, @eugene_weinstein].map(&:name))
    end

    context "only contacts" do
      before do
        @robert_contact = @user.contacts.create(person: @robert_grimm, aspects: [@user.aspects.first])
        @eugene_contact = @user.contacts.create(person: @eugene_weinstein, aspects: [@user.aspects.first])
        @invisible_contact = @user.contacts.create(person: @invisible_person, aspects: [@user.aspects.first])
      end

      it "orders results by last name" do
        @robert_grimm.profile.first_name = "AAA"
        @robert_grimm.profile.save!

        @eugene_weinstein.profile.first_name = "AAA"
        @eugene_weinstein.profile.save!

        @casey_grippi.profile.first_name = "AAA"
        @casey_grippi.profile.save!

        people = Person.search("AAA", @user, only_contacts: true)
        expect(people.map(&:name)).to eq([@robert_grimm, @eugene_weinstein].map(&:name))
      end

      it "returns nothing on an empty query" do
        people = Person.search("", @user, only_contacts: true)
        expect(people).to be_empty
      end

      it "returns nothing on a one-character query" do
        people = Person.search("i", @user, only_contacts: true)
        expect(people).to be_empty
      end

      it "returns results for partial names" do
        people = Person.search("Eug", @user, only_contacts: true)
        expect(people.count).to eq(1)
        expect(people.first).to eq(@eugene_weinstein)

        people = Person.search("wEi", @user, only_contacts: true)
        expect(people.count).to eq(1)
        expect(people.first).to eq(@eugene_weinstein)

        @user.contacts.create(person: @casey_grippi, aspects: [@user.aspects.first])
        people = Person.search("gri", @user, only_contacts: true)
        expect(people.count).to eq(2)
        expect(people.first).to eq(@robert_grimm)
        expect(people.second).to eq(@casey_grippi)
      end

      it "returns results for full names" do
        people = Person.search("Robert Grimm", @user, only_contacts: true)
        expect(people.count).to eq(1)
        expect(people.first).to eq(@robert_grimm)
      end

      it "returns results for Diaspora handles" do
        people = Person.search(@robert_grimm.diaspora_handle, @user, only_contacts: true)
        expect(people).to eq([@robert_grimm])
      end
    end
  end

  describe "#public_key" do
    it "returns the public key for the person" do
      key = @person.public_key
      expect(key).to be_a(OpenSSL::PKey::RSA)
      expect(key.to_s).to eq(@person.serialized_public_key)
    end

    it "handles broken keys and returns nil" do
      @person.update(serialized_public_key: "broken")
      expect(@person.public_key).to be_nil
    end
  end

  context "people finders for webfinger" do
    let(:user) { FactoryBot.create(:user) }
    let(:person) { FactoryBot.create(:person) }

    describe ".by_account_identifier" do
      it "should find a local users person" do
        p = Person.by_account_identifier(user.diaspora_handle)
        expect(p).to eq(user.person)
      end

      it "should find remote users person" do
        p = Person.by_account_identifier(person.diaspora_handle)
        expect(p).to eq(person)
      end

      it "should downcase and strip the diaspora_handle" do
        dh_upper = "    " + user.diaspora_handle.upcase + "   "
        expect(Person.by_account_identifier(dh_upper)).to eq(user.person)
      end

      it "finds a local person with a mixed-case username" do
        user = FactoryBot.create(:user, username: "SaMaNtHa")
        person = Person.by_account_identifier(user.person.diaspora_handle)
        expect(person).to eq(user.person)
      end

      it "is case insensitive" do
        user1 = FactoryBot.create(:user, username: "SaMaNtHa")
        person = Person.by_account_identifier(user1.person.diaspora_handle.upcase)
        expect(person).to eq(user1.person)
      end

      it "should only find people who are exact matches (1/2)" do
        FactoryBot.create(:person, diaspora_handle: "tomtom@tom.joindiaspora.com")
        FactoryBot.create(:person, diaspora_handle: "tom@tom.joindiaspora.com")
        expect(Person.by_account_identifier("tom@tom.joindiaspora.com").diaspora_handle)
          .to eq("tom@tom.joindiaspora.com")
      end

      it "should only find people who are exact matches (2/2)" do
        FactoryBot.create(:person, diaspora_handle: "tomtom@tom.joindiaspora.com")
        FactoryBot.create(:person, diaspora_handle: "tom@tom.joindiaspora.comm")
        expect(Person.by_account_identifier("tom@tom.joindiaspora.com")).to be_nil
      end
    end
  end

  describe "#has_photos?" do
    it "returns false if the user has no photos" do
      expect(alice.person.has_photos?).to be false
    end

    it "returns true if the user has photos" do
      alice.post(:photo, user_file: uploaded_photo, to: alice.aspects.first.id)

      expect(alice.person.has_photos?).to be true
    end
  end

  describe "#as_json" do
    it "returns a hash representation of a person" do
      expect(@person.as_json).to eq(
        id:     @person.id,
        guid:   @person.guid,
        name:   @person.name,
        avatar: @person.profile.image_url(size: :thumb_medium),
        handle: @person.diaspora_handle,
        url:    Rails.application.routes.url_helpers.person_path(@person)
      )
    end
    it "return tags if asked" do
      expect(@person.as_json(includes: "tags"))
        .to eq(@person.as_json.merge(tags: @person.profile.tags.map {|t| "##{t.name}" }))
    end
  end

  describe ".community_spotlight" do
    describe "when the pod owner hasn't set up any community spotlight members" do
      it "returns people with the community spotlight role" do
        Role.add_spotlight(bob.person)
        expect(Person.community_spotlight).to be_present
      end

      it "returns an empty array" do
        expect(Person.community_spotlight).to eq([])
      end
    end
  end

  describe "#lock_access!" do
    it "sets the closed_account flag" do
      @person.lock_access!
      expect(@person.reload.closed_account).to be true
    end
  end

  describe "#clear_profile!!" do
    before do
      @person = FactoryBot.build(:person)
    end

    it "calls Profile#tombstone!" do
      expect(@person.profile).to receive(:tombstone!)
      @person.clear_profile!
    end
  end

  context "validation" do
    it "validates that no other person with same guid exists" do
      person = FactoryBot.build(:person)
      person.guid = alice.guid

      expect(person.valid?).to be_falsey
      expect(person.errors.full_messages).to include("Person with same GUID already exists: #{alice.diaspora_handle}")
    end
  end
end
