diaspora/spec/integration/migration_service_spec.rb
2021-09-19 02:14:50 +02:00

436 lines
17 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: FactoryBot.create(:status_message).guid)
}
let(:poll_participation_entity) {
poll = FactoryBot.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) { FactoryBot.create(:person) }
let(:known_contact_person) { FactoryBot.create(:person) }
let!(:collided_status_message) { FactoryBot.create(:status_message, guid: colliding_status_message_entity.guid) }
let!(:collided_like) { FactoryBot.create(:like, guid: like_entity.guid) }
let!(:reshare_root_author) { FactoryBot.create(:person, diaspora_handle: reshare_entity.root_author) }
# This is for testing migrated contacts handling
let(:account_migration) { FactoryBot.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"}
],
"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) {
FactoryBot.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(%w[archive .json]) }
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) {
FactoryBot.create(:status_message, guid: comment_entity.parent_guid)
}
expect_relayable_parent_fetch(archive_author, unknown_poll_guid, "Poll") {
FactoryBot.create(
:poll_answer,
poll: FactoryBot.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) {
FactoryBot.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_truthy
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_truthy
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_truthy
expect(contact.receiving).to be_truthy
aspect = user.aspects.find_by(name: "Friends")
expect(aspect).not_to be_nil
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 "compressed archives" do
it "uncompresses gz archive" do
gz_compressed_file = create_gz_archive
service = MigrationService.new(gz_compressed_file, new_username)
uncompressed_file = service.send(:archive_file)
json = uncompressed_file.read
expect {
JSON.parse(json)
}.not_to raise_error
end
it "uncompresses zip archive" do
zip_compressed_file = create_zip_archive
service = MigrationService.new(zip_compressed_file, new_username)
uncompressed_file = service.send(:archive_file)
json = uncompressed_file.read
expect {
JSON.parse(json)
}.not_to raise_error
end
def create_gz_archive
target_file = Tempfile.new(%w[archive .json.gz]).path
Zlib::GzipWriter.open(target_file) do |gz|
File.open(archive_file.path).each do |line|
gz.write line
end
end
target_file
end
def create_zip_archive
target_file = Tempfile.new(%w[archive .zip]).path
Zip::OutputStream.open(target_file) do |zip|
zip.put_next_entry("archive.json")
File.open(archive_file.path).each do |line|
zip.write line
end
end
target_file
end
end
context "old user is a known remote user" do
let(:old_person) {
FactoryBot.create(:person,
profile: FactoryBot.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) { FactoryBot.create(:contact, person: old_person, sharing: true, receiving: true) }
let!(:existing_subscription) {
FactoryBot.create(:participation,
author: old_person,
target: FactoryBot.create(:status_message, guid: existing_subscription_guid))
}
let!(:existing_status_message) {
FactoryBot.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
FactoryBot.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