Account migration model and message support

This commit introduces support for AccountMigration federation message
receive. It covers the cases when the new home pod for a user is remote
respective to the recepient pod of the message. It also allows to initiate
migration locally by a podmin from the rails console. This will give the
pods a possibility to understand the account migration event on the
federation level and thus future version which will implement migration
will be backward compatible with the pods starting from this commit.
This commit is contained in:
cmrd Senya 2017-08-13 22:05:25 +03:00
parent e2979df65a
commit 45619cb153
No known key found for this signature in database
GPG key ID: 5FCC5BA680E67BFE
22 changed files with 1051 additions and 69 deletions

View file

@ -0,0 +1,165 @@
class AccountMigration < ApplicationRecord
include Diaspora::Federated::Base
belongs_to :old_person, class_name: "Person"
belongs_to :new_person, class_name: "Person"
validates :old_person, uniqueness: true
validates :new_person, uniqueness: true
after_create :lock_old_user!
attr_accessor :old_private_key
def receive(*)
perform!
end
def public?
true
end
def sender
@sender ||= old_user || ephemeral_sender
end
# executes a migration plan according to this AccountMigration object
def perform!
raise "already performed" if performed?
ActiveRecord::Base.transaction do
account_deleter.tombstone_person_and_profile
account_deleter.close_user if user_left_our_pod?
account_deleter.tombstone_user if user_changed_id_locally?
update_all_references
end
dispatch if locally_initiated?
dispatch_contacts if remotely_initiated?
end
def performed?
old_person.closed_account?
end
# We assume that migration message subscribers are people that are subscribed to a new user profile updates.
# Since during the migration we update contact references, this includes all the contacts of the old person.
# In case when a user migrated to our pod from a remote one, we include remote person to subscribers so that
# the new pod is informed about the migration as well.
def subscribers
new_user.profile.subscribers.remote.to_a.tap do |subscribers|
subscribers.push(old_person) if old_person.remote?
end
end
private
# Normally pod initiates migration locally when the new user is local. Then the pod creates AccountMigration object
# itself. If new user is remote, then AccountMigration object is normally received via the federation and this is
# remote initiation then.
def remotely_initiated?
new_person.remote?
end
def locally_initiated?
!remotely_initiated?
end
def old_user
old_person.owner
end
def new_user
new_person.owner
end
def lock_old_user!
old_user&.lock_access!
end
def user_left_our_pod?
old_user && !new_user
end
def user_changed_id_locally?
old_user && new_user
end
# We need to resend contacts of users of our pod for the remote new person so that the remote pod received this
# contact information from the authoritative source.
def dispatch_contacts
new_person.contacts.sharing.each do |contact|
Diaspora::Federation::Dispatcher.defer_dispatch(contact.user, contact)
end
end
def dispatch
Diaspora::Federation::Dispatcher.build(sender, self).dispatch
end
EphemeralUser = Struct.new(:diaspora_handle, :serialized_private_key) do
def id
diaspora_handle
end
def encryption_key
OpenSSL::PKey::RSA.new(serialized_private_key)
end
end
def ephemeral_sender
raise "can't build sender without old private key defined" if old_private_key.nil?
EphemeralUser.new(old_person.diaspora_handle, old_private_key)
end
def update_all_references
update_person_references
update_user_references if user_changed_id_locally?
end
def person_references
references = Person.reflections.reject {|key, _|
%w[profile owner notifications pod].include?(key)
}
references.map {|key, value|
{value.foreign_key => key}
}
end
def user_references
references = User.reflections.reject {|key, _|
%w[
person profile auto_follow_back_aspect invited_by aspect_memberships contact_people followed_tags
ignored_people conversation_visibilities pairwise_pseudonymous_identifiers conversations o_auth_applications
].include?(key)
}
references.map {|key, value|
{value.foreign_key => key}
}
end
def update_person_references
logger.debug "Updating references from person id=#{old_person.id} to person id=#{new_person.id}"
update_references(person_references, old_person, new_person.id)
end
def update_user_references
logger.debug "Updating references from user id=#{old_user.id} to user id=#{new_user.id}"
update_references(user_references, old_user, new_user.id)
end
def update_references(references, object, new_id)
references.each do |pair|
key_id = pair.flatten[0]
association = pair.flatten[1]
object.send(association).update_all(key_id => new_id)
end
end
def account_deleter
@account_deleter ||= AccountDeleter.new(old_person)
end
end

View file

@ -40,7 +40,10 @@ class Person < ApplicationRecord
has_many :likes, foreign_key: :author_id, dependent: :destroy # This person's own likes
has_many :participations, :foreign_key => :author_id, :dependent => :destroy
has_many :poll_participations, foreign_key: :author_id, dependent: :destroy
has_many :conversation_visibilities
has_many :conversation_visibilities, dependent: :destroy
has_many :messages, foreign_key: :author_id, dependent: :destroy
has_many :conversations, foreign_key: :author_id, dependent: :destroy
has_many :blocks, dependent: :destroy
has_many :roles
@ -307,11 +310,6 @@ class Person < ApplicationRecord
serialized_public_key
end
def exported_key= new_key
raise "Don't change a key" if serialized_public_key
serialized_public_key = new_key
end
# discovery (webfinger)
def self.find_or_fetch_by_identifier(diaspora_id)
# exiting person?

View file

@ -126,6 +126,7 @@ class Profile < ApplicationRecord
end
def tombstone!
@tag_string = nil
self.taggings.delete_all
clearable_fields.each do |field|
self[field] = nil

View file

@ -54,6 +54,8 @@ class User < ApplicationRecord
belongs_to :auto_follow_back_aspect, class_name: "Aspect", optional: true
belongs_to :invited_by, class_name: "User", optional: true
has_many :invited_users, class_name: "User", inverse_of: :invited_by, foreign_key: :invited_by_id
has_many :aspect_memberships, :through => :aspects
has_many :contacts

View file

@ -0,0 +1,14 @@
class CreateAccountMigrations < ActiveRecord::Migration[5.1]
def change
create_table :account_migrations do |t|
t.integer :old_person_id, null: false
t.integer :new_person_id, null: false
end
add_foreign_key :account_migrations, :people, column: :old_person_id
add_foreign_key :account_migrations, :people, column: :new_person_id
add_index :account_migrations, %i[old_person_id new_person_id], unique: true
add_index :account_migrations, :old_person_id, unique: true
end
end

View file

@ -30,18 +30,20 @@ class AccountDeleter
delete_contacts_of_me
tombstone_person_and_profile
if self.user
#user deletion methods
remove_share_visibilities_on_contacts_posts
delete_standard_user_associations
disconnect_contacts
tombstone_user
end
close_user if user
mark_account_deletion_complete
end
end
# user deletion methods
def close_user
remove_share_visibilities_on_contacts_posts
disconnect_contacts
delete_standard_user_associations
tombstone_user
end
#user deletions
def normal_ar_user_associates_to_delete
%i[tag_followings services aspects user_preferences
@ -53,7 +55,7 @@ class AccountDeleter
end
def ignored_ar_user_associations
%i[followed_tags invited_by contact_people aspect_memberships
%i[followed_tags invited_by invited_users contact_people aspect_memberships
ignored_people share_visibilities conversation_visibilities conversations reports]
end
@ -70,7 +72,7 @@ class AccountDeleter
end
def disconnect_contacts
user.contacts.reload.destroy_all
user.contacts.destroy_all
end
# Currently this would get deleted due to the db foreign key constrainsts,
@ -97,12 +99,12 @@ class AccountDeleter
end
def normal_ar_person_associates_to_delete
%i[posts photos mentions participations roles]
%i[posts photos mentions participations roles blocks]
end
def ignored_or_special_ar_person_associations
%i[comments likes poll_participations contacts notification_actors notifications owner profile
conversation_visibilities pod]
conversation_visibilities pod conversations messages]
end
def mark_account_deletion_complete

View file

@ -22,6 +22,13 @@ module Diaspora
)
end
def self.account_migration(account_migration)
DiasporaFederation::Entities::AccountMigration.new(
author: account_migration.sender.diaspora_handle,
profile: profile(account_migration.new_person.profile)
)
end
def self.comment(comment)
DiasporaFederation::Entities::Comment.new(
{

View file

@ -6,6 +6,7 @@ module Diaspora
# used in Diaspora::Federation::Receive
def self.receiver_for(federation_entity)
case federation_entity
when DiasporaFederation::Entities::AccountMigration then :account_migration
when DiasporaFederation::Entities::Comment then :comment
when DiasporaFederation::Entities::Contact then :contact
when DiasporaFederation::Entities::Conversation then :conversation
@ -24,6 +25,7 @@ module Diaspora
# used in Diaspora::Federation::Entities
def self.builder_for(diaspora_entity)
case diaspora_entity
when AccountMigration then :account_migration
when AccountDeletion then :account_deletion
when Comment then :comment
when Contact then :contact

View file

@ -11,6 +11,14 @@ module Diaspora
AccountDeletion.create!(person: author_of(entity))
end
def self.account_migration(entity)
profile = profile(entity.profile)
AccountMigration.create!(
old_person: Person.by_account_identifier(entity.author),
new_person: profile.person
)
end
def self.comment(entity)
receive_relayable(Comment, entity) do
Comment.new(

View file

@ -53,6 +53,11 @@ FactoryGirl.define do
association :person
end
factory :account_migration do
association :old_person, factory: :person
association :new_person, factory: :person
end
factory :like do
association :author, :factory => :person
association :target, :factory => :status_message
@ -145,6 +150,11 @@ FactoryGirl.define do
end
end
factory(:share_visibility) do
user
association :shareable, factory: :status_message
end
factory(:location) do
sequence(:address) {|n| "Fernsehturm Berlin, #{n}, Berlin, Germany" }
sequence(:lat) {|n| 52.520645 + 0.0000001 * n }
@ -222,13 +232,8 @@ FactoryGirl.define do
sequence(:uid) { |token| "00000#{token}" }
sequence(:access_token) { |token| "12345#{token}" }
sequence(:access_secret) { |token| "98765#{token}" }
end
factory :service_user do
sequence(:uid) { |id| "a#{id}"}
sequence(:name) { |num| "Rob Fergus the #{num.ordinalize}" }
association :service
photo_url "/assets/user/adams.jpg"
user
end
factory :pod do
@ -354,7 +359,18 @@ FactoryGirl.define do
text SecureRandom.hex(1000)
end
factory(:status, :parent => :status_message)
factory(:status, parent: :status_message)
factory :block do
user
person
end
factory :report do
user
association :item, factory: :status_message
text "offensive content"
end
factory :o_auth_application, class: Api::OpenidConnect::OAuthApplication do
client_name { "Diaspora Test Client #{r_str}" }

View file

@ -12,25 +12,11 @@ describe "deleteing account", type: :request do
DataGenerator.create(subject, :generic_user_data)
end
it "deletes all of the user data" do
expect {
account_removal_method
}.to change(nil, "user preferences empty?") { UserPreference.where(user_id: user.id).empty? }.to(be_truthy)
.and(change(nil, "notifications empty?") { Notification.where(recipient_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "blocks empty?") { Block.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "services empty?") { Service.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "share visibilities empty?") { ShareVisibility.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "aspects empty?") { user.aspects.empty? }.to(be_truthy))
.and(change(nil, "contacts empty?") { user.contacts.empty? }.to(be_truthy))
.and(change(nil, "tag followings empty?") { user.tag_followings.empty? }.to(be_truthy))
.and(change(nil, "clearable fields blank?") {
user.send(:clearable_fields).map {|field|
user.reload[field].blank?
}
}.to(eq([true] * user.send(:clearable_fields).count)))
end
it_behaves_like "deletes all of the user data"
it_behaves_like "it removes the person associations"
it_behaves_like "it keeps the person conversations"
end
context "of remote person" do
@ -41,5 +27,13 @@ describe "deleteing account", type: :request do
end
it_behaves_like "it removes the person associations"
it_behaves_like "it keeps the person conversations"
it_behaves_like "it makes account closed and clears profile" do
before do
account_removal_method
end
end
end
end

View file

@ -0,0 +1,204 @@
require "integration/federation/federation_helper"
def create_remote_contact(user, pod_host)
FactoryGirl.create(
:contact,
user: user,
person: FactoryGirl.create(
:person,
pod: Pod.find_or_create_by(url: "http://#{pod_host}"),
diaspora_handle: "#{r_str}@#{pod_host}"
)
)
end
shared_examples_for "old person account is closed and profile is cleared" do
subject { old_user.person }
before do
run_migration
subject.reload
end
include_examples "it makes account closed and clears profile"
end
shared_examples_for "old person doesn't have any reference left" do
let(:person) { old_user.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"
include_examples "it removes the person conversations"
end
shared_examples_for "every migration scenario" do
it_behaves_like "it updates person references"
it_behaves_like "old person account is closed and profile is cleared"
it_behaves_like "old person doesn't have any reference left"
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
contacts = Array.new(2) { FactoryGirl.create(:contact, person: old_user.person, sharing: true) }
expect(DiasporaFederation::Federation::Sender).to receive(:private)
.twice do |sender_id, obj_str, _urls, _xml|
expect(sender_id).to eq(contacts.first.user_id)
expect(obj_str).to eq("Contact:#{contacts.first.user.diaspora_handle}:#{new_user.diaspora_handle}")
contacts.shift
[]
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 do
run_migration
end
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
include_context "with remote old user"
include_context "with remote new user"
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"
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
include_context "with local old user"
include_context "with remote new user"
include_examples "every migration scenario"
include_examples "migration scenarios initiated remotely"
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
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
include_context "with remote old user"
include_context "with local new user"
def run_migration
AccountMigration.create!(
old_person: old_user.person,
new_person: new_user.person,
old_private_key: old_user.serialized_private_key
).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
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
include_context "with local old user"
include_context "with local new user"
def run_migration
AccountMigration.create!(old_person: old_user.person, new_person: new_user.person).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
it_behaves_like "it updates user references"
end
end
end

View file

@ -6,21 +6,43 @@ def remote_user_on_pod_c
@remote_on_c ||= create_remote_user("remote-c.net")
end
def create_remote_user(pod)
def allow_private_key_fetch(user)
allow(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_private_key, user.diaspora_handle
) { user.encryption_key }
end
def allow_public_key_fetch(user)
allow(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_public_key, user.diaspora_handle
) { OpenSSL::PKey::RSA.new(user.person.serialized_public_key) }
end
def create_undiscovered_user(pod)
FactoryGirl.build(:user).tap do |user|
allow(user).to receive(:person).and_return(
FactoryGirl.create(:person,
profile: FactoryGirl.build(:profile),
serialized_public_key: user.encryption_key.public_key.export,
pod: Pod.find_or_create_by(url: "http://#{pod}"),
diaspora_handle: "#{user.username}@#{pod}")
FactoryGirl.build(:person,
profile: FactoryGirl.build(:profile),
serialized_public_key: user.encryption_key.public_key.export,
pod: Pod.find_or_create_by(url: "http://#{pod}"),
diaspora_handle: "#{user.username}@#{pod}")
)
allow(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_private_key, user.diaspora_handle
) { user.encryption_key }
allow(DiasporaFederation.callbacks).to receive(:trigger).with(
:fetch_public_key, user.diaspora_handle
) { OpenSSL::PKey::RSA.new(user.person.serialized_public_key) }
end
end
def expect_person_discovery(undiscovered_user)
allow(Person).to receive(:find_or_fetch_by_identifier).with(any_args).and_call_original
expect(Person).to receive(:find_or_fetch_by_identifier).with(undiscovered_user.diaspora_handle) {
undiscovered_user.person.save!
undiscovered_user.person
}
end
def create_remote_user(pod)
create_undiscovered_user(pod).tap do |user|
user.person.save!
allow_private_key_fetch(user)
allow_public_key_fetch(user)
end
end
@ -44,6 +66,14 @@ def create_relayable_entity(entity_name, parent, diaspora_id)
)
end
def create_account_migration_entity(diaspora_id, new_user)
Fabricate(
:account_migration_entity,
author: diaspora_id,
profile: Diaspora::Federation::Entities.build(new_user.profile)
)
end
def generate_payload(entity, remote_user, recipient=nil)
magic_env = DiasporaFederation::Salmon::MagicEnvelope.new(
entity,

View file

@ -9,7 +9,7 @@ describe "Receive federation messages feature" do
end
let(:sender) { remote_user_on_pod_b }
let(:sender_id) { remote_user_on_pod_b.diaspora_handle }
let(:sender_id) { sender.diaspora_handle }
context "with public receive" do
let(:recipient) { nil }
@ -29,6 +29,80 @@ describe "Receive federation messages feature" do
end
end
context "account migration" do
# In case when sender is unknown we should just ignore the migration
# but this depends on https://github.com/diaspora/diaspora_federation/issues/72
# which is low-priority, so we just discover the sender profile in this case.
# But there won't be a spec for that.
let(:entity) { create_account_migration_entity(sender_id, new_user) }
def run_migration
post_message(generate_payload(entity, sender))
end
context "with undiscovered new user profile" do
before do
allow_callbacks(%i[fetch_public_key])
allow_private_key_fetch(new_user)
expect_person_discovery(new_user)
end
let(:new_user) { create_undiscovered_user("example.org") }
it "receives account migration correctly" do
run_migration
expect(AccountMigration.where(old_person: sender.person, new_person: new_user.person)).to exist
expect(AccountMigration.find_by(old_person: sender.person, new_person: new_user.person)).to be_performed
end
it "doesn't accept the same migration for the second time" do
run_migration
expect {
run_migration
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "doesn't accept second migration for the same sender" do
run_migration
expect {
entity = create_account_migration_entity(sender_id, create_remote_user("example.org"))
post_message(generate_payload(entity, sender))
}.to raise_error(ActiveRecord::RecordInvalid)
end
it "doesn't accept second migration for the same new user profile" do
run_migration
expect {
sender = create_remote_user("example.org")
entity = create_account_migration_entity(sender.diaspora_handle, new_user)
post_message(generate_payload(entity, sender))
}.to raise_error(ActiveRecord::RecordInvalid)
end
context "when our pod was left" do
let(:sender) { FactoryGirl.create(:user) }
it "locks the old user account access" do
run_migration
expect(sender.reload.access_locked?).to be_truthy
end
end
end
context "with discovered profile" do
let(:new_user) { create_remote_user("example.org") }
it "updates person profile with data from entity" do
new_user.profile.bio = "my updated biography"
expect(entity.profile.bio).to eq("my updated biography")
expect(new_user.profile.reload.bio).not_to eq("my updated biography")
run_migration
expect(new_user.profile.reload.bio).to eq("my updated biography")
end
end
end
context "reshare" do
it "reshare of public post passes" do
post = FactoryGirl.create(:status_message, author: alice.person, public: true)

View file

@ -14,12 +14,6 @@ describe AccountDeleter do
end
describe '#perform' do
user_removal_methods = %i[
delete_standard_user_associations
remove_share_visibilities_on_contacts_posts
disconnect_contacts tombstone_user
]
person_removal_methods = %i[
delete_contacts_of_me
delete_standard_person_associations
@ -32,7 +26,7 @@ describe AccountDeleter do
@account_deletion.perform!
end
(user_removal_methods + person_removal_methods).each do |method|
[*person_removal_methods, :close_user].each do |method|
it "calls ##{method.to_s}" do
expect(@account_deletion).to receive(method)
@ -64,11 +58,8 @@ describe AccountDeleter do
@person_deletion.perform!
end
(user_removal_methods).each do |method|
it "does not call ##{method.to_s}" do
expect(@person_deletion).not_to receive(method)
end
it "does not call #close_user" do
expect(@person_deletion).not_to receive(:close_user)
end
(person_removal_methods).each do |method|
@ -81,6 +72,24 @@ describe AccountDeleter do
end
describe "#close_user" do
user_removal_methods = %i[
delete_standard_user_associations
remove_share_visibilities_on_contacts_posts
disconnect_contacts tombstone_user
]
after do
@account_deletion.perform!
end
user_removal_methods.each do |method|
it "calls ##{method}" do
expect(@account_deletion).to receive(method)
end
end
end
describe "#delete_standard_user_associations" do
it 'removes all standard user associaltions' do
@account_deletion.normal_ar_user_associates_to_delete.each do |asso|

View file

@ -8,6 +8,16 @@ describe Diaspora::Federation::Entities do
expect(federation_entity.author).to eq(diaspora_entity.person.diaspora_handle)
end
it "builds an account migration" do
diaspora_entity = FactoryGirl.build(:account_migration)
diaspora_entity.old_private_key = OpenSSL::PKey::RSA.generate(1024).export
federation_entity = described_class.build(diaspora_entity)
expect(federation_entity).to be_instance_of(DiasporaFederation::Entities::AccountMigration)
expect(federation_entity.author).to eq(diaspora_entity.old_person.diaspora_handle)
expect(federation_entity.profile.author).to eq(diaspora_entity.new_person.diaspora_handle)
end
it "builds a comment" do
diaspora_entity = FactoryGirl.build(:comment)
federation_entity = described_class.build(diaspora_entity)

View file

@ -12,6 +12,20 @@ describe Diaspora::Federation::Receive do
end
end
describe ".account_migration" do
let(:new_person) { FactoryGirl.create(:person) }
let(:profile_entity) { Fabricate(:profile_entity, author: new_person.diaspora_handle) }
let(:account_migration_entity) {
Fabricate(:account_migration_entity, author: sender.diaspora_handle, profile: profile_entity)
}
it "saves the account deletion" do
Diaspora::Federation::Receive.account_migration(account_migration_entity)
expect(AccountMigration.exists?(old_person: sender, new_person: new_person)).to be_truthy
end
end
describe ".comment" do
let(:comment_entity) {
build_relayable_federation_entity(

View file

@ -0,0 +1,148 @@
require "integration/federation/federation_helper"
describe AccountMigration, type: :model do
describe "create!" do
include_context "with local old user"
it "locks old local user after creation" do
expect {
AccountMigration.create!(old_person: old_person, new_person: FactoryGirl.create(:person))
}.to change { old_user.reload.access_locked? }.to be_truthy
end
end
let(:old_person) { FactoryGirl.create(:person) }
let(:new_person) { FactoryGirl.create(:person) }
let(:account_migration) {
AccountMigration.create!(old_person: old_person, new_person: new_person)
}
describe "receive" do
it "calls perform!" do
expect(account_migration).to receive(:perform!)
account_migration.receive
end
end
describe "sender" do
context "with remote old user" do
include_context "with remote old user"
it "creates ephemeral user when private key is provided" do
account_migration.old_private_key = old_user.serialized_private_key
sender = account_migration.sender
expect(sender.id).to eq(old_user.diaspora_handle)
expect(sender.diaspora_handle).to eq(old_user.diaspora_handle)
expect(sender.encryption_key.to_s).to eq(old_user.encryption_key.to_s)
end
it "raises when no private key is provided" do
expect {
account_migration.sender
}.to raise_error("can't build sender without old private key defined")
end
end
context "with local old user" do
include_context "with local old user"
it "matches the old user" do
expect(account_migration.sender).to eq(old_user)
end
end
end
describe "performed?" do
it "is changed after perform!" do
expect {
account_migration.perform!
}.to change(account_migration, :performed?).to be_truthy
end
it "calls old_person.closed_account?" do
expect(account_migration.old_person).to receive(:closed_account?)
account_migration.performed?
end
end
context "with local new user" do
include_context "with local new user"
describe "subscribers" do
it "picks remote subscribers of new user profile and old person" do
_local_friend, remote_contact = DataGenerator.create(new_user, %i[mutual_friend remote_mutual_friend])
expect(account_migration.new_person.owner.profile).to receive(:subscribers).and_call_original
expect(account_migration.subscribers).to match_array([remote_contact.person, old_person])
end
context "with local old user" do
include_context "with local old user"
it "doesn't include old person" do
expect(account_migration.subscribers).to be_empty
end
end
end
end
describe "perform!" do
# TODO: add references update tests
# This spec is missing references update tests. We didn't come with a good idea of how to test it
# and it is currently covered by integration tests. But it's beter to add these tests at some point
# in future when we have more time to think about it.
let(:embedded_account_deleter) { account_migration.send(:account_deleter) }
it "raises if already performed" do
expect(account_migration).to receive(:performed?).and_return(true)
expect {
account_migration.perform!
}.to raise_error("already performed")
end
it "calls AccountDeleter#tombstone_person_and_profile" do
expect(embedded_account_deleter).to receive(:tombstone_person_and_profile)
account_migration.perform!
end
context "with local old and remote new users" do
include_context "with local old user"
it "calls AccountDeleter#close_user" do
expect(embedded_account_deleter).to receive(:close_user)
account_migration.perform!
end
it "resends contacts to the remote pod" do
contact = FactoryGirl.create(:contact, person: old_person, sharing: true)
expect(Diaspora::Federation::Dispatcher).to receive(:defer_dispatch).with(contact.user, contact)
account_migration.perform!
end
end
context "with local new and remote old users" do
include_context "with remote old user"
include_context "with local new user"
it "dispatches account migration message" do
expect(account_migration).to receive(:sender).and_return(old_user)
dispatcher = double
expect(dispatcher).to receive(:dispatch)
expect(Diaspora::Federation::Dispatcher).to receive(:build)
.with(old_user, account_migration)
.and_return(dispatcher)
account_migration.perform!
end
end
context "with local old and new users" do
include_context "with local old user"
include_context "with local new user"
it "calls AccountDeleter#tombstone_user" do
expect(embedded_account_deleter).to receive(:tombstone_user)
account_migration.perform!
end
end
end
end

View file

@ -287,6 +287,12 @@ describe Profile, :type => :model do
expect(@profile.taggings).to receive(:delete_all)
@profile.tombstone!
end
it "doesn't recreate taggings if tag string was requested" do
@profile.tag_string
@profile.tombstone!
expect(@profile.taggings).to be_empty
end
end
describe "#clearable_fields" do

View file

@ -2,9 +2,27 @@
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
shared_examples_for "it removes the person associations" do
RSpec::Matchers.define_negated_matcher :remain, :change
shared_examples_for "deletes all of the user data" do
it "deletes all of the user data" do
expect(user).not_to be_a_clear_account
expect {
account_removal_method
}.to change(nil, "user preferences empty?") { UserPreference.where(user_id: user.id).empty? }
.to(be_truthy)
.and(change(nil, "notifications empty?") { Notification.where(recipient_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "blocks empty?") { Block.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "services empty?") { Service.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "share visibilities empty?") { ShareVisibility.where(user_id: user.id).empty? }.to(be_truthy))
.and(change(nil, "aspects empty?") { user.aspects.empty? }.to(be_truthy))
.and(change(nil, "contacts empty?") { user.contacts.empty? }.to(be_truthy))
.and(change(nil, "tag followings empty?") { user.tag_followings.empty? }.to(be_truthy))
expect(user.reload).to be_a_clear_account
end
end
shared_examples_for "it removes the person associations" do
it "removes all of the person associations" do
expect {
account_removal_method
@ -20,9 +38,39 @@ shared_examples_for "it removes the person associations" do
.and(change(nil, "conversation visibilities empty?") {
ConversationVisibility.where(person_id: person.id).empty?
}.to(be_truthy))
.and(remain(nil, "conversations empty?") { Conversation.where(author: person).empty? }.from(be_falsey))
end
end
shared_examples_for "it keeps the person conversations" do
RSpec::Matchers.define_negated_matcher :remain, :change
it "remains the person conversations" do
expect {
account_removal_method
}.to remain(nil, "conversations empty?") { Conversation.where(author: person).empty? }
.from(be_falsey)
.and(remain(nil, "conversation visibilities of other participants empty?") {
ConversationVisibility.where(conversation: Conversation.where(author: person)).empty?
}.from(be_falsey))
end
end
shared_examples_for "it removes the person conversations" do
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
# In fact this example group if for testing effect of AccountDeleter.tombstone_person_and_profile
shared_examples_for "it makes account closed and clears profile" do
it "" do
expect(subject).to be_a_closed_account
expect(subject.profile).to be_a_clear_profile
end
end

View file

@ -0,0 +1,187 @@
shared_context "with local old user" do
let(:old_user) { FactoryGirl.create(:user) }
let(:old_person) { old_user.person }
end
shared_context "with local new user" do
let(:new_user) { FactoryGirl.create(:user) }
let(:new_person) { new_user.person }
end
shared_context "with remote old user" do
let(:old_user) { remote_user_on_pod_c }
let(:old_person) { old_user.person }
end
shared_context "with remote new user" do
let(:new_user) { remote_user_on_pod_b }
let(:new_person) { new_user.person }
end
shared_examples_for "it updates person references" do
it "updates contact reference" do
contact = FactoryGirl.create(:contact, person: old_person)
run_migration
expect(contact.reload.person).to eq(new_person)
end
it "updates status message reference" do
post = FactoryGirl.create(:status_message, author: old_person)
run_migration
expect(post.reload.author).to eq(new_person)
end
it "updates reshare reference" do
reshare = FactoryGirl.create(:reshare, author: old_person)
run_migration
expect(reshare.reload.author).to eq(new_person)
end
it "updates photo reference" do
photo = FactoryGirl.create(:photo, author: old_person)
run_migration
expect(photo.reload.author).to eq(new_person)
end
it "updates comment reference" do
comment = FactoryGirl.create(:comment, author: old_person)
run_migration
expect(comment.reload.author).to eq(new_person)
end
it "updates like reference" do
like = FactoryGirl.create(:like, author: old_person)
run_migration
expect(like.reload.author).to eq(new_person)
end
it "updates participations reference" do
participation = FactoryGirl.create(:participation, author: old_person)
run_migration
expect(participation.reload.author).to eq(new_person)
end
it "updates poll participations reference" do
poll_participation = FactoryGirl.create(:poll_participation, author: old_person)
run_migration
expect(poll_participation.reload.author).to eq(new_person)
end
it "updates conversation visibilities reference" do
conversation = FactoryGirl.build(:conversation)
FactoryGirl.create(:contact, user: old_user, person: conversation.author) if old_person.local?
conversation.participants << old_person
conversation.save!
visibility = ConversationVisibility.find_by(person_id: old_person.id)
run_migration
expect(visibility.reload.person).to eq(new_person)
end
it "updates message reference" do
message = FactoryGirl.create(:message, author: old_person)
run_migration
expect(message.reload.author).to eq(new_person)
end
it "updates conversation reference" do
conversation = FactoryGirl.create(:conversation, author: old_person)
run_migration
expect(conversation.reload.author).to eq(new_person)
end
it "updates block references" do
user = FactoryGirl.create(:user)
block = user.blocks.create(person: old_person)
run_migration
expect(block.reload.person).to eq(new_person)
end
it "updates role reference" do
role = FactoryGirl.create(:role, person: old_person)
run_migration
expect(role.reload.person).to eq(new_person)
end
it "updates notification actors" do
notification = FactoryGirl.build(:notification)
notification.actors << old_person
notification.save!
actor = notification.notification_actors.find_by(person_id: old_person.id)
run_migration
expect(actor.reload.person).to eq(new_person)
end
it "updates mention reference" do
mention = FactoryGirl.create(:mention, person: old_person)
run_migration
expect(mention.reload.person).to eq(new_person)
end
end
shared_examples_for "it updates user references" do
it "updates invited users reference" do
invited_user = FactoryGirl.create(:user, invited_by: old_user)
run_migration
expect(invited_user.reload.invited_by).to eq(new_user)
end
it "updates aspect reference" do
aspect = FactoryGirl.create(:aspect, user: old_user, name: r_str)
run_migration
expect(aspect.reload.user).to eq(new_user)
end
it "updates contact reference" do
contact = FactoryGirl.create(:contact, user: old_user)
run_migration
expect(contact.reload.user).to eq(new_user)
end
it "updates services reference" do
service = FactoryGirl.create(:service, user: old_user)
run_migration
expect(service.reload.user).to eq(new_user)
end
it "updates user preference references" do
pref = UserPreference.create!(user: old_user, email_type: "also_commented")
run_migration
expect(pref.reload.user).to eq(new_user)
end
it "updates tag following references" do
tag_following = FactoryGirl.create(:tag_following, user: old_user)
run_migration
expect(tag_following.reload.user).to eq(new_user)
end
it "updates blocks refrences" do
block = FactoryGirl.create(:block, user: old_user)
run_migration
expect(block.reload.user).to eq(new_user)
end
it "updates notification refrences" do
notification = FactoryGirl.create(:notification, recipient: old_user)
run_migration
expect(notification.reload.recipient).to eq(new_user)
end
it "updates report refrences" do
report = FactoryGirl.create(:report, user: old_user)
run_migration
expect(report.reload.user).to eq(new_user)
end
it "updates authorization refrences" do
authorization = FactoryGirl.create(:auth_with_read, user: old_user)
run_migration
expect(authorization.reload.user).to eq(new_user)
end
it "updates share visibility refrences" do
share_visibility = FactoryGirl.create(:share_visibility, user: old_user)
run_migration
expect(share_visibility.reload.user).to eq(new_user)
end
end

View file

@ -0,0 +1,43 @@
RSpec::Matchers.define :be_a_discovered_person do
match do |person|
!Person.by_account_identifier(person.diaspora_handle).nil?
end
end
RSpec::Matchers.define :be_a_closed_account do
match(&:closed_account?)
end
RSpec::Matchers.define :be_a_locked_account do
match(&:access_locked?)
end
RSpec::Matchers.define :be_a_clear_profile do
match do |profile|
attributes = %i[
diaspora_handle first_name last_name image_url image_url_small image_url_medium birthday gender bio
location nsfw public_details
].map {|attribute| profile[attribute] }
profile.taggings.empty? && !profile.searchable && attributes.reject(&:nil?).empty?
end
end
RSpec::Matchers.define :be_a_clear_account do
match do |user|
attributes = %i[
language reset_password_token remember_created_at sign_in_count current_sign_in_at last_sign_in_at
current_sign_in_ip last_sign_in_ip invited_by_id authentication_token unconfirmed_email confirm_email_token
auto_follow_back auto_follow_back_aspect_id reset_password_sent_at last_seen color_theme
].map {|attribute| user[attribute] }
user.disable_mail &&
user.strip_exif &&
!user.getting_started &&
!user.show_community_spotlight_in_stream &&
!user.post_default_public &&
user.email == "deletedaccount_#{user.id}@example.org" &&
user.hidden_shareables.empty? &&
attributes.reject(&:nil?).empty?
end
end