Users Controller and unit tests complete

This commit is contained in:
Hank Grabowski 2018-11-21 16:47:00 -05:00
parent a5bdabea9b
commit c3852a8e9c
11 changed files with 747 additions and 2 deletions

View file

@ -0,0 +1,85 @@
# frozen_string_literal: true
module Api
module V1
class UsersController < Api::V1::BaseController
include TagsHelper
before_action except: %i[update] do
require_access_token %w[read]
end
before_action only: %i[update] do
require_access_token %w[write]
end
rescue_from ActiveRecord::RecordNotFound do
render json: I18n.t("api.endpoint_errors.users.not_found"), status: :not_found
end
def show
person = if params.has_key?(:id)
Person.find_by!(guid: params[:id])
else
current_user.person
end
render json: PersonPresenter.new(person, current_user).profile_hash_as_api_json
end
def update
raise RuntimeError if params.has_key?(:id)
params_to_update = profile_update_params
if params_to_update && current_user.update_profile(params_to_update)
render json: PersonPresenter.new(current_user.person, current_user).profile_hash_as_api_json
else
render json: I18n.t("api.endpoint_errors.users.cant_update"), status: :unprocessable_entity
end
rescue RuntimeError
render json: I18n.t("api.endpoint_errors.users.cant_update"), status: :unprocessable_entity
end
def contacts
if params[:user_id] != current_user.guid
render json: I18n.t("api.endpoint_errors.users.not_found"), status: :not_found
return
end
contacts_with_profile = AspectsMembershipService.new(current_user).all_contacts
render json: contacts_with_profile.map {|c| PersonPresenter.new(c.person).as_api_json }
end
def photos
person = Person.find_by!(guid: params[:user_id])
photos = Photo.visible(current_user, person, :all, Time.current)
render json: photos.map {|photo| PhotoPresenter.new(photo).as_api_json(false) }
end
def posts
person = Person.find_by!(guid: params[:user_id])
posts = current_user.posts_from(person)
render json: posts.map {|post| PostPresenter.new(post, current_user).as_api_response }
end
private
def profile_update_params
updates = params.permit(:bio, :birthday, :gender, :location, :first_name, :last_name,
:searchable, :show_profile_info, :nsfw, :tags).to_h || {}
if updates.has_key?(:show_profile_info)
updates[:public_details] = updates[:show_profile_info]
updates.delete(:show_profile_info)
end
process_tags_updates(updates)
updates
end
def process_tags_updates(updates)
return unless params.has_key?(:tags)
raise RuntimeError if params[:tags].length > Profile::MAX_TAGS
tags = params[:tags].map {|tag| "#" + normalize_tag_name(tag) }.join(" ")
updates[:tag_string] = tags
updates.delete(:tags)
end
end
end
end

View file

@ -5,6 +5,7 @@
# the COPYRIGHT file.
class Profile < ApplicationRecord
MAX_TAGS = 5
self.include_root_in_json = false
include Diaspora::Federated::Base

View file

@ -28,6 +28,20 @@ class PersonPresenter < BasePresenter
)
end
def profile_hash_as_api_json
if own_profile?
ProfilePresenter.new(profile).as_self_api_json.merge(guid: guid)
else
show_detailed = @presentable.public_details? || person_is_following_current_user
ProfilePresenter.new(profile).as_other_api_json(show_detailed).merge(
guid: guid,
blocked: is_blocked?,
relationship: relationship_detailed,
aspects: aspects_detailed
)
end
end
def as_json(_options={})
full_hash_with_profile
end
@ -72,6 +86,25 @@ class PersonPresenter < BasePresenter
end || :not_sharing
end
def relationship_detailed
status = {receiving: false, sharing: false}
return status unless current_user
contact = current_user_person_contact
return status unless contact
status.keys.each do |key|
status[key] = contact.public_send("#{key}?")
end
status
end
def aspects_detailed
return [] unless current_user_person_contact
aspects_for_person = current_user.aspects_with_person(@presentable)
aspects_for_person.map {|a| AspectPresenter.new(a).as_api_json(false) }
end
def person_is_following_current_user
return false unless current_user
contact = current_user_person_contact

View file

@ -17,8 +17,8 @@ class PhotoPresenter < BasePresenter
}
end
def as_api_json
{
def as_api_json(no_guid=true)
based_data = {
dimensions: {
height: height,
width: width
@ -29,5 +29,7 @@ class PhotoPresenter < BasePresenter
large: url(:scaled_full)
}
}
return based_data if no_guid
based_data.merge(guid: guid)
end
end

View file

@ -24,6 +24,19 @@ class ProfilePresenter < BasePresenter
}
end
def as_self_api_json
base_api_json.merge(added_details_api_json).merge(
searchable: searchable,
show_profile_info: public_details,
nsfw: nsfw
)
end
def as_other_api_json(all_details)
return base_api_json unless all_details
base_api_json.merge(added_details_api_json)
end
def private_hash
public_hash.merge(
bio: bio_message.plain_text_for_json,
@ -36,4 +49,25 @@ class ProfilePresenter < BasePresenter
def formatted_birthday
birthday_format(birthday) if birthday
end
private
def base_api_json
{
first_name: first_name,
last_name: last_name,
diaspora_id: diaspora_handle,
avatar: AvatarPresenter.new(@presentable).base_hash,
tags: tags.pluck(:name)
}
end
def added_details_api_json
{
birthday: formatted_birthday,
gender: gender,
location: location_message.plain_text_for_json,
bio: bio_message.plain_text_for_json
}
end
end

View file

@ -43,6 +43,12 @@ class AspectsMembershipService
).includes(person: :profile).order(order)
end
def all_contacts
order = ["profiles.first_name ASC", "profiles.last_name ASC",
"profiles.diaspora_handle ASC"]
@user.contacts.includes(person: :profile).order(order)
end
private
def destroy(aspect, contact)

View file

@ -982,6 +982,9 @@ en:
failed_create: "Failed to create the post"
failed_delete: "Not allowed to delete the post"
cant_report: "Failed to create report on this post"
users:
cant_update: "Failed to update the user settings"
not_found: "User not found"
tags:
cant_process: "Failed to process the tag followings request"

View file

@ -240,6 +240,14 @@ Rails.application.routes.draw do
resources :messages, only: %i[index create]
end
resources :notifications, only: %i[index show update]
patch "user" => "users#update"
get "user" => "users#show"
resources :users, only: %i[show] do
get :contacts
get :photos
get :posts
end
resources :tag_followings, only: %i[index create destroy]
get "streams/activity" => "streams#activity", :as => "activity_stream"
get "streams/main" => "streams#multi", :as => "stream"

View file

@ -0,0 +1,486 @@
# frozen_sTring_literal: true
require "spec_helper"
describe Api::V1::UsersController do
include PeopleHelper
let(:auth) { FactoryGirl.create(:auth_with_read_and_write) }
let(:auth_read_only) { FactoryGirl.create(:auth_with_read) }
let!(:access_token) { auth.create_access_token.to_s }
let!(:access_token_read_only) { auth_read_only.create_access_token.to_s }
describe "#show" do
context "Current User" do
it "succeeds when logged in" do
get(
api_v1_user_path,
params: {access_token: access_token}
)
user = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(user["guid"]).to eq(auth.user.guid)
confirm_self_data_format(user)
end
it "fails if invalid token" do
get(
api_v1_user_path,
params: {access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
end
context "Single User" do
it "succeeds with public user not in Aspect" do
alice.profile[:public_details] = true
alice.profile.save
get(
"/api/v1/users/#{alice.guid}",
params: {access_token: access_token}
)
user = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(user["guid"]).to eq(alice.person.guid)
confirm_public_profile_hash(user)
end
it "succeeds with in Aspect valid user" do
alice.profile[:public_details] = true
alice.profile.save
auth.user.share_with(alice.person, auth.user.aspects.first)
get(
"/api/v1/users/#{alice.guid}",
params: {access_token: access_token}
)
user = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(user["guid"]).to eq(alice.person.guid)
confirm_public_profile_hash(user)
end
it "succeeds with limited data on non-public/not shared" do
eve.profile[:public_details] = false
eve.profile.save
get(
"/api/v1/users/#{eve.guid}",
params: {access_token: access_token}
)
user = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(user["guid"]).to eq(eve.person.guid)
confirm_private_profile_hash(user)
eve.aspects.create(name: "new aspect")
eve.share_with(auth.user.person, eve.aspects.first)
get(
"/api/v1/users/#{eve.guid}",
params: {access_token: access_token}
)
user = JSON.parse(response.body)
expect(response.status).to eq(200)
expect(user["guid"]).to eq(eve.person.guid)
confirm_public_profile_hash(user)
end
it "fails if invalid token" do
get(
api_v1_user_path(alice.person.guid),
params: {access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
it "fails with invalid user GUID" do
get(
"/api/v1/users/999_999_999",
params: {access_token: access_token}
)
expect(response.status).to eq(404)
expect(response.body).to eq(I18n.t("api.endpoint_errors.users.not_found"))
end
end
end
describe "#update" do
context "Partial with all valid fields" do
it "succeeds when logged in" do
new_location = "New Location"
new_bio = "New Bio"
patch(
api_v1_user_path,
params: {location: new_location, bio: new_bio, access_token: access_token}
)
expect(response.status).to eq(200)
user = JSON.parse(response.body)
confirm_self_data_format(user)
expect(user["bio"]).to eq(new_bio)
expect(user["location"]).to eq(new_location)
auth.user.profile.reload
expect(auth.user.profile[:location]).to eq(new_location)
expect(auth.user.profile[:bio]).to eq(new_bio)
end
end
context "Full with all valid fields" do
it "succeeds" do
new_bio = "new bio"
new_birthday = Time.current + 8_640_000
new_gender = "ask1"
new_location = "new location"
new_first_name = "new first"
new_last_name = "new last"
new_searchable = !auth.user.profile[:searchable]
new_show_profile_info = !auth.user.profile[:public_details]
new_nsfw = !auth.user.profile[:nsfw]
new_tags = %w[tag1 tag2]
patch(
api_v1_user_path,
params: {
bio: new_bio,
location: new_location,
gender: new_gender,
birthday: new_birthday,
first_name: new_first_name,
last_name: new_last_name,
searchable: new_searchable,
show_profile_info: new_show_profile_info,
nsfw: new_nsfw,
tags: new_tags,
access_token: access_token
}
)
expect(response.status).to eq(200)
user = JSON.parse(response.body)
confirm_self_data_format(user)
expect(user["bio"]).to eq(new_bio)
expect(user["birthday"]).to eq(birthday_format(new_birthday))
expect(user["location"]).to eq(new_location)
expect(user["gender"]).to eq(new_gender)
expect(user["first_name"]).to eq(new_first_name)
expect(user["last_name"]).to eq(new_last_name)
expect(user["searchable"]).to eq(new_searchable)
expect(user["show_profile_info"]).to eq(new_show_profile_info)
expect(user["nsfw"]).to eq(new_nsfw)
expect(user["tags"]).to eq(new_tags)
auth.user.profile.reload
expect(auth.user.profile[:location]).to eq(new_location)
expect(auth.user.profile[:bio]).to eq(new_bio)
expect(birthday_format(auth.user.profile[:birthday])).to eq(birthday_format(new_birthday))
expect(auth.user.profile[:gender]).to eq(new_gender)
expect(auth.user.profile[:first_name]).to eq(new_first_name)
expect(auth.user.profile[:last_name]).to eq(new_last_name)
expect(auth.user.profile[:searchable]).to eq(new_searchable)
expect(auth.user.profile[:public_details]).to eq(new_show_profile_info)
expect(auth.user.profile[:nsfw]).to eq(new_nsfw)
expect(user["tags"]).to eq(new_tags)
end
end
context "fails" do
it "skips invalid fields" do
new_bio = "new bio"
patch(
api_v1_user_path,
params: {
bio: new_bio,
no_idea_what_field_this_is: "some value",
access_token: access_token
}
)
expect(response.status).to eq(200)
user = JSON.parse(response.body)
expect(user["bio"]).to eq(new_bio)
expect(user.has_key?("no_idea_what_field_this_is")).to be_falsey
end
it "fails to update guid" do
new_bio = "new bio"
original_guid = auth.user.guid.to_s
patch(
api_v1_user_path,
params: {
bio: new_bio,
guid: "999_999_999",
access_token: access_token
}
)
expect(response.status).to eq(200)
user = JSON.parse(response.body)
expect(user["bio"]).to eq(new_bio)
expect(user["guid"]).to eq(original_guid)
end
it "fails if invalid token" do
patch(
api_v1_user_path,
params: {location: "New Location", bio: "New Bio", access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
it "fails if read only token" do
patch(
api_v1_user_path,
params: {location: "New Location", bio: "New Bio", access_token: access_token_read_only}
)
expect(response.status).to eq(403)
end
end
end
describe "#contacts" do
it "succeeds when logged in and ask for own" do
get(
api_v1_user_contacts_path(auth.user.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
contacts = JSON.parse(response.body)
expect(contacts.length).to eq(0)
auth.user.share_with(alice.person, auth.user.aspects.first)
get(
api_v1_user_contacts_path(auth.user.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
contacts = JSON.parse(response.body)
expect(contacts.length).to eq(1)
confirm_person_format(contacts[0], alice)
end
it "fails with invalid GUID" do
get(
api_v1_user_contacts_path("999_999_999"),
params: {access_token: access_token}
)
expect(response.status).to eq(404)
expect(response.body).to eq(I18n.t("api.endpoint_errors.users.not_found"))
end
it "fails with other user's GUID" do
get(
api_v1_user_contacts_path(alice.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(404)
expect(response.body).to eq(I18n.t("api.endpoint_errors.users.not_found"))
end
it "fails if invalid token" do
get(
api_v1_user_contacts_path(alice.guid),
params: {access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
end
describe "#photos" do
before do
alice_private_spec = alice.aspects.create(name: "private aspect")
alice.share_with(eve.person, alice_private_spec)
alice.share_with(auth.user.person, alice.aspects.first)
auth.user.post(:photo, pending: false, user_file: File.open(photo_fixture_name), to: "all")
auth.user.post(:photo, pending: false, user_file: File.open(photo_fixture_name), to: "all")
@public_photo1 = alice.post(:photo, pending: false, user_file: File.open(photo_fixture_name), to: "all")
@public_photo2 = alice.post(:photo, pending: false, user_file: File.open(photo_fixture_name), to: "all")
@shared_photo1 = alice.post(:photo, pending: false, user_file: File.open(photo_fixture_name),
to: alice.aspects.first.id)
@private_photo1 = alice.post(:photo, pending: false, user_file: File.open(photo_fixture_name),
to: alice_private_spec.id)
end
context "logged in" do
it "returns only visible photos of other user" do
get(
api_v1_user_photos_path(alice.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
photos = JSON.parse(response.body)
expect(photos.length).to eq(3)
guids = photos.map {|photo| photo["guid"] }
expect(guids).to include(@public_photo1.guid, @public_photo2.guid, @shared_photo1.guid)
expect(guids).not_to include(@private_photo1.guid)
confirm_photos(photos)
end
it "returns logged in user's photos" do
get(
api_v1_user_photos_path(auth.user.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
photos = JSON.parse(response.body)
expect(photos.length).to eq(2)
end
end
it "fails with invalid GUID" do
get(
api_v1_user_photos_path("999_999_999"),
params: {access_token: access_token}
)
expect(response.status).to eq(404)
expect(response.body).to eq(I18n.t("api.endpoint_errors.users.not_found"))
end
it "fails if invalid token" do
get(
api_v1_user_photos_path(alice.guid),
params: {access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
end
describe "#posts" do
before do
alice_private_spec = alice.aspects.create(name: "private aspect")
alice.share_with(eve.person, alice_private_spec)
alice.share_with(auth.user.person, alice.aspects.first)
auth.user.post(:status_message, text: "auth user message1", public: true, to: "all")
auth.user.post(:status_message, text: "auth user message2", public: true, to: "all")
@public_post1 = alice.post(:status_message, text: "alice public message1", public: true, to: "all")
@public_post2 = alice.post(:status_message, text: "alice public message2", public: true, to: "all")
@shared_post1 = alice.post(:status_message, text: "alice limited to auth user message",
public: false, to: alice.aspects.first.id)
@private_post1 = alice.post(:status_message, text: "alice limited hidden from auth user message",
public: false, to: alice_private_spec.id)
end
context "logged in" do
it "returns only visible posts of other user" do
get(
api_v1_user_posts_path(alice.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
posts = JSON.parse(response.body)
expect(posts.length).to eq(3)
guids = posts.map {|post| post["guid"] }
expect(guids).to include(@public_post1.guid, @public_post2.guid, @shared_post1.guid)
expect(guids).not_to include(@private_post1.guid)
post = posts.select {|p| p["guid"] == @public_post1.guid }
confirm_post_format(post[0], alice, @public_post1)
end
it "returns logged in user's photos" do
get(
api_v1_user_posts_path(auth.user.guid),
params: {access_token: access_token}
)
expect(response.status).to eq(200)
posts = JSON.parse(response.body)
expect(posts.length).to eq(2)
end
end
it "fails with invalid GUID" do
get(
api_v1_user_posts_path("999_999_999"),
params: {access_token: access_token}
)
expect(response.status).to eq(404)
expect(response.body).to eq(I18n.t("api.endpoint_errors.users.not_found"))
end
it "fails if invalid token" do
get(
api_v1_user_posts_path(alice.person.guid),
params: {access_token: "999_999_999"}
)
expect(response.status).to eq(401)
end
end
def confirm_self_data_format(json)
confirm_common_profile_elements(json)
confirm_profile_details(json)
expect(json).to have_key("searchable")
expect(json).to have_key("show_profile_info")
end
def confirm_public_profile_hash(json)
confirm_common_profile_elements(json)
confirm_profile_details(json)
expect(json).to have_key("blocked")
expect(json).to have_key("relationship")
expect(json).to have_key("aspects")
end
# rubocop:disable Metrics/AbcSize
def confirm_private_profile_hash(json)
confirm_common_profile_elements(json)
expect(json).to have_key("blocked")
expect(json).to have_key("relationship")
expect(json).to have_key("aspects")
expect(json).not_to have_key("birthday")
expect(json).not_to have_key("gender")
expect(json).not_to have_key("location")
expect(json).not_to have_key("bio")
end
def confirm_common_profile_elements(json)
expect(json).to have_key("guid")
expect(json).to have_key("diaspora_id")
expect(json).to have_key("first_name")
expect(json).to have_key("last_name")
expect(json).to have_key("avatar")
expect(json).to have_key("tags")
end
def confirm_profile_details(json)
expect(json).to have_key("birthday")
expect(json).to have_key("gender")
expect(json).to have_key("location")
expect(json).to have_key("bio")
end
def confirm_post_format(post, user, reference_post)
confirm_post_top_level(post, reference_post)
confirm_person_format(post["author"], user)
confirm_interactions(post["interaction_counters"], reference_post)
end
def confirm_post_top_level(post, reference_post)
expect(post["guid"]).to eq(reference_post.guid)
expect(post.has_key?("created_at")).to be_truthy
expect(post["created_at"]).not_to be_nil
expect(post["title"]).to eq(reference_post.message.title)
expect(post["body"]).to eq(reference_post.message.plain_text_for_json)
expect(post["post_type"]).to eq(reference_post.post_type)
expect(post["provider_display_name"]).to eq(reference_post.provider_display_name)
expect(post["public"]).to eq(reference_post.public)
expect(post["nsfw"]).to eq(reference_post.nsfw)
end
def confirm_interactions(interactions, reference_post)
expect(interactions["comments"]).to eq(reference_post.comments_count)
expect(interactions["likes"]).to eq(reference_post.likes_count)
expect(interactions["reshares"]).to eq(reference_post.reshares_count)
end
def confirm_person_format(post_person, user)
expect(post_person["guid"]).to eq(user.guid)
expect(post_person["diaspora_id"]).to eq(user.diaspora_handle)
expect(post_person["name"]).to eq(user.name)
expect(post_person["avatar"]).to eq(user.profile.image_url)
end
def confirm_photos(photos)
photos.each do |photo|
expect(photo.has_key?("guid")).to be_truthy
expect(photo["dimensions"].has_key?("height")).to be_truthy
expect(photo["sizes"]["small"]).to be_truthy
expect(photo["sizes"]["medium"]).to be_truthy
expect(photo["sizes"]["large"]).to be_truthy
end
end
# rubocop:enable Metrics/AbcSize
end

View file

@ -142,4 +142,78 @@ describe PersonPresenter do
expect(presenter.hovercard[:contact]).to have_key(:aspect_memberships)
end
end
describe "#profile_hash_as_api_json" do
let(:current_user) { FactoryGirl.create(:user) }
it "contains internal profile if self" do
profile_hash = PersonPresenter.new(current_user.person, current_user).profile_hash_as_api_json
expect(profile_hash[:diaspora_id]).to eq(current_user.profile.diaspora_handle)
confirm_self_data_format(profile_hash)
end
it "contains full data only if private profile is Sharing to me" do
alice.profile[:public_details] = false
profile_hash = PersonPresenter.new(alice.person, current_user).profile_hash_as_api_json
expect(profile_hash[:diaspora_id]).to eq(alice.profile.diaspora_handle)
confirm_private_profile_hash(profile_hash)
alice.share_with(current_user.person, alice.aspects.first)
profile_hash = PersonPresenter.new(alice.person, current_user).profile_hash_as_api_json
expect(profile_hash[:diaspora_id]).to eq(alice.profile.diaspora_handle)
confirm_public_profile_hash(profile_hash)
end
it "contains full profile data for public profile" do
alice.profile[:public_details] = true
profile_hash = PersonPresenter.new(alice.person, current_user).profile_hash_as_api_json
expect(profile_hash[:diaspora_id]).to eq(alice.profile.diaspora_handle)
confirm_public_profile_hash(profile_hash)
end
end
def confirm_self_data_format(profile_hash)
confirm_common_profile_elements(profile_hash)
confirm_profile_details(profile_hash)
expect(profile_hash).to have_key(:searchable)
expect(profile_hash).to have_key(:show_profile_info)
expect(profile_hash).to have_key(:nsfw)
end
def confirm_public_profile_hash(profile_hash)
confirm_common_profile_elements(profile_hash)
confirm_profile_details(profile_hash)
expect(profile_hash).to have_key(:blocked)
expect(profile_hash).to have_key(:relationship)
expect(profile_hash).to have_key(:aspects)
end
# rubocop:disable Metrics/AbcSize
def confirm_private_profile_hash(profile_hash)
confirm_common_profile_elements(profile_hash)
expect(profile_hash).to have_key(:blocked)
expect(profile_hash).to have_key(:relationship)
expect(profile_hash).to have_key(:aspects)
expect(profile_hash).not_to have_key(:birthday)
expect(profile_hash).not_to have_key(:gender)
expect(profile_hash).not_to have_key(:location)
expect(profile_hash).not_to have_key(:bio)
end
# rubocop:enable Metrics/AbcSize
def confirm_common_profile_elements(profile_hash)
expect(profile_hash).to have_key(:guid)
expect(profile_hash).to have_key(:diaspora_id)
expect(profile_hash).to have_key(:first_name)
expect(profile_hash).to have_key(:last_name)
expect(profile_hash).to have_key(:avatar)
expect(profile_hash).to have_key(:tags)
end
def confirm_profile_details(profile_hash)
expect(profile_hash).to have_key(:birthday)
expect(profile_hash).to have_key(:gender)
expect(profile_hash).to have_key(:location)
expect(profile_hash).to have_key(:bio)
end
end

View file

@ -131,6 +131,19 @@ describe AspectsMembershipService do
end
end
describe "#all_contacts" do
before do
aspects_membership_service.create(@alice_aspect2.id, bob.person.id)
aspects_membership_service.create(@alice_aspect2.id, eve.person.id)
@alice_aspect3 = alice.aspects.create(name: "empty aspect")
end
it "returns all user's contacts" do
contacts = aspects_membership_service.all_contacts
expect(contacts.length).to eq(2)
end
end
def aspects_membership_service(user=alice)
AspectsMembershipService.new(user)
end