diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index dc7e1416b..ac9c708d2 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -90,6 +90,11 @@ class PeopleController < ApplicationController raise ActiveRecord::RecordNotFound end + if @person.closed_account? + redirect_to :back, :notice => t("people.show.closed_account") + return + end + @post_type = :all @aspect = :profile @share_with = (params[:share_with] == 'true') @@ -177,6 +182,4 @@ class PeopleController < ApplicationController def remote_profile_with_no_user_session? @person && @person.remote? && !user_signed_in? end - - end diff --git a/app/controllers/publics_controller.rb b/app/controllers/publics_controller.rb index a24525534..c6d8bbc76 100644 --- a/app/controllers/publics_controller.rb +++ b/app/controllers/publics_controller.rb @@ -32,6 +32,12 @@ class PublicsController < ApplicationController def hcard @person = Person.where(:guid => params[:guid]).first + + if @person && @person.closed_account? + render :nothing => true, :status => 404 + return + end + unless @person.nil? || @person.owner.nil? render 'publics/hcard' else @@ -45,6 +51,12 @@ class PublicsController < ApplicationController def webfinger @person = Person.local_by_account_identifier(params[:q]) if params[:q] + + if @person && @person.closed_account? + render :nothing => true, :status => 404 + return + end + unless @person.nil? render 'webfinger', :content_type => 'application/xrd+xml' else diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5777c8e33..aea10ee1d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -81,11 +81,9 @@ class UsersController < ApplicationController def destroy if params[:user] && params[:user][:current_password] && current_user.valid_password?(params[:user][:current_password]) - Resque.enqueue(Jobs::DeleteAccount, current_user.id) - current_user.lock_access! + current_user.close_account! sign_out current_user - flash[:notice] = I18n.t 'users.destroy.success' - redirect_to multi_path + redirect_to(multi_path, :notice => I18n.t('users.destroy.success')) else if params[:user].present? && params[:user][:current_password].present? flash[:error] = t 'users.destroy.wrong_password' diff --git a/app/helpers/aspects_helper.rb b/app/helpers/aspects_helper.rb index 906ddc272..f02505b01 100644 --- a/app/helpers/aspects_helper.rb +++ b/app/helpers/aspects_helper.rb @@ -39,6 +39,8 @@ module AspectsHelper end def aspect_membership_button(aspect, contact, person) + return if person && person.closed_account? + if contact.nil? || !contact.aspect_memberships.detect{ |am| am.aspect_id == aspect.id} add_to_aspect_button(aspect.id, person.id) else diff --git a/app/models/account_deleter.rb b/app/models/account_deleter.rb new file mode 100644 index 000000000..9c14ed68f --- /dev/null +++ b/app/models/account_deleter.rb @@ -0,0 +1,112 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class AccountDeleter + + # Things that are not removed from the database: + # - Comments + # - Likes + # - Messages + # - NotificationActors + # + # Given that the User in question will be tombstoned, all of the + # above will come from an anonomized account (via the UI). + # The deleted user will appear as "Deleted Account" in + # the interface. + + attr_accessor :person, :user + + def initialize(diaspora_handle) + self.person = Person.where(:diaspora_handle => diaspora_handle).first + self.user = self.person.owner + end + + def perform! + #person + delete_standard_person_associations + remove_conversation_visibilities + remove_share_visibilities_on_persons_posts + delete_contacts_of_me + tombstone_person_and_profile + + if self.user + #user deletion methods + remove_share_visibilities_on_contacts_posts + delete_standard_user_associations + disassociate_invitations + disconnect_contacts + tombstone_user + end + end + + #user deletions + def normal_ar_user_associates_to_delete + [:tag_followings, :authorizations, :invitations_to_me, :services, :aspects, :user_preferences, :notifications, :blocks] + end + + def special_ar_user_associations + [:invitations_from_me, :person, :contacts] + end + + def ignored_ar_user_associations + [:followed_tags, :invited_by, :contact_people, :applications, :aspect_memberships] + end + + def delete_standard_user_associations + normal_ar_user_associates_to_delete.each do |asso| + self.user.send(asso).each{|model| model.delete} + end + end + + def delete_standard_person_associations + normal_ar_person_associates_to_delete.each do |asso| + self.person.send(asso).delete_all + end + end + + def disassociate_invitations + user.invitations_from_me.each do |inv| + inv.convert_to_admin! + end + end + + def disconnect_contacts + user.contacts.destroy_all + end + + # Currently this would get deleted due to the db foreign key constrainsts, + # but we'll keep this method here for completeness + def remove_share_visibilities_on_persons_posts + ShareVisibility.for_contacts_of_a_person(person).destroy_all + end + + def remove_share_visibilities_on_contacts_posts + ShareVisibility.for_a_users_contacts(user).destroy_all + end + + def remove_conversation_visibilities + ConversationVisibility.where(:person_id => person.id).destroy_all + end + + def tombstone_person_and_profile + self.person.lock_access! + self.person.clear_profile! + end + + def tombstone_user + self.user.clear_account! + end + + def delete_contacts_of_me + Contact.all_contacts_of_person(self.person).destroy_all + end + + def normal_ar_person_associates_to_delete + [:posts, :photos, :mentions] + end + + def ignored_or_special_ar_person_associations + [:comments, :contacts, :notification_actors, :notifications, :owner, :profile ] + end +end diff --git a/app/models/account_deletion.rb b/app/models/account_deletion.rb new file mode 100644 index 000000000..9bd2a9e3b --- /dev/null +++ b/app/models/account_deletion.rb @@ -0,0 +1,50 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +class AccountDeletion < ActiveRecord::Base + include ROXML + include Diaspora::Webhooks + + + belongs_to :person + after_create :queue_delete_account + + attr_accessible :person + + xml_name :account_deletion + xml_attr :diaspora_handle + + def person=(person) + self[:diaspora_handle] = person.diaspora_handle + self[:person_id] = person.id + end + + def diaspora_handle=(diaspora_handle) + self[:diaspora_handle] = diaspora_handle + self[:person_id] ||= Person.find_by_diaspora_handle(diaspora_handle).id + end + + + + def queue_delete_account + Resque.enqueue(Jobs::DeleteAccount, self.id) + end + + def perform! + self.dispatch if person.local? + AccountDeleter.new(self.diaspora_handle).perform! + end + + def subscribers(user) + person.owner.contact_people.remote | Person.who_have_reshared_a_users_posts(person.owner).remote + end + + def dispatch + Postzord::Dispatcher.build(person.owner, self).post + end + + def public? + true + end +end diff --git a/app/models/contact.rb b/app/models/contact.rb index 518b11e89..22c5a5f06 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -15,7 +15,8 @@ class Contact < ActiveRecord::Base has_many :posts, :through => :share_visibilities, :source => :shareable, :source_type => 'Post' validate :not_contact_for_self, - :not_blocked_user + :not_blocked_user, + :not_contact_with_closed_account validates_presence_of :user validates_uniqueness_of :person_id, :scope => :user_id @@ -23,7 +24,10 @@ class Contact < ActiveRecord::Base before_destroy :destroy_notifications, :repopulate_cache! - # contact.sharing is true when contact.person is sharing with contact.user + + scope :all_contacts_of_person, lambda {|x| where(:person_id => x.id)} + + # contact.sharing is true when contact.person is sharing with contact.user scope :sharing, lambda { where(:sharing => true) } @@ -94,6 +98,12 @@ class Contact < ActiveRecord::Base end private + def not_contact_with_closed_account + if person_id && person.closed_account? + errors[:base] << 'Cannot be in contact with a closed account' + end + end + def not_contact_for_self if person_id && person.owner == user errors[:base] << 'Cannot create self-contact' diff --git a/app/models/invitation.rb b/app/models/invitation.rb index 3be97a991..fc364b16c 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -99,6 +99,17 @@ class Invitation < ActiveRecord::Base self end + + # converts a personal invitation to an admin invite + # used in account deletion + # @return [Invitation] self + def convert_to_admin! + self.admin = true + self.sender = nil + self.aspect = nil + self.save + self + end # @return [Invitation] self def resend self.send! diff --git a/app/models/jobs/delete_account.rb b/app/models/jobs/delete_account.rb index 0d9b42f8f..0d856dda4 100644 --- a/app/models/jobs/delete_account.rb +++ b/app/models/jobs/delete_account.rb @@ -6,10 +6,9 @@ module Jobs class DeleteAccount < Base @queue = :delete_account - def self.perform(user_id) - user = User.find(user_id) - user.remove_all_traces - user.destroy + def self.perform(account_deletion_id) + account_deletion = AccountDeletion.find(account_deletion_id) + account_deletion.perform! end end end diff --git a/app/models/person.rb b/app/models/person.rb index 0d6d40829..9899f8528 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -40,7 +40,7 @@ class Person < ActiveRecord::Base before_destroy :remove_all_traces before_validation :clean_url - + validates :url, :presence => true validates :profile, :presence => true validates :serialized_public_key, :presence => true @@ -61,6 +61,10 @@ class Person < ActiveRecord::Base scope :profile_tagged_with, lambda{|tag_name| joins(:profile => :tags).where(:profile => {:tags => {:name => tag_name}}).where('profiles.searchable IS TRUE') } + scope :who_have_reshared_a_users_posts, lambda{|user| + joins(:posts).where(:posts => {:root_guid => StatusMessage.guids_for_author(user.person), :type => 'Reshare'} ) + } + def self.community_spotlight AppConfig[:community_spotlight].present? ? Person.where(:diaspora_handle => AppConfig[:community_spotlight]) : [] end @@ -78,7 +82,7 @@ class Person < ActiveRecord::Base super self.profile ||= Profile.new unless profile_set end - + def self.find_from_id_or_username(params) p = if params[:id].present? Person.where(:id => params[:id]).first @@ -91,7 +95,6 @@ class Person < ActiveRecord::Base p end - def self.search_query_string(query) query = query.downcase like_operator = postgres? ? "ILIKE" : "LIKE" @@ -276,8 +279,6 @@ class Person < ActiveRecord::Base end end - - # @param person [Person] # @param url [String] def update_url(url) @@ -288,6 +289,16 @@ class Person < ActiveRecord::Base self.update_attributes(:url => newuri) end + def lock_access! + self.closed_account = true + self.save + end + + def clear_profile! + self.profile.tombstone! + self + end + protected def clean_url diff --git a/app/models/profile.rb b/app/models/profile.rb index 7764a4f28..cdbb1b2be 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -146,6 +146,14 @@ class Profile < ActiveRecord::Base self.full_name end + def tombstone! + self.taggings.delete_all + clearable_fields.each do |field| + self[field] = nil + end + self.save + end + protected def strip_names self.first_name.strip! if self.first_name @@ -166,6 +174,10 @@ class Profile < ActiveRecord::Base end private + def clearable_fields + self.attributes.keys - Profile.protected_attributes.to_a - ["created_at", "updated_at", "person_id"] + end + def absolutify_local_url url pod_url = AppConfig[:pod_url].dup pod_url.chop! if AppConfig[:pod_url][-1,1] == '/' diff --git a/app/models/share_visibility.rb b/app/models/share_visibility.rb index 6fd3c0712..b46fb3342 100644 --- a/app/models/share_visibility.rb +++ b/app/models/share_visibility.rb @@ -6,6 +6,13 @@ class ShareVisibility < ActiveRecord::Base belongs_to :contact belongs_to :shareable, :polymorphic => :true + scope :for_a_users_contacts, lambda { |user| + where(:contact_id => user.contacts.map {|c| c.id}) + } + scope :for_contacts_of_a_person, lambda { |person| + where(:contact_id => person.contacts.map {|c| c.id}) + } + # Perform a batch import, given a set of contacts and a shareable # @note performs a bulk insert in mySQL; performs linear insertions in postgres # @param contacts [Array] Recipients diff --git a/app/models/status_message.rb b/app/models/status_message.rb index af7250069..dd32b6cc0 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -42,6 +42,10 @@ class StatusMessage < Post joins(:likes).where(:likes => {:author_id => person.id}) } + def self.guids_for_author(person) + Post.connection.select_values(Post.where(:author_id => person.id).select('posts.guid').to_sql) + end + def self.user_tag_stream(user, tag_ids) owned_or_visible_by_user(user). tag_stream(tag_ids) diff --git a/app/models/user.rb b/app/models/user.rb index 654bb7bb1..303d86d6e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -33,17 +33,18 @@ class User < ActiveRecord::Base has_one :person, :foreign_key => :owner_id delegate :public_key, :posts, :photos, :owns?, :diaspora_handle, :name, :public_url, :profile, :first_name, :last_name, :to => :person - has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id, :dependent => :destroy - has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id, :dependent => :destroy + has_many :invitations_from_me, :class_name => 'Invitation', :foreign_key => :sender_id + has_many :invitations_to_me, :class_name => 'Invitation', :foreign_key => :recipient_id has_many :aspects, :order => 'order_id ASC' has_many :aspect_memberships, :through => :aspects has_many :contacts has_many :contact_people, :through => :contacts, :source => :person - has_many :services, :dependent => :destroy - has_many :user_preferences, :dependent => :destroy - has_many :tag_followings, :dependent => :destroy + has_many :services + has_many :user_preferences + has_many :tag_followings has_many :followed_tags, :through => :tag_followings, :source => :tag, :order => 'tags.name' has_many :blocks + has_many :notifications, :foreign_key => :recipient_id has_many :authorizations, :class_name => 'OAuth2::Provider::Models::ActiveRecord::Authorization', :foreign_key => :resource_owner_id has_many :applications, :through => :authorizations, :source => :client @@ -487,4 +488,26 @@ class User < ActiveRecord::Base errors[:base] << 'That username has already been taken' end end + + def close_account! + self.person.lock_access! + self.lock_access! + AccountDeletion.create(:person => self.person) + end + + def clear_account! + clearable_fields.each do |field| + self[field] = nil + end + + random_password = ActiveSupport::SecureRandom.hex(20) + self.password = random_password + self.password_confirmation = random_password + self.save(:validate => false) + end + + private + def clearable_fields + self.attributes.keys - ["id", "username", "encrypted_password", "created_at", "updated_at", "locked_at", "serialized_private_key"] + end end diff --git a/app/views/users/edit.html.haml b/app/views/users/edit.html.haml index d0e6ac949..6f92f1ce7 100644 --- a/app/views/users/edit.html.haml +++ b/app/views/users/edit.html.haml @@ -164,13 +164,42 @@ = link_to t('.download_photos'), "#", :class => "button", :id => "photo-export-button", :title => t('.photo_export_unavailable') .span-5.last - %h3 - = t('.close_account') - = form_for 'user', :url => user_path, :html => { :method => :delete } do |f| - = f.error_messages + %h3 + = t('.close_account_text') + =link_to 'Close Account', '#close_account_pane', :rel => 'facebox', :class => "button" - %p - = f.label :close_account_password, t('.current_password'), :for => :close_account_password - = f.password_field :current_password, :id => :close_account_password - %p - = f.submit t('.close_account'), :confirm => t('are_you_sure') + .hidden#close_account_pane{:rel => 'facebox'} + #inner_account_delete + %h1 + = t('.close_account.dont_go') + %p + = t('.close_account.make_diaspora_better') + .span-10 + = image_tag 'http://itstrulyrandom.com/wp-content/uploads/2008/03/sadcat.jpg' + %br + %small + %b + = t('.close_account.mr_wiggles') + .span-10.last + %ul + %li + = t('.close_account.what_we_delete') + %li + = t('.close_account.locked_out') + %li + = t('.close_account.lock_username') + %li + = t('.close_account.no_turning_back') + %p + %b + = t('.close_account.no_turning_back') + + + = form_for 'user', :url => user_path, :html => { :method => :delete } do |f| + = f.error_messages + + %p + = f.label :close_account_password, t('.current_password'), :for => :close_account_password + = f.password_field :current_password, :id => :close_account_password + %p + = f.submit t('.close_account_text'), :confirm => t('are_you_sure_delete_account') diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 87a6fd482..6bd15c7fd 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -27,6 +27,7 @@ en: password: "Password" password_confirmation: "Password confirmation" are_you_sure: "Are you sure?" + are_you_sure_delete_account: "Are you sure you want to close your account? This can't be undone!" fill_me_out: "Fill me out" back: "Back" public: "Public" @@ -551,6 +552,7 @@ en: message: "Message" mention: "Mention" ignoring: "You are ignoring all posts from %{name}." + closed_account: "This account has been closed." sub_header: you_have_no_tags: "you have no tags!" add_some: "add some" @@ -930,7 +932,7 @@ en: edit: export_data: "Export Data" photo_export_unavailable: "Photo exporting currently unavailable" - close_account: "Close Account" + close_account_text: "Close Account" change_language: "Change language" change_password: "Change password" change_email: "Change email" @@ -956,6 +958,16 @@ en: show_getting_started: 'Re-enable Getting Started' getting_started: 'New User Prefrences' + close_account: + dont_go: "Hey, please don't go!" + make_diaspora_better: "We want you to help us make Diaspora better, so you should help us out instead of leaving. If you do want to leave, we want you to know what happens next." + mr_wiggles: 'Mr Wiggles will be sad to see you go' + what_we_delete: "We delete all of your posts, profile data, as soon as humanly possible. Your comments will hang around, but be associated with your Diaspora Handle." + locked_out: "You will get signed out and locked out of your account." + lock_username: "This will lock your username if you decided to sign back up." + no_turning_back: "Currently, there is no turning back." + if_you_want_this: "If you really want this, type in your password below and click 'Close Account'" + privacy_settings: title: "Privacy Settings" ignored_users: "Ignored Users" diff --git a/db/migrate/20111103184050_add_closed_account_flag_to_person.rb b/db/migrate/20111103184050_add_closed_account_flag_to_person.rb new file mode 100644 index 000000000..0a845f835 --- /dev/null +++ b/db/migrate/20111103184050_add_closed_account_flag_to_person.rb @@ -0,0 +1,9 @@ +class AddClosedAccountFlagToPerson < ActiveRecord::Migration + def self.up + add_column :people, :closed_account, :boolean, :default => false + end + + def self.down + remove_column :people, :closed_account + end +end diff --git a/db/migrate/20111109023618_create_account_deletions.rb b/db/migrate/20111109023618_create_account_deletions.rb new file mode 100644 index 000000000..6e2d1f9da --- /dev/null +++ b/db/migrate/20111109023618_create_account_deletions.rb @@ -0,0 +1,12 @@ +class CreateAccountDeletions < ActiveRecord::Migration + def self.up + create_table :account_deletions do |t| + t.string :diaspora_handle + t.integer :person_id + end + end + + def self.down + drop_table :account_deletions + end +end diff --git a/db/schema.rb b/db/schema.rb index 4a3992302..67b994b0e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,12 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20111101202137) do +ActiveRecord::Schema.define(:version => 20111109023618) do + + create_table "account_deletions", :force => true do |t| + t.string "diaspora_handle" + t.integer "person_id" + end create_table "aspect_memberships", :force => true do |t| t.integer "aspect_id", :null => false @@ -238,13 +243,14 @@ ActiveRecord::Schema.define(:version => 20111101202137) do add_index "oauth_clients", ["nonce"], :name => "index_oauth_clients_on_nonce", :unique => true create_table "people", :force => true do |t| - t.string "guid", :null => false - t.text "url", :null => false - t.string "diaspora_handle", :null => false - t.text "serialized_public_key", :null => false + t.string "guid", :null => false + t.text "url", :null => false + t.string "diaspora_handle", :null => false + t.text "serialized_public_key", :null => false t.integer "owner_id" t.datetime "created_at" t.datetime "updated_at" + t.boolean "closed_account", :default => false end add_index "people", ["diaspora_handle"], :name => "index_people_on_diaspora_handle", :unique => true diff --git a/features/closes_account.feature b/features/closes_account.feature index 6f6b0b24d..11242455a 100644 --- a/features/closes_account.feature +++ b/features/closes_account.feature @@ -8,22 +8,24 @@ Feature: Close Account Given I am signed in When I click on my name in the header And I follow "Settings" - And I put in my password in "close_account_password" + And I follow "Close Account" + And I put in my password in "close_account_password" in the modal window And I preemptively confirm the alert - And I press "Close Account" + And I press "Close Account" in the modal window Then I should be on the new user session page When I try to sign in manually Then I should be on the new user session page When I wait for the ajax to finish - Then I should see "Invalid email or password." + Then I should see "Your account is locked." Scenario: user is forced to enter something in the password field on closing account Given I am signed in When I click on my name in the header And I follow "Settings" + And I follow "Close Account" And I preemptively confirm the alert - And I press "Close Account" + And I press "Close Account" in the modal window Then I should be on the edit user page And I should see "Please enter your current password to close your account." @@ -31,9 +33,10 @@ Feature: Close Account Given I am signed in When I click on my name in the header And I follow "Settings" + And I follow "Close Account" And I preemptively confirm the alert - And I fill in "close_account_password" with "none sense" - And I press "Close Account" + And I fill in "close_account_password" with "none sense" in the modal window + And I press "Close Account" in the modal window Then I should be on the edit user page And I should see "The entered password didn't match your current password." @@ -50,9 +53,10 @@ Feature: Close Account Then I sign in as "bob@bob.bob" When I click on my name in the header And I follow "Settings" - And I put in my password in "close_account_password" + And I follow "Close Account" + And I put in my password in "close_account_password" in the modal window And I preemptively confirm the alert - And I press "Close Account" + And I press "Close Account" in the modal window Then I sign in as "alice@alice.alice" And I am on the home page Then I should see "Hi, Bob Jones long time no see" diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb index 277de7bb1..cd93179a2 100644 --- a/lib/postzord/receiver/public.rb +++ b/lib/postzord/receiver/public.rb @@ -23,6 +23,8 @@ class Postzord::Receiver::Public < Postzord::Receiver if @object.respond_to?(:relayable?) receive_relayable + elsif @object.is_a?(AccountDeletion) + #nothing else Resque.enqueue(Jobs::ReceiveLocalBatch, @object.class.to_s, @object.id, self.recipient_user_ids) true diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass index 2910d0af6..43b1767f0 100644 --- a/public/stylesheets/sass/application.sass +++ b/public/stylesheets/sass/application.sass @@ -2281,6 +2281,9 @@ ul.show_comments, :position relative :top 10px +#inner_account_delete + :width 810px + #aspect_edit_pane :width 810px .person_tiles diff --git a/spec/controllers/people_controller_spec.rb b/spec/controllers/people_controller_spec.rb index 6dfc245af..3c4630b7e 100644 --- a/spec/controllers/people_controller_spec.rb +++ b/spec/controllers/people_controller_spec.rb @@ -161,6 +161,13 @@ describe PeopleController do response.code.should == "404" end + it 'redirects home for closed account' do + @person = Factory.create(:person, :closed_account => true) + get :show, :id => @person.id + response.should be_redirect + flash[:notice].should_not be_blank + end + it 'does not allow xss attacks' do user2 = bob profile = user2.profile diff --git a/spec/controllers/publics_controller_spec.rb b/spec/controllers/publics_controller_spec.rb index 903fb9162..ddff1c413 100644 --- a/spec/controllers/publics_controller_spec.rb +++ b/spec/controllers/publics_controller_spec.rb @@ -97,6 +97,12 @@ describe PublicsController do assigns[:person].should be_nil response.should be_not_found end + + it 'finds nothing for closed accounts' do + @user.person.update_attributes(:closed_account => true) + get :hcard, :guid => @user.person.guid.to_s + response.should be_not_found + end end describe '#webfinger' do @@ -127,6 +133,12 @@ describe PublicsController do get :webfinger, :q => @user.diaspora_handle response.body.should include "http://webfinger.net/rel/profile-page" end + + it 'finds nothing for closed accounts' do + @user.person.update_attributes(:closed_account => true) + get :webfinger, :q => @user.diaspora_handle + response.should be_not_found + end end describe '#hub' do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 568fdcff7..1f47f3c70 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -10,6 +10,7 @@ describe UsersController do @aspect = @user.aspects.first @aspect1 = @user.aspects.create(:name => "super!!") sign_in :user, @user + @controller.stub(:current_user).and_return(@user) end describe '#export' do @@ -192,15 +193,16 @@ describe UsersController do delete :destroy, :user => { :current_password => "stuff" } end + it 'closes the account' do + alice.should_receive(:close_account!) + delete :destroy, :user => { :current_password => "bluepin7" } + end + it 'enqueues a delete job' do Resque.should_receive(:enqueue).with(Jobs::DeleteAccount, alice.id) delete :destroy, :user => { :current_password => "bluepin7" } end - it 'locks the user out' do - delete :destroy, :user => { :current_password => "bluepin7" } - alice.reload.access_locked?.should be_true - end end describe '#confirm_email' do diff --git a/spec/factories.rb b/spec/factories.rb index 74578cf6e..69a563615 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -13,9 +13,18 @@ end Factory.define :profile do |p| p.sequence(:first_name) { |n| "Robert#{n}#{r_str}" } p.sequence(:last_name) { |n| "Grimm#{n}#{r_str}" } + p.bio "I am a cat lover and I love to run" + p.gender "robot" + p.location "Earth" p.birthday Date.today end +Factory.define :profile_with_image_url, :parent => :profile do |p| + p.image_url "http://example.com/image.jpg" + p.image_url_medium "http://example.com/image_mid.jpg" + p.image_url_small "http://example.com/image_small.jpg" +end + Factory.define :person do |p| p.sequence(:diaspora_handle) { |n| "bob-person-#{n}#{r_str}@example.net" } p.sequence(:url) { |n| AppConfig[:pod_url] } @@ -28,6 +37,13 @@ Factory.define :person do |p| end end +Factory.define :account_deletion do |d| + d.association :person + d.after_build do |delete| + delete.diaspora_handle= delete.person.diaspora_handle + end +end + Factory.define :searchable_person, :parent => :person do |p| p.after_build do |person| person.profile = Factory.build(:profile, :person => person, :searchable => true) @@ -166,3 +182,22 @@ end Factory.define(:oauth_access_token, :class => OAuth2::Provider.access_token_class) do |a| a.association(:authorization, :factory => :oauth_authorization) end + +Factory.define(:tag, :class => ActsAsTaggableOn::Tag) do |t| + t.name "partytimeexcellent" +end + +Factory.define(:tag_following) do |a| + a.association(:tag, :factory => :tag) + a.association(:user, :factory => :user) +end + +Factory.define(:contact) do |c| + c.association(:person, :factory => :person) + c.association(:user, :factory => :user) +end + +Factory.define(:mention) do |c| + c.association(:person, :factory => :person) + c.association(:post, :factory => :status_message) +end diff --git a/spec/helper_methods.rb b/spec/helper_methods.rb index 4c2fa3196..471526452 100644 --- a/spec/helper_methods.rb +++ b/spec/helper_methods.rb @@ -61,4 +61,15 @@ module HelperMethods fixture_name = File.join(File.dirname(__FILE__), 'fixtures', fixture_filename) File.open(fixture_name) end + + def create_conversation_with_message(sender, recipient_person, subject, text) + create_hash = { + :author => sender.person, + :participant_ids => [sender.person.id, recipient_person.id], + :subject => subject, + :messages_attributes => [ {:author => sender.person, :text => text} ] + } + + Conversation.create!(create_hash) + end end diff --git a/spec/integration/account_deletion_spec.rb b/spec/integration/account_deletion_spec.rb new file mode 100644 index 000000000..ae350f1f5 --- /dev/null +++ b/spec/integration/account_deletion_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe 'deleteing your account' do + context "user" do + before do + @bob2 = bob + @person = @bob2.person + @alices_post = alice.post(:status_message, :text => "@{@bob2 Grimn; #{@bob2.person.diaspora_handle}} you are silly", :to => alice.aspects.find_by_name('generic')) + + @bobs_contact_ids = @bob2.contacts.map {|c| c.id} + + #@bob2's own content + @bob2.post(:status_message, :text => 'asldkfjs', :to => @bob2.aspects.first) + f = Factory(:photo, :author => @bob2.person) + + @aspect_vis = AspectVisibility.where(:aspect_id => @bob2.aspects.map(&:id)) + + #objects on post + @bob2.like(true, :target => @alices_post) + @bob2.comment("here are some thoughts on your post", :post => @alices_post) + + #conversations + create_conversation_with_message(alice, @bob2.person, "Subject", "Hey @bob2") + + #join tables + @users_sv = ShareVisibility.where(:contact_id => @bobs_contact_ids).all + @persons_sv = ShareVisibility.where(:contact_id => bob.person.contacts.map(&:id)).all + + #user associated objects + @prefs = [] + %w{mentioned liked reshared}.each do |pref| + @prefs << @bob2.user_preferences.create!(:email_type => pref) + end + + # notifications + @notifications = [] + 3.times do |n| + @notifications << Factory(:notification, :recipient => @bob2) + end + + # services + @services = [] + 3.times do |n| + @services << Factory(:service, :user => @bob2) + end + + # block + @block = @bob2.blocks.create!(:person => eve.person) + + #authorization + @authorization = Factory.create(:oauth_authorization, :resource_owner => @bob2) + + AccountDeleter.new(@bob2.person.diaspora_handle).perform! + @bob2.reload + end + + it "deletes all of the user's preferences" do + UserPreference.where(:id => @prefs.map{|pref| pref.id}).should be_empty + end + + it "deletes all of the user's notifications" do + Notification.where(:id => @notifications.map{|n| n.id}).should be_empty + end + + it "deletes all of the users's blocked users" do + Block.where(:id => @block.id).should be_empty + end + + it "deletes all of the user's services" do + Service.where(:id => @services.map{|s| s.id}).should be_empty + end + + it 'deletes all of @bob2s share visiblites' do + ShareVisibility.where(:id => @users_sv.map{|sv| sv.id}).should be_empty + ShareVisibility.where(:id => @persons_sv.map{|sv| sv.id}).should be_empty + end + + it 'deletes all of @bob2s aspect visiblites' do + AspectVisibility.where(:id => @aspect_vis.map(&:id)).should be_empty + end + + it 'deletes all aspects' do + @bob2.aspects.should be_empty + end + + it 'deletes all user contacts' do + @bob2.contacts.should be_empty + end + + it 'deletes all the authorizations' do + OAuth2::Provider.authorization_class.where(:id => @authorization.id).should be_empty + end + + it "clears the account fields" do + @bob2.send(:clearable_fields).each do |field| + @bob2.reload[field].should be_blank + end + end + + it_should_behave_like 'it removes the person associations' + end + + context 'remote person' do + before do + @person = remote_raphael + + #contacts + @contacts = @person.contacts + + #posts + @posts = (1..3).map do + Factory.create(:status_message, :author => @person) + end + + @persons_sv = @posts.each do |post| + @contacts.each do |contact| + ShareVisibility.create!(:contact_id => contact.id, :shareable => post) + end + end + + #photos + @photo = Factory(:photo, :author => @person) + + #mentions + @mentions = 3.times do + Factory.create(:mention, :person => @person) + end + + #conversations + create_conversation_with_message(alice, @person, "Subject", "Hey @bob2") + + AccountDeleter.new(@person.diaspora_handle).perform! + @person.reload + end + + it_should_behave_like 'it removes the person associations' + end +end diff --git a/spec/misc_spec.rb b/spec/misc_spec.rb index 1c5ce9edb..642ccc477 100644 --- a/spec/misc_spec.rb +++ b/spec/misc_spec.rb @@ -61,4 +61,22 @@ describe 'making sure the spec runner works' do alice.comment "yo", :post => person_status end end + + describe '#post' do + it 'creates a notification with a mention' do + lambda{ + alice.post(:status_message, :text => "@{Bob Grimn; #{bob.person.diaspora_handle}} you are silly", :to => alice.aspects.find_by_name('generic')) + }.should change(Notification, :count).by(1) + end + end + + describe "#create_conversation_with_message" do + it 'creates a conversation and a message' do + conversation = create_conversation_with_message(alice, bob.person, "Subject", "Hey Bob") + + conversation.participants.should == [alice.person, bob.person] + conversation.subject.should == "Subject" + conversation.messages.first.text.should == "Hey Bob" + end + end end diff --git a/spec/models/account_deleter_spec.rb b/spec/models/account_deleter_spec.rb new file mode 100644 index 000000000..d8137fa97 --- /dev/null +++ b/spec/models/account_deleter_spec.rb @@ -0,0 +1,183 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe AccountDeleter do + before do + @account_deletion = AccountDeleter.new(bob.person.diaspora_handle) + @account_deletion.user = bob + end + + it "attaches the user" do + AccountDeleter.new(bob.person.diaspora_handle).user.should == bob + AccountDeleter.new(remote_raphael.diaspora_handle).user.should == nil + end + + describe '#perform' do + + + user_removal_methods = [:delete_standard_user_associations, + :disassociate_invitations, + :remove_share_visibilities_on_contacts_posts, + :disconnect_contacts, + :tombstone_user] + + person_removal_methods = [:delete_contacts_of_me, + :delete_standard_person_associations, + :tombstone_person_and_profile, + :remove_share_visibilities_on_persons_posts, + :remove_conversation_visibilities] + + context "user deletion" do + after do + @account_deletion.perform! + end + + (user_removal_methods + person_removal_methods).each do |method| + + it "calls ##{method.to_s}" do + @account_deletion.should_receive(method) + end + end + end + + context "person deletion" do + before do + @person_deletion = AccountDeleter.new(remote_raphael.diaspora_handle) + end + + after do + @person_deletion.perform! + end + + (user_removal_methods).each do |method| + + it "does not call ##{method.to_s}" do + @person_deletion.should_not_receive(method) + end + end + + (person_removal_methods).each do |method| + + it "calls ##{method.to_s}" do + @person_deletion.should_receive(method) + end + 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| + association_mock = mock + association_mock.should_receive(:delete) + bob.should_receive(asso).and_return([association_mock]) + end + + @account_deletion.delete_standard_user_associations + end + end + + describe "#delete_standard_person_associations" do + before do + @account_deletion.person = bob.person + end + it 'removes all standard person associaltions' do + @account_deletion.normal_ar_person_associates_to_delete.each do |asso| + association_mock = mock + association_mock.should_receive(:delete_all) + bob.person.should_receive(asso).and_return(association_mock) + end + + @account_deletion.delete_standard_person_associations + end + end + + describe "#disassociate_invitations" do + it "sets invitations_from_me to be admin invitations" do + invites = [mock] + bob.stub(:invitations_from_me).and_return(invites) + invites.first.should_receive(:convert_to_admin!) + @account_deletion.disassociate_invitations + end + end + + context 'person associations' do + describe '#disconnect_contacts' do + it "deletes all of user's contacts" do + bob.contacts.should_receive(:destroy_all) + @account_deletion.disconnect_contacts + end + end + + describe '#delete_contacts_of_me' do + it 'deletes all the local contact objects where deleted account is the person' do + contacts = mock + Contact.should_receive(:all_contacts_of_person).with(bob.person).and_return(contacts) + contacts.should_receive(:destroy_all) + @account_deletion.delete_contacts_of_me + end + end + + describe '#tombstone_person_and_profile' do + it 'calls clear_profile! on person' do + @account_deletion.person.should_receive(:clear_profile!) + @account_deletion.tombstone_person_and_profile + end + + it 'calls lock_access! on person' do + @account_deletion.person.should_receive(:lock_access!) + @account_deletion.tombstone_person_and_profile + end + end + describe "#remove_conversation_visibilities" do + it "removes the conversation visibility for the deleted user" do + vis = stub + ConversationVisibility.should_receive(:where).with(hash_including(:person_id => bob.person.id)).and_return(vis) + vis.should_receive(:destroy_all) + @account_deletion.remove_conversation_visibilities + end + end + end + + describe "#remove_person_share_visibilities" do + it 'removes the share visibilities for a person ' do + @s_vis = stub + ShareVisibility.should_receive(:for_contacts_of_a_person).with(bob.person).and_return(@s_vis) + @s_vis.should_receive(:destroy_all) + + @account_deletion.remove_share_visibilities_on_persons_posts + end + end + + describe "#remove_share_visibilities_by_contacts_of_user" do + it 'removes the share visibilities for a user' do + @s_vis = stub + ShareVisibility.should_receive(:for_a_users_contacts).with(bob).and_return(@s_vis) + @s_vis.should_receive(:destroy_all) + + @account_deletion.remove_share_visibilities_on_contacts_posts + end + end + + describe "#tombstone_user" do + it 'calls strip_model on user' do + bob.should_receive(:clear_account!) + @account_deletion.tombstone_user + end + end + + it 'has all user association keys accounted for' do + all_keys = (@account_deletion.normal_ar_user_associates_to_delete + @account_deletion.special_ar_user_associations + @account_deletion.ignored_ar_user_associations) + all_keys.sort{|x, y| x.to_s <=> y.to_s}.should == User.reflections.keys.sort{|x, y| x.to_s <=> y.to_s} + end + + it 'has all person association keys accounted for' do + all_keys = (@account_deletion.normal_ar_person_associates_to_delete + @account_deletion.ignored_or_special_ar_person_associations) + all_keys.sort{|x, y| x.to_s <=> y.to_s}.should == Person.reflections.keys.sort{|x, y| x.to_s <=> y.to_s} + end +end + diff --git a/spec/models/account_deletion_spec.rb b/spec/models/account_deletion_spec.rb new file mode 100644 index 000000000..5bd96f79f --- /dev/null +++ b/spec/models/account_deletion_spec.rb @@ -0,0 +1,86 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe AccountDeletion do + it 'assigns the diaspora_handle from the person object' do + a = AccountDeletion.new(:person => alice.person) + a.diaspora_handle.should == alice.person.diaspora_handle + end + + it 'fires a resque job after creation'do + Resque.should_receive(:enqueue).with(Jobs::DeleteAccount, anything) + + AccountDeletion.create(:person => alice.person) + end + + describe "#perform!" do + before do + @ad = AccountDeletion.new(:person => alice.person) + end + + it 'creates a deleter' do + AccountDeleter.should_receive(:new).with(alice.person.diaspora_handle).and_return(stub(:perform! => true)) + @ad.perform! + end + + it 'dispatches the account deletion if the user exists' do + @ad.should_receive(:dispatch) + @ad.perform! + end + + it 'does not dispatch an account deletion for non-local people' do + deletion = AccountDeletion.new(:person => remote_raphael) + deletion.should_not_receive(:dispatch) + deletion.perform! + end + end + + describe '#dispatch' do + it "sends the account deletion xml" do + @ad = AccountDeletion.new(:person => alice.person) + @ad.send(:dispatch) + end + + it 'creates a public postzord' do + Postzord::Dispatcher::Public.should_receive(:new).and_return(stub.as_null_object) + @ad = AccountDeletion.new(:person => alice.person) + @ad.send(:dispatch) + end + end + + describe "#subscribers" do + it 'includes all remote contacts' do + @ad = AccountDeletion.new(:person => alice.person) + alice.share_with(remote_raphael, alice.aspects.first) + + @ad.subscribers(alice).should == [remote_raphael] + end + + it 'includes remote resharers' do + @ad = AccountDeletion.new(:person => alice.person) + sm = Factory( :status_message, :public => true, :author => alice.person) + r1 = Factory( :reshare, :author => remote_raphael, :root => sm) + r2 = Factory( :reshare, :author => local_luke.person, :root => sm) + + @ad.subscribers(alice).should == [remote_raphael] + end + end + + describe 'serialization' do + before do + account_deletion = AccountDeletion.new(:person => alice.person) + @xml = account_deletion.to_xml.to_s + end + + it 'should have a diaspora_handle' do + @xml.include?(alice.person.diaspora_handle).should == true + end + + it 'marshals the xml' do + AccountDeletion.from_xml(@xml).should be_valid + end + end +end diff --git a/spec/models/contact_spec.rb b/spec/models/contact_spec.rb index 1a3c507aa..d93b17188 100644 --- a/spec/models/contact_spec.rb +++ b/spec/models/contact_spec.rb @@ -44,6 +44,15 @@ describe Contact do contact.person = person contact.should_not be_valid end + + it "validates that the person's account is not closed" do + person = Factory.create(:person, :closed_account => true) + + contact = alice.contacts.new(:person=>person) + + contact.should_not be_valid + contact.errors.full_messages.should include "Cannot be in contact with a closed account" + end end context 'scope' do @@ -81,6 +90,16 @@ describe Contact do }.by(2) end end + + describe "all_contacts_of_person" do + it 'returns all contacts where the person is the passed in person' do + person = Factory.create(:person) + contact1 = Factory(:contact, :person => person) + contact2 = Factory(:contact) + contacts = Contact.all_contacts_of_person(person) + contacts.should == [contact1] + end + end end describe '#contacts' do diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 4681ba9c5..ad19d8b59 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -75,6 +75,17 @@ describe Invitation do }.should_not change(User, :count) end end + + describe '#convert_to_admin!' do + it 'reset sender and aspect to nil, and sets admin flag to true' do + invite = Factory(:invitation) + invite.convert_to_admin! + invite.reload + invite.admin?.should be_true + invite.sender_id.should be_nil + invite.aspect_id.should be_nil + end + end describe '.batch_invite' do before do diff --git a/spec/models/jobs/delete_account_spec.rb b/spec/models/jobs/delete_account_spec.rb index 0bf04b5c1..507f0d2b9 100644 --- a/spec/models/jobs/delete_account_spec.rb +++ b/spec/models/jobs/delete_account_spec.rb @@ -6,25 +6,12 @@ require 'spec_helper' describe Jobs::DeleteAccount do describe '#perform' do - it 'calls remove_all_traces' do - stub_find_for(bob) - bob.should_receive(:remove_all_traces) - Jobs::DeleteAccount.perform(bob.id) - end - - it 'calls destroy' do - stub_find_for(bob) - bob.should_receive(:destroy) - Jobs::DeleteAccount.perform(bob.id) - end - def stub_find_for model - model.class.stub!(:find) do |id, conditions| - if id == model.id - model - else - model.class.find_by_id(id) - end - end + it 'performs the account deletion' do + account_deletion = stub + AccountDeletion.stub(:find).and_return(account_deletion) + account_deletion.should_receive(:perform!) + + Jobs::DeleteAccount.perform(1) end end end diff --git a/spec/models/person_spec.rb b/spec/models/person_spec.rb index 48d5f515d..f37f80c51 100644 --- a/spec/models/person_spec.rb +++ b/spec/models/person_spec.rb @@ -91,6 +91,14 @@ describe Person do Person.all_from_aspects(aspect_ids, bob).map(&:id).should == [] end end + + describe ".who_have_reshared a user's posts" do + it 'pulls back users who reshared the status message of a user' do + sm = Factory.create(:status_message, :author => alice.person, :public => true) + reshare = Factory.create(:reshare, :root => sm) + Person.who_have_reshared_a_users_posts(alice).should == [reshare.author] + end + end end describe "delegating" do @@ -536,4 +544,22 @@ describe Person do end end end + + describe '#lock_access!' do + it 'sets the closed_account flag' do + @person.lock_access! + @person.reload.closed_account.should be_true + end + end + + describe "#clear_profile!!" do + before do + @person = Factory(:person) + end + + it 'calls Profile#tombstone!' do + @person.profile.should_receive(:tombstone!) + @person.clear_profile! + end + end end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index c93984dcb..2905c6b05 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -264,7 +264,6 @@ describe Profile do end describe '#receive' do - it 'updates the profile in place' do local_luke, local_leia, remote_raphael = set_up_friends new_profile = Factory.build :profile @@ -275,4 +274,43 @@ describe Profile do end end + + describe "#tombstone!" do + before do + @profile = bob.person.profile + end + it "clears the profile fields" do + attributes = @profile.send(:clearable_fields) + + @profile.tombstone! + @profile.reload + attributes.each{ |attr| + @profile[attr.to_sym].should be_blank + } + end + + it 'removes all the tags from the profile' do + @profile.taggings.should_receive(:delete_all) + @profile.tombstone! + end + end + + describe "#clearable_fields" do + it 'returns the current profile fields' do + profile = Factory.build :profile + profile.send(:clearable_fields).sort.should == + ["diaspora_handle", + "first_name", + "last_name", + "image_url", + "image_url_small", + "image_url_medium", + "birthday", + "gender", + "bio", + "searchable", + "location", + "full_name"].sort + end + end end diff --git a/spec/models/share_visibility_spec.rb b/spec/models/share_visibility_spec.rb index 805c1bbdb..4c72adf21 100644 --- a/spec/models/share_visibility_spec.rb +++ b/spec/models/share_visibility_spec.rb @@ -25,5 +25,28 @@ describe ShareVisibility do ShareVisibility.batch_import([@contact.id], @post) }.should_not raise_error end + + context "scopes" do + describe '.for_a_users_contacts' do + before do + alice.post(:status_message, :text => "Hey", :to => alice.aspects.first) + end + + it 'searches for share visibilies for all users contacts' do + contact_ids = alice.contacts.map{|c| c.id} + ShareVisibility.for_a_users_contacts(alice).should == ShareVisibility.where(:contact_id => contact_ids).all + end + end + + describe '.for_contacts_of_a_person' do + it 'searches for share visibilties generated by a person' do + + contact_ids = alice.person.contacts.map{|c| c.id} + + ShareVisibility.for_contacts_of_a_person(alice.person) == ShareVisibility.where(:contact_id => contact_ids).all + + end + end + end end end diff --git a/spec/models/status_message_spec.rb b/spec/models/status_message_spec.rb index 9c3450249..8202c021b 100644 --- a/spec/models/status_message_spec.rb +++ b/spec/models/status_message_spec.rb @@ -66,6 +66,15 @@ describe StatusMessage do end end + describe ".guids_for_author" do + it 'returns an array of the status_message guids' do + sm1 = Factory(:status_message, :author => alice.person) + sm2 = Factory(:status_message, :author => bob.person) + guids = StatusMessage.guids_for_author(alice.person) + guids.should == [sm1.guid] + end + end + describe '.before_create' do it 'calls build_tags' do status = Factory.build(:status_message) diff --git a/spec/models/tag_following_spec.rb b/spec/models/tag_following_spec.rb index 3b1614dfa..611c4fee3 100644 --- a/spec/models/tag_following_spec.rb +++ b/spec/models/tag_following_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe TagFollowing do before do - @tag = ActsAsTaggableOn::Tag.create(:name => "partytimeexcellent") + @tag = Factory.create(:tag) TagFollowing.create!(:tag => @tag, :user => alice) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 431a745be..a10ca23cf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -625,7 +625,8 @@ describe User do describe '#disconnect_everyone' do it 'has no error on a local friend who has deleted his account' do - Jobs::DeleteAccount.perform(alice.id) + d = Factory(:account_deletion, :person => alice.person) + Jobs::DeleteAccount.perform(d.id) lambda { bob.disconnect_everyone }.should_not raise_error @@ -907,6 +908,7 @@ describe User do fantasy_resque do @invitation = Factory.create(:invitation, :sender => eve, :identifier => 'invitee@example.org', :aspect => eve.aspects.first) end + @invitation.reload @form_params = { :invitation_token => "abc", @@ -1005,4 +1007,79 @@ describe User do user.send_reset_password_instructions end end + + context "close account" do + before do + @user = bob + end + + describe "#close_account!" do + it 'locks the user out' do + @user.close_account! + @user.reload.access_locked?.should be_true + end + + it 'creates an account deletion' do + expect{ + @user.close_account! + }.to change(AccountDeletion, :count).by(1) + end + + it 'calls person#lock_access!' do + @user.person.should_receive(:lock_access!) + @user.close_account! + end + end + + describe "#clear_account!" do + it 'resets the password to a random string' do + random_pass = "12345678909876543210" + ActiveSupport::SecureRandom.should_receive(:hex).and_return(random_pass) + @user.clear_account! + @user.valid_password?(random_pass) + end + + it 'clears all the clearable fields' do + @user.reload + attributes = @user.send(:clearable_fields) + @user.clear_account! + + @user.reload + attributes.each do |attr| + @user.send(attr.to_sym).should be_blank + end + end + end + + describe "#clearable_attributes" do + it 'returns the clearable fields' do + user = Factory.create :user + user.send(:clearable_fields).sort.should == %w{ + getting_started + disable_mail + language + email + invitation_token + invitation_sent_at + reset_password_token + remember_token + remember_created_at + sign_in_count + current_sign_in_at + last_sign_in_at + current_sign_in_ip + last_sign_in_ip + invitation_service + invitation_identifier + invitation_limit + invited_by_id + invited_by_type + authentication_token + unconfirmed_email + confirm_email_token + show_community_spotlight_in_stream + }.sort + end + end + end end diff --git a/spec/shared_behaviors/account_deletion.rb b/spec/shared_behaviors/account_deletion.rb new file mode 100644 index 000000000..d685432e6 --- /dev/null +++ b/spec/shared_behaviors/account_deletion.rb @@ -0,0 +1,42 @@ +# Copyright (c) 2010-2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe 'deleteing your account' do + shared_examples_for 'it removes the person associations' do + it "removes all of the person's posts" do + Post.where(:author_id => @person.id).count.should == 0 + end + + it 'deletes all person contacts' do + Contact.where(:person_id => @person.id).should be_empty + end + + it 'deletes all mentions' do + @person.mentions.should be_empty + end + + it "removes all of the person's photos" do + Photo.where(:author_id => @person.id).should be_empty + end + + it 'sets the person object as closed and the profile is cleared' do + @person.reload.closed_account.should be_true + + @person.profile.reload.first_name.should be_blank + @person.profile.reload.last_name.should be_blank + end + + it 'deletes only the converersation visibility for the deleted user' do + ConversationVisibility.where(:person_id => alice.person.id).should_not be_empty + ConversationVisibility.where(:person_id => @person.id).should be_empty + end + + it "deletes the share visibilities on the person's posts" do + ShareVisibility.for_contacts_of_a_person(@person).should be_empty + end + end +end +