spec/integration/account_migration_spec.rb
# frozen_string_literal: true

require "integration/federation/federation_helper"

def create_remote_contact(user, pod_host)
  FactoryBot.create(
    :contact,
    user:   user,
    person: FactoryBot.create(
      :person,
      pod:             Pod.find_or_create_by(url: "http://#{pod_host}"),
      diaspora_handle: "#{r_str}@#{pod_host}"
    )
  )
end

shared_examples_for "every migration scenario" do
  it "updates person references" do
    contact = FactoryBot.create(:contact, person: old_person)
    post = FactoryBot.create(:status_message, author: old_person)
    reshare = FactoryBot.create(:reshare, author: old_person)
    photo = FactoryBot.create(:photo, author: old_person)
    comment = FactoryBot.create(:comment, author: old_person)
    like = FactoryBot.create(:like, author: old_person)
    participation = FactoryBot.create(:participation, author: old_person)
    poll_participation = FactoryBot.create(:poll_participation, author: old_person)
    mention = FactoryBot.create(:mention, person: old_person)
    message = FactoryBot.create(:message, author: old_person)
    conversation = FactoryBot.create(:conversation, author: old_person)
    block = FactoryBot.create(:user).blocks.create(person: old_person)
    role = FactoryBot.create(:role, person: old_person)

    # Create ConversationVisibility by creating a conversation with participants
    conversation2 = FactoryBot.build(:conversation)
    FactoryBot.create(:contact, user: old_user, person: conversation2.author) if old_person.local?
    conversation2.participants << old_person
    conversation2.save!
    visibility = ConversationVisibility.find_by(person_id: old_person.id)

    # In order to create a notification actor we need to create a notification first
    notification = FactoryBot.build(:notification)
    notification.actors << old_person
    notification.save!
    actor = notification.notification_actors.find_by(person_id: old_person.id)

    run_migration

    expect(contact.reload.person).to eq(new_person)
    expect(post.reload.author).to eq(new_person)
    expect(reshare.reload.author).to eq(new_person)
    expect(photo.reload.author).to eq(new_person)
    expect(comment.reload.author).to eq(new_person)
    expect(like.reload.author).to eq(new_person)
    expect(participation.reload.author).to eq(new_person)
    expect(poll_participation.reload.author).to eq(new_person)
    expect(mention.reload.person).to eq(new_person)
    expect(message.reload.author).to eq(new_person)
    expect(conversation.reload.author).to eq(new_person)
    expect(block.reload.person).to eq(new_person)
    expect(role.reload.person).to eq(new_person)

    expect(visibility.reload.person).to eq(new_person)
    expect(actor.reload.person).to eq(new_person)
  end

  describe "old person account is closed and profile is cleared" do
    subject { old_person }

    before do
      run_migration
      subject.reload
    end

    include_examples "it makes account closed and clears profile"
  end

  describe "old person doesn't have any reference left" do
    let(:person) { old_person }

    before do
      DataGenerator.create(person, :generic_person_data)
    end

    def account_removal_method
      run_migration
      person.reload
    end

    include_examples "it removes the person associations"

    it "removes the person conversations" do
      expect {
        account_removal_method
      }.to change(nil, "conversations empty?") { Conversation.where(author: person).empty? }
        .to(be_truthy)
        .and(change(nil, "conversation visibilities of other participants empty?") {
          ConversationVisibility.where(conversation: Conversation.where(author: person)).empty?
        }.to(be_truthy))
    end
  end
end

shared_examples_for "migration scenarios with local old user" do
  it "locks the old user account" do
    run_migration
    expect(old_user.reload).to be_a_locked_account
  end
end

shared_examples_for "migration scenarios initiated remotely" do
  it "resends known contacts to the new user" do
    2.times do
      contact = FactoryBot.create(:contact, person: old_user.person, sharing: true)
      expect(DiasporaFederation::Federation::Sender).to receive(:private)
        .with(
          contact.user_id,
          "Contact:#{contact.user.diaspora_handle}:#{new_user.diaspora_handle}",
          kind_of(Hash)
        ).and_return([])
    end

    inlined_jobs { run_migration }
  end
end

shared_examples_for "migration scenarios initiated locally" do
  it "dispatches account migration message to the federation" do
    expect(DiasporaFederation::Federation::Sender).to receive(:public) do |sender_id, obj_str, urls, xml|
      if old_user.person.remote?
        expect(sender_id).to eq(old_user.diaspora_handle)
      else
        expect(sender_id).to eq(old_user.id)
      end
      expect(obj_str).to eq("AccountMigration:#{old_user.diaspora_handle}:#{new_user.diaspora_handle}")
      subscribers = [remote_contact.person]
      subscribers.push(old_user) if old_user.person.remote?
      expect(urls).to match_array(subscribers.map(&:url).map {|url| "#{url}receive/public" })

      entity = nil
      expect {
        magic_env = Nokogiri::XML(xml).root
        entity = DiasporaFederation::Salmon::MagicEnvelope
                 .unenvelop(magic_env, old_user.diaspora_handle).payload
      }.not_to raise_error

      expect(entity).to be_a(DiasporaFederation::Entities::AccountMigration)
      expect(entity.author).to eq(old_user.diaspora_handle)
      expect(entity.profile.author).to eq(new_user.diaspora_handle)
      []
    end

    inlined_jobs { run_migration }
  end

  it "does not change the remote paths" do
    photo = FactoryBot.create(:photo, author: old_person)
    remote_photo_path = photo.remote_photo_path

    run_migration

    expect(photo.reload.remote_photo_path).to eq(remote_photo_path)
  end
end

shared_examples_for "remote photo migration" do
  it "changes the remote paths of photos of the old person" do
    old_photo = FactoryBot.create(:photo, author: old_person)
    new_photo = FactoryBot.create(:photo, author: new_person)
    new_remote_photo_path = old_photo.remote_photo_path

    run_migration

    expect(old_photo.reload.remote_photo_path).to eq("https://diaspora.example.tld/uploads/images/")
    expect(new_photo.reload.remote_photo_path).to eq(new_remote_photo_path)
  end
end

shared_examples_for "migration scenarios with local user rename" do
  it "updates user references" do
    invited_user = FactoryBot.create(:user, invited_by: old_user)
    aspect = FactoryBot.create(:aspect, user: old_user, name: r_str)
    contact = FactoryBot.create(:contact, user: old_user)
    service = FactoryBot.create(:service, user: old_user)
    pref = UserPreference.create!(user: old_user, email_type: "also_commented")
    tag_following = FactoryBot.create(:tag_following, user: old_user)
    block = FactoryBot.create(:block, user: old_user)
    notification = FactoryBot.create(:notification, recipient: old_user)
    report = FactoryBot.create(:report, user: old_user)
    authorization = FactoryBot.create(:auth_with_read_scopes, user: old_user)
    share_visibility = FactoryBot.create(:share_visibility, user: old_user)

    run_migration

    expect(invited_user.reload.invited_by).to eq(new_user)
    expect(aspect.reload.user).to eq(new_user)
    expect(contact.reload.user).to eq(new_user)
    expect(service.reload.user).to eq(new_user)
    expect(pref.reload.user).to eq(new_user)
    expect(tag_following.reload.user).to eq(new_user)
    expect(block.reload.user).to eq(new_user)
    expect(notification.reload.recipient).to eq(new_user)
    expect(report.reload.user).to eq(new_user)
    expect(authorization.reload.user).to eq(new_user)
    expect(share_visibility.reload.user).to eq(new_user)
  end
end

describe "account migration" do
  # this is the case when we receive account migration message from the federation
  context "remotely initiated" do
    let(:entity) { create_account_migration_entity(old_user.diaspora_handle, new_user) }

    def run_migration
      allow_callbacks(%i[queue_public_receive fetch_public_key receive_entity])
      post_message(generate_payload(entity, old_user))
    end

    context "both new and old profiles are remote" do
      let(:old_user) { remote_user_on_pod_c }
      let(:old_person) { old_user.person }
      let(:new_user) { remote_user_on_pod_b }
      let(:new_person) { new_user.person }

      it "creates AccountMigration db object" do
        run_migration
        expect(AccountMigration.where(old_person: old_user.person, new_person: new_user.person)).to exist
      end

      include_examples "every migration scenario"

      include_examples "migration scenarios initiated remotely"

      include_examples "remote photo migration"

      context "when new person has been migrated before" do
        let(:intermidiate_person) { create_remote_user("remote-d.net").person }

        before do
          AccountMigration.create!(old_person: intermidiate_person, new_person: new_person).perform!
        end

        def run_migration
          AccountMigration.create!(old_person: old_person, new_person: intermidiate_person).perform!
        end

        include_examples "every migration scenario"

        include_examples "migration scenarios initiated remotely"
      end
    end

    # this is the case when we're a pod, which was left by a person in favor of remote one
    context "old user is local, new user is remote" do
      let(:old_user) { FactoryBot.create(:user) }
      let(:old_person) { old_user.person }
      let(:new_user) { remote_user_on_pod_b }
      let(:new_person) { new_user.person }

      include_examples "every migration scenario"

      include_examples "migration scenarios initiated remotely"

      include_examples "remote photo migration"

      it_behaves_like "migration scenarios with local old user"

      it_behaves_like "deletes all of the user data" do
        let(:user) { old_user }

        before do
          DataGenerator.create(user, :generic_user_data)
        end

        def account_removal_method
          run_migration
          user.reload
        end
      end

      context "when new person has been migrated before" do
        let(:intermidiate_person) { create_remote_user("remote-d.net").person }

        before do
          AccountMigration.create!(old_person: intermidiate_person, new_person: new_person).perform!
        end

        def run_migration
          AccountMigration.create!(old_person: old_user.person, new_person: intermidiate_person).perform!
        end

        include_examples "every migration scenario"

        include_examples "migration scenarios initiated remotely"

        it_behaves_like "migration scenarios with local old user"
      end
    end
  end

  context "locally initiated" do
    before do
      allow(DiasporaFederation.callbacks).to receive(:trigger).and_call_original
    end

    # this is the case when user migrates to our pod from a remote one
    context "old user is remote and new user is local" do
      let(:old_user) { remote_user_on_pod_c }
      let(:old_person) { old_user.person }
      let(:new_user) { FactoryBot.create(:user) }
      let(:new_person) { new_user.person }

      def run_migration
        AccountMigration.create!(
          old_person:        old_user.person,
          new_person:        new_user.person,
          old_private_key:   old_user.serialized_private_key,
          remote_photo_path: "https://diaspora.example.tld/uploads/images/"
        ).perform!
      end

      include_examples "every migration scenario"

      it_behaves_like "migration scenarios initiated locally" do
        let!(:remote_contact) { create_remote_contact(new_user, "remote-friend.org") }
      end

      context "when new person has been migrated before" do
        let(:intermidiate_person) { FactoryBot.create(:user).person }

        before do
          AccountMigration.create!(old_person: intermidiate_person, new_person: new_person).perform!
        end

        def run_migration
          AccountMigration.create!(
            old_person:      old_person,
            new_person:      intermidiate_person,
            old_private_key: old_user.serialized_private_key
          ).perform!
        end

        include_examples "every migration scenario"
      end
    end

    # this is the case when a user changes diaspora id but stays on the same pod
    context "old user is local and new user is local" do
      let(:old_user) { FactoryBot.create(:user) }
      let(:old_person) { old_user.person }
      let(:new_user) { FactoryBot.create(:user) }
      let(:new_person) { new_user.person }

      def run_migration
        AccountMigration.create!(old_person:        old_person,
                                 new_person:        new_person,
                                 remote_photo_path: "https://diaspora.example.tld/uploads/images/").perform!
      end

      include_examples "every migration scenario"

      it_behaves_like "migration scenarios initiated locally" do
        let!(:remote_contact) { create_remote_contact(old_user, "remote-friend.org") }
      end

      it_behaves_like "migration scenarios with local old user"

      it "clears the old user account" do
        run_migration
        expect(old_user.reload).to be_a_clear_account
      end

      include_examples "migration scenarios with local user rename"

      context "when new user has been migrated before" do
        let(:intermidiate_person) { FactoryBot.create(:user).person }

        before do
          AccountMigration.create!(old_person: intermidiate_person, new_person: new_person).perform!
        end

        def run_migration
          AccountMigration.create!(
            old_person: old_person,
            new_person: intermidiate_person
          ).perform!
        end

        include_examples "every migration scenario"

        it_behaves_like "migration scenarios with local old user"

        it "clears the old user account" do
          run_migration
          expect(old_user.reload).to be_a_clear_account
        end

        include_examples "migration scenarios with local user rename"
      end
    end
  end
end
