Merge pull request #6750 from cmrd-senya/account_migration_message

Account migration model/message
This commit is contained in:
Benjamin Neff 2017-08-15 01:11:50 +02:00
commit cbc3900d59
No known key found for this signature in database
GPG key ID: 971464C3F1A90194
28 changed files with 1073 additions and 76 deletions

View file

@ -56,6 +56,7 @@ If so, please delete it since it will prevent the federation from working proper
* Support cmd+enter to submit posts, comments and conversations [#7524](https://github.com/diaspora/diaspora/pull/7524)
* Add markdown editor for posts, comments and conversations on mobile [#7235](https://github.com/diaspora/diaspora/pull/7235)
* Mark as "Mobile Web App Capable" on Android [#7534](https://github.com/diaspora/diaspora/pull/7534)
* Add support for receiving account migrations [#6750](https://github.com/diaspora/diaspora/pull/6750)
# 0.6.8.0

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

@ -5,6 +5,7 @@
describe StreamsController, :type => :controller do
describe '#multi' do
before do
allow(Workers::SendPublic).to receive(:perform_async)
sign_in alice, scope: :user
end

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

@ -14,6 +14,7 @@ describe DataGenerator do
generator.generic_user_data
expect(user.aspects).not_to be_empty
expect(Post.subscribed_by(user)).not_to be_empty
expect(Contact.where(user: user).mutual).not_to be_empty
end
end

View file

@ -106,8 +106,6 @@ RSpec.configure do |config|
I18n.locale = :en
stub_request(:post, "https://pubsubhubbub.appspot.com/")
$process_queue = false
allow(Workers::SendPublic).to receive(:perform_async)
allow(Workers::SendPrivate).to receive(:perform_async)
end
config.expect_with :rspec do |expect_config|

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

View file

@ -38,6 +38,7 @@ class DataGenerator
private_post_as_receipient
tag_following
generic_person_data
remote_mutual_friend
end
def generic_person_data
@ -98,6 +99,10 @@ class DataGenerator
}
end
def remote_mutual_friend
FactoryGirl.create(:contact, user: user, sharing: true, receiving: true)
end
def first_aspect
user.aspects.first || FactoryGirl.create(:aspect, user: user)
end

View file

@ -2,6 +2,8 @@ class User
alias_method :share_with_original, :share_with
def share_with(*args)
disable_send_workers
inlined_jobs do
share_with_original(*args)
end
@ -13,6 +15,8 @@ class User
end
def post(class_name, opts = {})
disable_send_workers
inlined_jobs do
aspects = self.aspects_from_ids(opts[:to])
@ -22,11 +26,9 @@ class User
self.aspects.reload
dispatch_opts = {
url: Rails.application.routes.url_helpers.post_url(
p,
host: AppConfig.pod_uri.to_s
),
to: opts[:to]}
url: Rails.application.routes.url_helpers.post_url(p, host: AppConfig.pod_uri.to_s),
to: opts[:to]
}
dispatch_post(p, dispatch_opts)
end
unless opts[:created_at]
@ -40,4 +42,11 @@ class User
def build_comment(options={})
Comment::Generator.new(self, options.delete(:post), options.delete(:text)).build(options)
end
def disable_send_workers
RSpec.current_example&.example_group_instance&.instance_eval do
allow(Workers::SendPrivate).to receive(:perform_async)
allow(Workers::SendPublic).to receive(:perform_async)
end
end
end