diff --git a/.ruby-version b/.ruby-version index b1b25a5f..58594069 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.2.2 +2.2.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1047a9..bc879e8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add ability to sign in/sign up via Social networks. And connect/disconnect network with current account from the profile page. +- Update [uglifier](https://github.com/lautis/uglifier) gem up to 2.7.2 - Move Rack::CanonicalHost and Rack::Auth::Basic configuration to initializers - Support [Heroku Review Apps](https://devcenter.heroku.com/articles/github-integration#review-apps) - Update [rails](https://github.com/rails/rails) version up to 4.2.3 diff --git a/Gemfile b/Gemfile index f5443231..e7f7f3ee 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" -ruby "2.2.2" +ruby "2.2.3" gem "rails", "4.2.3" gem "pg" @@ -14,7 +14,7 @@ gem "jquery-rails" gem "sass-rails", "~> 5.0.0" gem "skim" gem "therubyracer", platforms: :ruby -gem "uglifier", ">= 1.3.0" +gem "uglifier" # views gem "active_link_to" @@ -28,6 +28,8 @@ gem "devise" gem "google-analytics-rails" gem "interactor" gem "kaminari" +gem "omniauth-facebook" +gem "omniauth-google-oauth2" gem "responders" gem "rollbar", "~> 0.10.3" gem "seedbank" diff --git a/Gemfile.lock b/Gemfile.lock index 3fd62acb..12235294 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -129,6 +129,8 @@ GEM railties (>= 3.0.0) faker (1.5.0) i18n (~> 0.5) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) fastercsv (1.5.5) foreman (0.63.0) dotenv (>= 0.7) @@ -151,6 +153,7 @@ GEM google-analytics-rails (0.0.6) haml (4.0.6) tilt + hashie (3.4.2) highline (1.6.21) i18n (0.7.0) interactor (3.1.0) @@ -166,6 +169,7 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.3) + jwt (1.5.1) kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -185,8 +189,30 @@ GEM mini_portile (0.6.2) minitest (5.8.0) multi_json (1.11.0) + multi_xml (0.5.5) + multipart-post (2.0.0) nokogiri (1.6.6.2) mini_portile (~> 0.6.0) + oauth2 (1.0.0) + faraday (>= 0.8, < 0.10) + jwt (~> 1.0) + multi_json (~> 1.3) + multi_xml (~> 0.5) + rack (~> 1.2) + omniauth (1.2.2) + hashie (>= 1.2, < 4) + rack (~> 1.0) + omniauth-facebook (2.0.1) + omniauth-oauth2 (~> 1.2) + omniauth-google-oauth2 (0.2.8) + addressable (~> 2.3) + jwt (~> 1.0) + multi_json (~> 1.3) + omniauth (>= 1.1.1) + omniauth-oauth2 (>= 1.1.1) + omniauth-oauth2 (1.3.1) + oauth2 (~> 1.0) + omniauth (~> 1.2) orm_adapter (0.5.0) parser (2.2.2.3) ast (>= 1.1, < 3.0) @@ -350,7 +376,7 @@ GEM tilt (1.4.1) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (2.4.0) + uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) uniform_notifier (1.9.0) @@ -404,6 +430,8 @@ DEPENDENCIES launchy letter_opener metamagic + omniauth-facebook + omniauth-google-oauth2 pg pry-rails pundit @@ -429,6 +457,9 @@ DEPENDENCIES spring-commands-rspec therubyracer thin - uglifier (>= 1.3.0) + uglifier web-console (~> 2.0) webmock + +BUNDLED WITH + 1.10.6 diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb new file mode 100644 index 00000000..2d7e457a --- /dev/null +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -0,0 +1,42 @@ +class OmniauthCallbacksController < Devise::OmniauthCallbacksController + include OmniauthHelper + + expose(:user) { OauthOrganizer.new(current_user, auth_hash).call } + + SocialProfile::PROVIDERS.each do |provider| + define_method(provider.to_s) do + begin + handle_user + rescue OauthOrganizer::OauthError + handle_error + end + end + end + + def after_sign_in_path_for(_resource) + edit_user_registration_path + end + + private + + def auth_hash + request.env["omniauth.auth"] + end + + # rubocop:disable Metrics/AbcSize + def handle_user + if user.persisted? + sign_in_and_redirect user, event: :authentication + set_flash_message(:notice, :success, kind: "#{provider_name(auth_hash.provider)}") if is_navigational_format? + else + session[:omniauth] = auth_hash.except("extra") + redirect_to new_user_registration_url + end + end + # rubocop:enable Metrics/AbcSize + + def handle_error + redirect_to new_user_session_path, + notice: t("omniauth.verification.failure", kind: provider_name(auth_hash.provider)) + end +end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 00000000..27eab5a6 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -0,0 +1,13 @@ +class RegistrationsController < Devise::RegistrationsController + def create + super + session[:omniauth] = nil unless @user.new_record? + end + + private + + def build_resource(*args) + super + @user.apply_omniauth(session[:omniauth]) if session[:omniauth] + end +end diff --git a/app/controllers/social_profiles_controller.rb b/app/controllers/social_profiles_controller.rb new file mode 100644 index 00000000..eac940b2 --- /dev/null +++ b/app/controllers/social_profiles_controller.rb @@ -0,0 +1,21 @@ +class SocialProfilesController < ApplicationController + before_action :authenticate_user! + + expose(:social_profiles) { current_user.social_profiles } + expose(:social_profile) + + def destroy + if social_profile.destroy + flash[:notice] = t "flash.actions.destroy.notice", resource_name: resource_name + else + flash[:alert] = t "flash.actions.destroy.alert", resource_name: resource_name + end + redirect_to edit_user_registration_url + end + + private + + def resource_name + SocialProfile.model_name.human + end +end diff --git a/app/helpers/omniauth_helper.rb b/app/helpers/omniauth_helper.rb new file mode 100644 index 00000000..88471a14 --- /dev/null +++ b/app/helpers/omniauth_helper.rb @@ -0,0 +1,5 @@ +module OmniauthHelper + def provider_name(provider) + t "active_record.attributes.social_profile.provider_name.#{provider}" + end +end diff --git a/app/interactors/connect_social_profile.rb b/app/interactors/connect_social_profile.rb new file mode 100644 index 00000000..de94d79a --- /dev/null +++ b/app/interactors/connect_social_profile.rb @@ -0,0 +1,23 @@ +class ConnectSocialProfile + attr_reader :user, :auth + private :user, :auth + + def initialize(user, auth) + @user = user + @auth = auth + end + + def call + if social_profile + social_profile.update_attribute(:user, user) + else + user.apply_omniauth(auth) && user.save + end + end + + private + + def social_profile + @social_profile ||= SocialProfile.from_omniauth(auth) + end +end diff --git a/app/interactors/fetch_oauth_user.rb b/app/interactors/fetch_oauth_user.rb new file mode 100644 index 00000000..199fc735 --- /dev/null +++ b/app/interactors/fetch_oauth_user.rb @@ -0,0 +1,22 @@ +class FetchOauthUser + attr_reader :auth + private :auth + + def initialize(auth) + @auth = auth + end + + def call + find_social_profile_user || find_user_by_email + end + + private + + def find_social_profile_user + SocialProfile.from_omniauth(auth).try(:user) + end + + def find_user_by_email + User.find_by(email: auth["info"]["email"]) + end +end diff --git a/app/interactors/oauth_organizer.rb b/app/interactors/oauth_organizer.rb new file mode 100644 index 00000000..dfd3d36c --- /dev/null +++ b/app/interactors/oauth_organizer.rb @@ -0,0 +1,44 @@ +class OauthOrganizer + class OauthError < StandardError + end + + attr_reader :auth, :current_user + private :auth, :current_user + + def initialize(current_user, auth) + @current_user = current_user + @auth = auth + end + + def call + user.present? ? connect_social_profile : fail_oauth + user + end + + private + + def user + @user ||= current_user || fetch_oauth_user || build_user + end + + def fetch_oauth_user + FetchOauthUser.new(auth).call if auth_verified? + end + + def auth_verified? + AuthVerificationPolicy.new(auth).verified? + end + + def build_user + User.build_from_omniauth(auth) if auth_verified? + end + + def connect_social_profile + ConnectSocialProfile.new(user, auth).call + end + + def fail_oauth + fail OauthError, "Sorry, but yours #{auth.provider.titleize} failed verification. + Seems like yours #{auth.provider.titleize} account hasn't been verified." + end +end diff --git a/app/models/social_profile.rb b/app/models/social_profile.rb new file mode 100644 index 00000000..87e1087c --- /dev/null +++ b/app/models/social_profile.rb @@ -0,0 +1,12 @@ +class SocialProfile < ActiveRecord::Base + PROVIDERS = %i(facebook google_oauth2) + + belongs_to :user + + validates :user, :provider, :uid, presence: true + validates :uid, uniqueness: { scope: :provider } + + def self.from_omniauth(auth) + find_by(provider: auth.provider, uid: auth.uid) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 521dad51..d823ebba 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,9 +1,12 @@ class User < ActiveRecord::Base devise :database_authenticatable, :registerable, :confirmable, - :recoverable, :rememberable, :trackable, :validatable + :recoverable, :rememberable, :trackable, :validatable, + :omniauthable, omniauth_providers: SocialProfile::PROVIDERS validates :full_name, presence: true + has_many :social_profiles, dependent: :destroy + def to_s full_name end @@ -11,4 +14,14 @@ def to_s def full_name_with_email "#{self[:full_name]} (#{email})" end + + def self.build_from_omniauth(auth) + new(email: auth["info"]["email"], full_name: auth["info"]["name"]) + end + + def apply_omniauth(auth) + self.email = auth["info"]["email"] if email.blank? + self.full_name = auth["info"]["name"] if full_name.blank? + social_profiles.build(provider: auth["provider"], uid: auth["uid"]) + end end diff --git a/app/policies/auth_verification_policy.rb b/app/policies/auth_verification_policy.rb new file mode 100644 index 00000000..a113c04e --- /dev/null +++ b/app/policies/auth_verification_policy.rb @@ -0,0 +1,32 @@ +class AuthVerificationPolicy + attr_reader :auth + private :auth + + def initialize(auth) + @auth = auth + end + + def verified? + request_verification_for + rescue NoMethodError + fail_with_error + end + + private + + def request_verification_for + send(auth.provider) + end + + def fail_with_error + fail ArgumentError, I18n.t("omniauth.verification.not_implemented", kind: auth.provider) + end + + def facebook + auth.info.verified? || auth.extra.raw_info.verified? + end + + def google_oauth2 + auth.extra.raw_info.email_verified? + end +end diff --git a/app/views/application/_navigation_user.html.slim b/app/views/application/_navigation_user.html.slim index f3fe4746..6c5d619d 100644 --- a/app/views/application/_navigation_user.html.slim +++ b/app/views/application/_navigation_user.html.slim @@ -13,3 +13,9 @@ ul.right - else = active_link_to 'Sign in', new_user_session_path, active: :exclusive, wrap_tag: :li = active_link_to 'Sign up', new_user_registration_path, active: :exclusive, wrap_tag: :li + + - SocialProfile::PROVIDERS.each do |provider| + = active_link_to "Sign in with #{provider_name(provider)}", + user_omniauth_authorize_path(provider), + active: :exclusive, + wrap_tag: :li diff --git a/app/views/social_profiles/_list.html.slim b/app/views/social_profiles/_list.html.slim new file mode 100644 index 00000000..69571677 --- /dev/null +++ b/app/views/social_profiles/_list.html.slim @@ -0,0 +1,14 @@ +- if social_profiles.any? + b Successfully authorized via: + ul.js-social-profiles + - social_profiles.each do |social_profile| + li = link_to "#{provider_name(social_profile.provider)} (#{social_profile.uid.truncate(9)}). Unauthorize?", + social_profile, + data: { confirm: "Are you sure you want to remove this social profile?" }, + method: :delete, + class: "js-unauthorize" + +b Add service to sign in with: +ul + - SocialProfile::PROVIDERS.each do |provider| + li = link_to provider_name(provider), user_omniauth_authorize_path(provider) diff --git a/app/views/users/registrations/edit.html.slim b/app/views/users/registrations/edit.html.slim index 0decc952..dcd25388 100644 --- a/app/views/users/registrations/edit.html.slim +++ b/app/views/users/registrations/edit.html.slim @@ -24,6 +24,8 @@ = f.button :submit, 'Update' .medium-6.columns.end + = render "social_profiles/list", social_profiles: current_user.social_profiles + h6 b Cancel my account p diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index e5b9fb2c..0f57869c 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -230,6 +230,8 @@ # Add a new OmniAuth provider. Check the wiki for more information on setting # up on your models and hooks. # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + config.omniauth :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"] + config.omniauth :facebook, ENV["FACEBOOK_APP_ID"], ENV["FACEBOOK_APP_SECRET"], info_fields: "email, name, verified" # ==> Warden configuration # If you want to use other strategies, that are not supported by Devise, or diff --git a/config/locales/models/social_profile.en.yml b/config/locales/models/social_profile.en.yml new file mode 100644 index 00000000..9a464125 --- /dev/null +++ b/config/locales/models/social_profile.en.yml @@ -0,0 +1,7 @@ +en: + active_record: + attributes: + social_profile: + provider_name: + google_oauth2: Google + facebook: Facebook diff --git a/config/locales/oauth.en.yml b/config/locales/oauth.en.yml new file mode 100644 index 00000000..9f69b348 --- /dev/null +++ b/config/locales/oauth.en.yml @@ -0,0 +1,5 @@ +en: + omniauth: + verification: + failure: Your %{kind} account can't be used to sign in. Please verify it via profile page. + not_implemented: Verification checking is not implemented for %{kind}. diff --git a/config/rails_best_practices.yml b/config/rails_best_practices.yml index e7ed6505..a2db146b 100644 --- a/config/rails_best_practices.yml +++ b/config/rails_best_practices.yml @@ -28,6 +28,9 @@ RemoveTrailingWhitespaceCheck: { } RemoveUnusedMethodsInControllersCheck: except_methods: - ApplicationController#devise_parameter_sanitizer + - RegistrationsController#create + - RegistrationsController#build_resource + - RegistrationsController#devise_parameter_sanitizer RemoveUnusedMethodsInHelpersCheck: { except_methods: [] } RemoveUnusedMethodsInModelsCheck: { except_methods: [] } ReplaceComplexCreationWithFactoryMethodCheck: { attribute_assignment_count: 2 } diff --git a/config/routes.rb b/config/routes.rb index 2053e363..791f8643 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,9 @@ Rails.application.routes.draw do - devise_for :users + devise_for :users, + controllers: { omniauth_callbacks: "omniauth_callbacks", registrations: "registrations" } resource :feedback, only: %i(new create) + resources :social_profiles, only: :destroy with_options controller: :pages do get :about diff --git a/db/migrate/20151013081630_create_social_profiles.rb b/db/migrate/20151013081630_create_social_profiles.rb new file mode 100644 index 00000000..1278f4d8 --- /dev/null +++ b/db/migrate/20151013081630_create_social_profiles.rb @@ -0,0 +1,9 @@ +class CreateSocialProfiles < ActiveRecord::Migration + def change + create_table :social_profiles do |t| + t.references :user, index: true + t.string :provider, index: true, null: false, default: "" + t.string :uid, index: true, null: false, default: "" + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 423f9aa7..bf4d9a46 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,21 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20100713113845) do +ActiveRecord::Schema.define(version: 20151013081630) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "social_profiles", force: :cascade do |t| + t.integer "user_id" + t.string "provider", default: "", null: false + t.string "uid", default: "", null: false + end + + add_index "social_profiles", ["provider"], name: "index_social_profiles_on_provider", using: :btree + add_index "social_profiles", ["uid"], name: "index_social_profiles_on_uid", using: :btree + add_index "social_profiles", ["user_id"], name: "index_social_profiles_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "email", limit: 255, default: "", null: false t.string "encrypted_password", limit: 255, default: "", null: false diff --git a/spec/factories/social_profiles.rb b/spec/factories/social_profiles.rb new file mode 100644 index 00000000..45755b4d --- /dev/null +++ b/spec/factories/social_profiles.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :social_profile do + user + provider "facebook" + uid "12345" + end +end diff --git a/spec/features/user/account/social_profiles/add_remove_spec.rb b/spec/features/user/account/social_profiles/add_remove_spec.rb new file mode 100644 index 00000000..18c0470e --- /dev/null +++ b/spec/features/user/account/social_profiles/add_remove_spec.rb @@ -0,0 +1,58 @@ +require "rails_helper" + +feature "Add/Remove social profiles" do + let(:user) { create(:user, :confirmed) } + let(:user_attributes) { user.attributes.slice(:full_name, :email) } + let(:oauth) { omniauth_mock(provider, "12345", user_attributes) } + let(:authentication_message) { "Successfully authenticated from #{provider_name} account." } + let(:unlink_message) { "Social profile was successfully destroyed." } + let(:unauthorize_link) { find(:css, ".js-unauthorize") } + let(:provider_name) { I18n.t("active_record.attributes.social_profile.provider_name.#{provider}") } + + before do + stub_omniauth(provider, oauth) + allow_any_instance_of(AuthVerificationPolicy).to receive(:verified?) + + login_as user + visit edit_user_registration_path + end + + context "when provider is Facebook" do + let(:provider) { :facebook } + + scenario "user adds social profile" do + expect(page).not_to have_providers_list(provider_name) + + click_link provider_name + expect(page).to have_text(authentication_message) + expect(page).to have_providers_list(provider_name) + + unauthorize_link.click + expect(page).to have_text(unlink_message) + expect(page).not_to have_providers_list(provider_name) + end + end + + context "when provider is Google" do + let(:provider) { :google_oauth2 } + + scenario "user adds social profile" do + expect(page).not_to have_providers_list(provider_name) + + click_link provider_name + expect(page).to have_text(authentication_message) + expect(page).to have_providers_list(provider_name) + + unauthorize_link.click + expect(page).to have_text(unlink_message) + expect(page).not_to have_providers_list(provider_name) + end + end + + # rubocop:disable Style/PredicateName + def have_providers_list(provider) + have_text("Successfully authorized via:") + have_css(".js-social-profiles", text: provider) + end + # rubocop:enable Style/PredicateName +end diff --git a/spec/features/visitor/social_profiles/sign_up_spec.rb b/spec/features/visitor/social_profiles/sign_up_spec.rb new file mode 100644 index 00000000..c2d4354d --- /dev/null +++ b/spec/features/visitor/social_profiles/sign_up_spec.rb @@ -0,0 +1,115 @@ +require "rails_helper" + +feature "Sign Up" do + let(:registered_user) { User.find_by_email(user_attributes[:email]) } + let(:oauth) { omniauth_mock(provider, "12345", user_attributes) } + let(:user_attributes) { attributes_for(:user).slice(:full_name, :email, :password, :password_confirmation) } + let(:provider_name) { I18n.t("active_record.attributes.social_profile.provider_name.#{provider}") } + + before do + stub_omniauth(provider, oauth) + allow_any_instance_of(AuthVerificationPolicy).to receive(:verified?).and_return(verified) + + visit root_path + end + + context "when user is not persisted" do + let(:verified) { true } + let(:success_message) { "You have signed up successfully." } + + context "when provider is Google" do + let(:provider) { :google_oauth2 } + + scenario "Visitor signs up through provider" do + click_link "Sign in with Google" + expect(page).to have_prefilled_fields(user_attributes) + + fill_profile_with(user_attributes) + expect(page).to have_text(success_message) + expect(page).to have_text(registered_user.email) + end + end + + context "when provider is Facebook" do + let(:provider) { :facebook } + + scenario "Visitor signs up through provider" do + click_link "Sign in with Facebook" + expect(page).to have_prefilled_fields(user_attributes) + + fill_profile_with(user_attributes) + expect(page).to have_text(success_message) + expect(page).to have_text(registered_user.email) + end + end + end + + context "when user is persisted" do + let(:verified) { true } + let(:success_message) { "Successfully authenticated from #{provider_name} account." } + + before { create(:user, user_attributes) } + + context "when provider is Google" do + let(:provider) { :google_oauth2 } + + scenario "Visitor signs in through provider" do + click_link "Sign in with Google" + + expect(page).to have_text(success_message) + expect(page).to have_text(registered_user.email) + end + end + + context "when provider is Facebook" do + let(:provider) { :facebook } + + scenario "Visitor signs in through provider" do + click_link "Sign in with Facebook" + + expect(page).to have_text(success_message) + expect(page).to have_text(registered_user.email) + end + end + end + + context "when oauth can't be used for authentication" do + let(:verified) { false } + let(:error_msg) { "Your #{provider_name} account can't be used to sign in. Please verify it via profile page." } + + context "when provider is Google" do + let(:provider) { :google_oauth2 } + + scenario "Visitor not able to sign up through provider" do + click_link "Sign in with Google" + + expect(page).to have_text(error_msg) + expect(page).not_to have_text(user_attributes[:email]) + end + end + + context "when provider is Facebook" do + let(:provider) { :facebook } + + scenario "Visitor not able to sign up through provider" do + click_link "Sign in with Facebook" + + expect(page).to have_text(error_msg) + expect(page).not_to have_text(user_attributes[:email]) + end + end + end + + # rubocop:disable Style/PredicateName + def have_prefilled_fields(opts = {}) + have_field("user_full_name", with: opts[:full_name]) + have_field("user_email", with: opts[:email]) + end + # rubocop:enable Style/PredicateName + + def fill_profile_with(opts = {}) + fill_in "user_password", with: opts[:password] + fill_in "user_password_confirmation", with: opts[:password_confirmation] + click_button "Sign up" + end +end diff --git a/spec/interactors/connect_social_profile_spec.rb b/spec/interactors/connect_social_profile_spec.rb new file mode 100644 index 00000000..f127d6b9 --- /dev/null +++ b/spec/interactors/connect_social_profile_spec.rb @@ -0,0 +1,27 @@ +require "rails_helper" + +describe ConnectSocialProfile do + let(:user) { create(:user) } + let(:provider) { "facebook" } + let(:uid) { "12345" } + let(:auth) { omniauth_mock(provider, uid) } + let(:service) { described_class.new(user, auth) } + + subject { service.call } + + context "when social_profile not exists" do + it "creates related social_profile" do + expect { subject }.to change { user.social_profiles.count }.by(1) + end + end + + context "when disconnected social profile exists" do + let!(:social_profile) { create(:social_profile, provider: provider, uid: uid, user: another_user) } + + let(:another_user) { create(:user) } + + it "changes user for given social_profile" do + expect { subject }.to change { social_profile.reload.user }.from(another_user).to(user) + end + end +end diff --git a/spec/interactors/fetch_oauth_user_spec.rb b/spec/interactors/fetch_oauth_user_spec.rb new file mode 100644 index 00000000..61809179 --- /dev/null +++ b/spec/interactors/fetch_oauth_user_spec.rb @@ -0,0 +1,27 @@ +require "rails_helper" + +describe FetchOauthUser do + let(:auth) { omniauth_mock("facebook", "12345", user_attributes) } + let(:service) { described_class.new(auth) } + let(:user_attributes) { attributes_for(:user).slice(:full_name, :email) } + + subject { service.call } + + context "when user is persisted" do + let!(:user) { create(:user, user_attributes) } + + it { is_expected.to eq(user) } + end + + context "when social_profile persisted" do + let!(:social_profile) { create(:social_profile, user: user) } + + let(:user) { create(:user, user_attributes) } + + it { is_expected.to eq(social_profile.user) } + end + + context "when user and social profile are not persisted" do + it { is_expected.to eq(nil) } + end +end diff --git a/spec/interactors/oauth_organizer_spec.rb b/spec/interactors/oauth_organizer_spec.rb new file mode 100644 index 00000000..1e21f845 --- /dev/null +++ b/spec/interactors/oauth_organizer_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" + +describe OauthOrganizer do + let(:oauth) { omniauth_mock("facebook", "12345") } + let(:service) { described_class.new(current_user, oauth) } + + subject { service.call } + + context "when user present" do + let(:current_user) { create(:user) } + + it "connects new social_profile" do + expect { subject }.to change { current_user.social_profiles.count }.by(1) + expect(subject).to eq(current_user) + end + end + + context "when user not present" do + before do + allow(FetchOauthUser).to receive_message_chain(:new, :call) + allow_any_instance_of(AuthVerificationPolicy).to receive(:verified?) + end + + let(:current_user) { nil } + + it "fails OauthError" do + expect { subject }.to raise_error(OauthOrganizer::OauthError) + end + end +end diff --git a/spec/models/social_profiles_spec.rb b/spec/models/social_profiles_spec.rb new file mode 100644 index 00000000..79c30cf3 --- /dev/null +++ b/spec/models/social_profiles_spec.rb @@ -0,0 +1,11 @@ +require "rails_helper" + +describe SocialProfile do + subject { create :social_profile } + + it { should belong_to :user } + it { is_expected.to validate_presence_of :user } + it { is_expected.to validate_presence_of :uid } + it { is_expected.to validate_presence_of :provider } + it { is_expected.to validate_uniqueness_of(:uid).scoped_to(:provider) } +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0d1f5ef0..5ca43167 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,4 +2,5 @@ describe User do it { is_expected.to validate_presence_of :full_name } + it { is_expected.to have_many(:social_profiles).dependent(:destroy) } end diff --git a/spec/policies/auth_verification_policy_spec.rb b/spec/policies/auth_verification_policy_spec.rb new file mode 100644 index 00000000..fb4f6fdf --- /dev/null +++ b/spec/policies/auth_verification_policy_spec.rb @@ -0,0 +1,43 @@ +require "rails_helper" + +describe AuthVerificationPolicy do + let(:auth) { double(:omniauth, provider: provider) } + + describe ".verified?" do + subject { described_class.new(auth).verified? } + + context "when provider is Facebook" do + let(:provider) { "facebook" } + + before do + allow(auth).to receive_message_chain(:info, :verified?).and_return(true) + allow(auth).to receive_message_chain(:extra, :raw_info, :verified?).and_return(true) + end + + it "returns corresponding value" do + expect(subject).to eq(true) + end + end + + context "when provider is Google" do + let(:provider) { "google_oauth2" } + + before do + allow(auth).to receive_message_chain(:extra, :raw_info, :email_verified?).and_return(true) + end + + it "returns corresponding value" do + expect(subject).to eq(true) + end + end + + context "when provider is not in the case statement" do + let(:provider) { "another" } + + it "raises Exception" do + expect { subject } + .to raise_error(ArgumentError, I18n.t("omniauth.verification.not_implemented", kind: provider)) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7177eac9..c1f2cbd8 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,4 +10,6 @@ RSpec.configure do |config| config.use_transactional_fixtures = false config.infer_spec_type_from_file_location! + + config.include OauthHelpers end diff --git a/spec/support/oauth_helpers.rb b/spec/support/oauth_helpers.rb new file mode 100644 index 00000000..478599ae --- /dev/null +++ b/spec/support/oauth_helpers.rb @@ -0,0 +1,15 @@ +module OauthHelpers + def stub_omniauth(provider, omniauth_mock) + OmniAuth.config.test_mode = true + OmniAuth.config.mock_auth[provider] = omniauth_mock + Rails.application.env_config["omniauth.auth"] = omniauth_mock + end + + def omniauth_mock(provider, uid, user_attrs = {}) + OmniAuth::AuthHash.new( + provider: provider.to_s, + uid: uid, + info: { name: user_attrs[:full_name], email: user_attrs[:email] } + ) + end +end