Skip to content

Instantly share code, notes, and snippets.

@maca
Last active September 21, 2019 04:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save maca/1aab560a9dc683193b6a to your computer and use it in GitHub Desktop.
Save maca/1aab560a9dc683193b6a to your computer and use it in GitHub Desktop.
spec/models/identity.rb
# app/models/account.rb
class Account < ActiveRecord::Base
devise :database_authenticatable, :registerable, :recoverable,
:rememberable, :trackable, :validatable, :confirmable, :validatable,
:omniauthable, omniauth_providers: [:facebook]
has_many :identities, dependent: :destroy
has_one :profile
...
end
# config/initializers/devise.rb
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
Devise.setup do |config|
...
config.omniauth :facebook,
Rails.application.secrets.facebook_app_id,
Rails.application.secrets.facebook_app_secret,
image_size: :large,
secure_image_url: true
end
# app/models/identity.rb
require 'ostruct'
class Identity < ActiveRecord::Base
belongs_to :account
before_validation :set_account
validates :provider, presence: true
validates :uid, presence: true, uniqueness: {scope: :provider}
validates :account_id, uniqueness: {scope: :provider}
validate do
unless account.confirmed?
errors.add(:account, 'There is already an unconfirmed account corresponding to that email. Please confirm the account.')
end
end
def user_info
UserInfo.new( (provider_response || {})['info'] )
end
def account
super || begin
self.account = Account.where('email IS NOT NULL').find_or_initialize_by(email: user_info.email)
end
end
private
def set_account
if !account.persisted?
account.skip_confirmation!
account.profile.attributes = user_info.profile_attributes
account.save! validate: false
elsif !account.profile.valid?
profile = account.profile
profile.attributes = user_info.profile_attributes
profile.save! validate: false
end
end
class << self
def find_or_create auth_hash, account = nil
find_or_initialize_by(uid: auth_hash.uid, provider: auth_hash.provider).tap do |identity|
next if identity.persisted?
identity.account = account
identity.provider_response = auth_hash
identity.save
end
end
end
class UserInfo < OpenStruct
def initialize hash
super
if first_name.blank? && last_name.blank? && name.present?
name = self.name.scan /\w+/
self.first_name, self.last_name = name[0], name[1..-1].join(' ')
end
end
def profile_attributes
{first_name: first_name, last_name: last_name, remote_avatar_url: image}
end
end
end
# spec/models/identity.rb
require 'rails_helper'
RSpec.describe Identity, :type => :model do
describe 'associations' do
it { should belong_to :account }
end
describe 'validations' do
subject { build :identity, :with_account }
it { should validate_uniqueness_of(:uid).scoped_to(:provider) }
it { should validate_presence_of :provider }
it { should validate_presence_of :uid }
it 'validates account is confirmed if persisted' do
account = double 'Account', confirmed?: false, persisted?: true, skip_confirmation!: true, save!: true, profile: double('Profile', valid?: true)
allow(subject).to receive(:account) { account }
expect(subject).not_to be_valid
expect(subject.errors).to include(:account)
end
end
describe 'find or create' do
let :auth_hash do
Hashie::Mash.new provider: 'facebook', uid: '123', info: {
email: 'mail@example.com',
first_name: 'Macario',
last_name: 'Ortega',
image: 'fb-avatar.png',
urls: { Facebook: 'https://www.facebook.com/app_scoped_user_id/10152302244611359/' },
verified: true
}
end
RSpec.shared_examples 'returns identity and sets account' do
it 'returns identity' do
expect(identity).to be_kind_of Identity
expect(identity).to be_persisted
expect(identity.uid).to eq auth_hash.uid
expect(identity.provider).to eq auth_hash.provider
end
it 'sets acount' do
account = identity.account
expect(account).not_to be_blank
expect(account).to be_confirmed
expect(account.email).to eq auth_hash.info.email
end
end
RSpec.shared_examples 'links to existing account' do
it 'links to existing account' do
expect(identity.account).to eq account
end
end
RSpec.shared_examples 'creates identity' do
it 'creates identity' do
expect { identity }.to change { Identity.count }.to(1)
end
end
context 'non existing identity and account' do
let(:identity) { Identity.find_or_create auth_hash }
it_behaves_like 'returns identity and sets account'
it_behaves_like 'creates identity'
end
context 'existing identity' do
let!(:existing_identity) { create :identity, uid: '123', provider: 'facebook', account: account }
let(:account) { create :account, :confirmed, email: auth_hash.info.email }
let(:identity) { Identity.find_or_create auth_hash }
it 'returns existing identity' do
expect { identity }.not_to change { Identity.count }
expect(identity).to be_persisted
expect(identity.id).to eq existing_identity.id
end
it_behaves_like 'returns identity and sets account'
end
describe 'linking to account by email' do
context 'existing confirmed account without profile' do
let!(:account) { create :account, :confirmed, email: auth_hash.info.email }
let!(:identity) { Identity.find_or_create auth_hash }
it_behaves_like 'returns identity and sets account'
it_behaves_like 'links to existing account'
it 'sets profile' do
profile = account.reload.profile
expect(profile).to be_persisted
expect(profile.first_name).to eq 'Macario'
expect(profile.last_name).to eq 'Ortega'
end
end
context 'existing confirmed account with profile' do
let!(:account) { create :account, :confirmed, :with_profile, email: auth_hash.info.email }
let(:identity) { Identity.find_or_create auth_hash }
it_behaves_like 'returns identity and sets account'
it_behaves_like 'links to existing account'
it 'keeps profile' do
expect { Identity.find_or_create auth_hash }.not_to change{ account.profile.reload }
end
end
end
context 'force linking to existing account' do
let!(:account) { create :account, :confirmed, email: 'user-1@example.com' }
let(:identity) { Identity.find_or_create auth_hash, account }
it_behaves_like 'links to existing account'
end
end
describe Identity::UserInfo, :type => :model do
it 'returns a UserInfo' do
expect(Identity.new.user_info ).to be_a Identity::UserInfo
end
it 'should obtain first and last names' do
user_info = Identity::UserInfo.new(name: 'Macario Ortega', image: 'avatar.png')
expect(user_info.first_name).to eq 'Macario'
expect(user_info.last_name).to eq 'Ortega'
end
it 'should output profile attributes' do
attributes = Identity::UserInfo.new(name: 'Macario Ortega', image: 'avatar.png').profile_attributes
expect(attributes[:remote_avatar_url]).to eq 'avatar.png'
expect(attributes[:first_name]).to eq 'Macario'
expect(attributes[:last_name]).to eq 'Ortega'
end
end
end
# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
def all
if identity.valid?
sign_in_and_redirect identity.account, :event => :authentication
set_flash_message(:notice, :success, :kind => identity.provider.titleize) if is_navigational_format?
else
redirect_to new_user_registration_url, alert: identity.errors.to_a.to_sentence
end
end
Devise.omniauth_providers.each { |provider| alias_method provider, :all }
private
def identity
@identity ||= Identity.find_or_create request.env["omniauth.auth"]
end
end
# config/routes.rb
Rails.application.routes.draw do
devise_for :users, class_name: 'Account', controllers: {
omniauth_callbacks: 'users/omniauth_callbacks'
}
get '/auth/:provider/callback', :to => 'oauth_sessions#create', :constraints => { :provider => /facebook/ }
...
root to: 'home#index'
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment