Merge branch 'close_account_rework'

This commit is contained in:
Maxwell Salzberg 2011-12-08 16:34:25 -08:00
commit dea01600aa
41 changed files with 1126 additions and 71 deletions

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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!

View file

@ -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

View file

@ -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

View file

@ -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] == '/'

View file

@ -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<Contact>] Recipients

View file

@ -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)

View file

@ -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

View file

@ -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')

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -2281,6 +2281,9 @@ ul.show_comments,
:position relative
:top 10px
#inner_account_delete
:width 810px
#aspect_edit_pane
:width 810px
.person_tiles

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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