diaspora/spec/integration/migration_service_spec.rb
cmrd Senya f85f167f50 Implement archive import backend
This implements archive import feature.

The feature is divided in two main subfeatures: archive validation and archive import.

Archive validation performs different validation on input user archive. This can be
used without actually running import, e.g. when user wants to check the archive
before import from the frontend. Validators may add messages and modify the archive.

Validators are separated in two types: critical validators and non-critical validators.

If validations by critical validators fail it means we can't import archive.

If non-critical validations fail, we can import archive, but some warning messages
are rendered.

Also validators may change archive contents, e.g. when some entity can't be
imported it may be removed from the archive.

Validators' job is to take away complexity from the importer and perform the validations
which are not implemented in other parts of the system, e.g. DB validations or
diaspora_federation entity validations.

Archive importer then takes the modified archive from the validator and imports it.

In order to incapsulate high-level migration logic a MigrationService is
introduced. MigrationService links ArchiveValidator, ArchiveImporter and
AccountMigration.

Also here is introduced a rake task which may be used by podmins to run archive
import.
2019-04-26 18:41:27 +03:00

395 lines
16 KiB
Ruby

# frozen_string_literal: true
require "integration/federation/federation_helper"
require "integration/archive_shared"
describe MigrationService do
let(:old_pod_hostname) { "originalhomepod.tld" }
let(:archive_author) { "previous_username@#{old_pod_hostname}" }
let(:archive_private_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:contact1_diaspora_id) { known_contact_person.diaspora_handle }
let(:contact2_diaspora_id) { Fabricate.sequence(:diaspora_id) }
let(:unknown_subscription_guid) { UUID.generate(:compact) }
let(:existing_subscription_guid) { UUID.generate(:compact) }
let(:reshare_entity) { Fabricate(:reshare_entity, author: archive_author) }
let(:reshare_entity_with_no_root) {
Fabricate(:reshare_entity, author: archive_author, root_guid: nil, root_author: nil)
}
let(:unknown_status_message_entity) { Fabricate(:status_message_entity, author: archive_author, public: false) }
let(:known_status_message_entity) { Fabricate(:status_message_entity, author: archive_author, public: false) }
let(:colliding_status_message_entity) { Fabricate(:status_message_entity, author: archive_author) }
let(:status_message_with_poll_entity) {
Fabricate(:status_message_entity,
author: archive_author,
poll: Fabricate(:poll_entity))
}
let(:status_message_with_location_entity) {
Fabricate(:status_message_entity,
author: archive_author,
location: Fabricate(:location_entity))
}
let(:status_message_with_photos_entity) {
Fabricate(:status_message_entity,
author: archive_author,
photos: [
Fabricate(:photo_entity, author: archive_author),
Fabricate(:photo_entity, author: archive_author)
])
}
let(:comment_entity) {
Fabricate(:comment_entity, author: archive_author, author_signature: "ignored XXXXXXXXXXXXXXXXXXXXXXXXXXX")
}
let(:like_entity) {
Fabricate(:like_entity,
author: archive_author,
author_signature: "ignored XXXXXXXXXXXXXXXXXXXXXXXXXXX",
parent_guid: FactoryGirl.create(:status_message).guid)
}
let(:poll_participation_entity) {
poll = FactoryGirl.create(:status_message_with_poll).poll
Fabricate(:poll_participation_entity,
author: archive_author,
author_signature: "ignored XXXXXXXXXXXXXXXXXXXXXXXXXXX",
poll_answer_guid: poll.poll_answers.first.guid,
parent_guid: poll.guid)
}
let(:unknown_poll_guid) { UUID.generate(:compact) }
let(:unknown_poll_answer_guid) { UUID.generate(:compact) }
let(:poll_participation_entity_unknown_root) {
Fabricate(:poll_participation_entity,
author: archive_author,
author_signature: "ignored XXXXXXXXXXXXXXXXXXXXXXXXXXX",
poll_answer_guid: unknown_poll_answer_guid,
parent_guid: unknown_poll_guid)
}
let(:others_comment_entity) {
data = Fabricate.attributes_for(:comment_entity,
author: remote_user_on_pod_b.diaspora_handle,
parent_guid: unknown_status_message_entity.guid)
data[:author_signature] = Fabricate(:comment_entity, data).sign_with_key(remote_user_on_pod_b.encryption_key)
Fabricate(:comment_entity, data)
}
let(:post_subscriber) { FactoryGirl.create(:person) }
let(:known_contact_person) { FactoryGirl.create(:person) }
let!(:collided_status_message) { FactoryGirl.create(:status_message, guid: colliding_status_message_entity.guid) }
let!(:collided_like) { FactoryGirl.create(:like, guid: like_entity.guid) }
let!(:reshare_root_author) { FactoryGirl.create(:person, diaspora_handle: reshare_entity.root_author) }
# This is for testing migrated contacts handling
let(:account_migration) { FactoryGirl.create(:account_migration).tap(&:perform!) }
let(:migrated_contact_diaspora_id) { account_migration.old_person.diaspora_handle }
let(:migrated_contact_new_diaspora_id) { account_migration.new_person.diaspora_handle }
let(:posts_in_archive) {
[
reshare_entity,
unknown_status_message_entity,
known_status_message_entity,
reshare_entity_with_no_root,
colliding_status_message_entity,
status_message_with_poll_entity,
status_message_with_location_entity,
status_message_with_photos_entity
]
}
let(:posts_in_archive_json) {
posts = posts_in_archive.map {|post|
post.to_json.as_json
}
posts[0]["subscribed_pods_uris"] = []
posts[1]["subscribed_users_ids"] = [post_subscriber.diaspora_handle]
posts[2]["subscribed_users_ids"] = [post_subscriber.diaspora_handle]
posts[3]["subscribed_pods_uris"] = []
posts[4]["subscribed_pods_uris"] = []
posts[5]["subscribed_pods_uris"] = []
posts[6]["subscribed_pods_uris"] = []
posts[7]["subscribed_pods_uris"] = []
posts.to_json
}
let(:archive_json) { <<~JSON }
{
"user": {
"username": "previous_username",
"email": "mail@example.com",
"private_key": #{archive_private_key.export.dump},
"profile": {
"entity_type": "profile",
"entity_data": {
"author": "#{archive_author}"
}
},
"contacts": [
{
"sharing": true,
"receiving": false,
"following": true,
"followed": false,
"account_id": "#{contact1_diaspora_id}",
"contact_groups_membership": ["Family"]
},
{
"sharing": true,
"receiving": true,
"following": true,
"followed": true,
"account_id": "#{migrated_contact_diaspora_id}",
"contact_groups_membership": ["Family"]
},
{
"sharing": true,
"receiving": true,
"following": true,
"followed": true,
"account_id": "#{contact2_diaspora_id}",
"contact_groups_membership": ["Family"]
}
],
"contact_groups": [
{"name":"Friends","chat_enabled":true},
{"name":"Friends","chat_enabled":false}
],
"post_subscriptions": [
"#{unknown_subscription_guid}",
"#{existing_subscription_guid}"
],
"posts": #{posts_in_archive_json},
"relayables": [
#{comment_entity.to_json.as_json.to_json},
#{like_entity.to_json.as_json.to_json},
#{poll_participation_entity.to_json.as_json.to_json},
#{poll_participation_entity_unknown_root.to_json.as_json.to_json}
]
},
"others_data": {
"relayables": [
#{others_comment_entity.to_json.as_json.to_json}
]
},
"version": "2.0"
}
JSON
def expect_reshare_root_fetch(root_author, root_guid)
expect(DiasporaFederation::Federation::Fetcher)
.to receive(:fetch_public)
.with(root_author.diaspora_handle, "Post", root_guid) {
FactoryGirl.create(:status_message, guid: root_guid, author: root_author, public: true)
}
end
def expect_relayable_parent_fetch(relayable_author, parent_guid, parent_type="Post", &block)
expect(DiasporaFederation::Federation::Fetcher)
.to receive(:fetch_public)
.with(relayable_author, parent_type, parent_guid, &block)
end
let(:new_username) { "newuser" }
let(:new_user_handle) { "#{new_username}@#{AppConfig.bare_pod_uri}" }
let(:archive_file) { Tempfile.new("archive") }
def setup_validation_time_expectations
expect_person_fetch(contact2_diaspora_id, nil)
# This is expected to be called during relayable validation
expect_relayable_parent_fetch(archive_author, comment_entity.parent_guid) {
FactoryGirl.create(:status_message, guid: comment_entity.parent_guid)
}
expect_relayable_parent_fetch(archive_author, unknown_poll_guid, "Poll") {
FactoryGirl.create(
:poll_answer,
poll: FactoryGirl.create(:poll, guid: unknown_poll_guid),
guid: unknown_poll_answer_guid
)
}
end
before do
archive_file.write(archive_json)
archive_file.close
allow_callbacks(
%i[queue_public_receive fetch_related_entity fetch_person_url_to fetch_public_key receive_entity
fetch_private_key]
)
end
shared_examples "imports archive" do
it "imports archive" do
expect_relayable_parent_fetch(archive_author, unknown_subscription_guid) {
FactoryGirl.create(:status_message, guid: unknown_subscription_guid)
}
expect_reshare_root_fetch(reshare_root_author, reshare_entity.root_guid)
service = MigrationService.new(archive_file.path, new_username)
service.validate
expect(service.warnings).to eq(
["reshare Reshare:#{reshare_entity_with_no_root.guid} doesn't have a root, ignored"]
)
service.perform!
user = User.find_by(username: new_username)
expect(user).not_to be_nil
unless Person.by_account_identifier(archive_author).nil?
expect(AccountMigration.where(new_person: user.person).any?).to be_truthy
existing_contact.reload
expect(existing_contact.person).to eq(user.person)
expect(existing_contact.sharing).to be_truthy
expect(existing_contact.receiving).to be_truthy
end
status_message = StatusMessage.find_by(guid: unknown_status_message_entity.guid)
expect(status_message.author).to eq(user.person)
# TODO: rewrite this expectation when new subscription implementation is there
# expect(status_message.participants).to include(post_subscriber)
status_message = StatusMessage.find_by(guid: known_status_message_entity.guid)
expect(status_message.author).to eq(user.person)
# TODO: rewrite this expectation when new subscription implementation is there
# expect(status_message.participants).to include(post_subscriber)
status_message = StatusMessage.find_by(guid: status_message_with_poll_entity.guid)
expect(status_message.author).to eq(user.person)
poll = status_message.poll
expect(poll).not_to be_nil
expect(poll.guid).to eq(status_message_with_poll_entity.poll.guid)
expect(poll.question).to eq(status_message_with_poll_entity.poll.question)
expect(poll.poll_answers.pluck(:answer, :guid)).to eq(
status_message_with_poll_entity.poll.poll_answers.map {|answer| [answer.answer, answer.guid] }
)
status_message = StatusMessage.find_by(guid: status_message_with_location_entity.guid)
expect(status_message.author).to eq(user.person)
expect(status_message.location.address).to eq(status_message_with_location_entity.location.address)
expect(status_message.location.lat).to eq(status_message_with_location_entity.location.lat)
expect(status_message.location.lng).to eq(status_message_with_location_entity.location.lng)
status_message = StatusMessage.find_by(guid: status_message_with_photos_entity.guid)
expect(status_message.author).to eq(user.person)
expect(
status_message.photos.pluck(:guid, :text, :remote_photo_path, :remote_photo_name, :width, :height)
).to match_array(
status_message_with_photos_entity.photos.map {|photo|
[photo.guid, photo.text, photo.remote_photo_path, photo.remote_photo_name, photo.width, photo.height]
}
)
comment = Comment.find_by(guid: comment_entity.guid)
expect(comment.author).to eq(user.person)
# Here we're testing the case when the like in the archive has the guid colliding with another known like
like = Like.find_by(guid: like_entity.guid)
expect(like.author).not_to eq(user.person)
contact = user.contacts.find_by(person: Person.by_account_identifier(contact1_diaspora_id))
expect(contact).not_to be_nil
expect(contact.sharing).to be_falsey
expect(contact.receiving).to be_falsey
contact = user.contacts.find_by(person: Person.by_account_identifier(contact2_diaspora_id))
expect(contact).not_to be_nil
expect(contact.sharing).to be_falsey
expect(contact.receiving).to be_truthy
contact = user.contacts.find_by(person: Person.by_account_identifier(migrated_contact_new_diaspora_id))
expect(contact).not_to be_nil
expect(contact.sharing).to be_falsey
expect(contact.receiving).to be_truthy
aspect = user.aspects.find_by(name: "Friends")
expect(aspect).not_to be_nil
expect(aspect.chat_enabled).to be_truthy
poll_participation = PollParticipation.find_by(author: user.person, guid: poll_participation_entity.guid)
expect(poll_participation).not_to be_nil
expect(poll_participation.parent.guid).to eq(poll_participation_entity.parent_guid)
expect(poll_participation.poll_answer.guid).to eq(poll_participation_entity.poll_answer_guid)
comment = Comment.find_by(guid: others_comment_entity.guid)
expect(comment.author.diaspora_handle).to eq(others_comment_entity.author)
expect(comment.parent.author.diaspora_handle).to eq(user.diaspora_handle)
end
end
context "old user is a known remote user" do
let(:old_person) {
FactoryGirl.create(:person,
profile: FactoryGirl.build(:profile),
serialized_public_key: archive_private_key.public_key.export,
diaspora_handle: archive_author)
}
# Some existing data for old_person to test data merge/migration
let!(:existing_contact) { FactoryGirl.create(:contact, person: old_person, sharing: true, receiving: true) }
let!(:existing_subscription) {
FactoryGirl.create(:participation,
author: old_person,
target: FactoryGirl.create(:status_message, guid: existing_subscription_guid))
}
let!(:existing_status_message) {
FactoryGirl.create(:status_message,
author: old_person,
guid: known_status_message_entity.guid).tap {|status_message|
status_message.participants << post_subscriber
}
}
it_behaves_like "imports archive" do
before do
setup_validation_time_expectations
end
end
context "when account migration already exists" do
before do
setup_validation_time_expectations
FactoryGirl.create(:account_migration, old_person: old_person)
end
it "raises exception" do
expect {
MigrationService.new(archive_file.path, new_username).validate
}.to raise_error(MigrationService::MigrationAlreadyExists)
end
end
describe "#only_import?" do
it "returns false" do
service = MigrationService.new(archive_file.path, new_username)
expect(service.only_import?).to be_falsey
end
end
end
context "old user is unknown" do
context "and non-fetchable" do
before do
expect(DiasporaFederation::Discovery::Discovery).to receive(:new).with(archive_author).and_call_original
stub_request(:get, "https://#{old_pod_hostname}/.well-known/webfinger?resource=acct:#{archive_author}")
.to_return(status: 404)
stub_request(:get, %r{https*://#{old_pod_hostname}/\.well-known/host-meta})
.to_return(status: 404)
expect_relayable_parent_fetch(archive_author, existing_subscription_guid)
.and_raise(DiasporaFederation::Federation::Fetcher::NotFetchable)
setup_validation_time_expectations
end
include_examples "imports archive"
end
describe "#only_import?" do
it "returns true" do
service = MigrationService.new(archive_file.path, new_username)
expect(service.only_import?).to be_truthy
end
end
end
end