From 629c7114dac0d0b8e573f3e344fc785102bf6f57 Mon Sep 17 00:00:00 2001 From: Josh Puetz Date: Thu, 13 Jan 2022 10:25:52 -0600 Subject: [PATCH] Login with Google (#15986) --- Gemfile | 1 + Gemfile.lock | 6 ++ app/assets/images/google_oauth2.svg | 3 + .../stylesheets/components/buttons.scss | 15 +++++ app/helpers/authentication_helper.rb | 4 +- app/lib/constants/settings/authentication.rb | 8 +++ app/models/settings/authentication.rb | 2 + app/services/authentication/providers.rb | 10 +++- .../authentication/providers/google_oauth2.rb | 55 ++++++++++++++++++ .../authentication/providers/provider.rb | 2 +- .../record_daily_notifications_worker.rb | 1 + config/initializers/devise.rb | 6 ++ ...20211220170907_add_google_o_auth_fields.rb | 6 ++ ...20171024_add_google_o_auth_fields_index.rb | 7 +++ db/schema.rb | 3 + db/seeds.rb | 2 + ...oogle_ouauth2_connect_broadcast_message.rb | 17 ++++++ spec/factories/broadcasts.rb | 9 +++ spec/helpers/authentication_helper_spec.rb | 2 +- ..._ouauth2_connect_broadcast_message_spec.rb | 19 ++++++ spec/models/user_spec.rb | 4 +- .../services/authentication/providers_spec.rb | 22 ++++++- .../welcome_notification/generator_spec.rb | 2 + spec/support/omniauth_helpers.rb | 53 +++++++++++++++++ .../creator_config_edit_spec.rb | 4 +- vendor/cache/omniauth-google-oauth2-1.0.0.gem | Bin 0 -> 22528 bytes 26 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 app/assets/images/google_oauth2.svg create mode 100644 app/services/authentication/providers/google_oauth2.rb create mode 100644 db/migrate/20211220170907_add_google_o_auth_fields.rb create mode 100644 db/migrate/20211220171024_add_google_o_auth_fields_index.rb create mode 100644 lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb create mode 100644 spec/lib/data_update_scripts/insert_google_ouauth2_connect_broadcast_message_spec.rb create mode 100644 vendor/cache/omniauth-google-oauth2-1.0.0.gem diff --git a/Gemfile b/Gemfile index 5db7c75b1..be795f50b 100644 --- a/Gemfile +++ b/Gemfile @@ -63,6 +63,7 @@ gem "omniauth", "~> 2.0" # A generalized Rack framework for multiple-provider au gem "omniauth-apple", "~> 1.0" # OmniAuth strategy for Sign In with Apple gem "omniauth-facebook", "~> 9.0" # OmniAuth strategy for Facebook gem "omniauth-github", "~> 2.0" # OmniAuth strategy for GitHub +gem "omniauth-google-oauth2", "~> 1.0" gem "omniauth-rails_csrf_protection", "~> 1.0" # Provides CSRF protection on OmniAuth request endpoint on Rails application. gem "omniauth-twitter", "~> 1.4" # OmniAuth strategy for Twitter gem "parallel", "~> 1.21" # Run any kind of code in parallel processes diff --git a/Gemfile.lock b/Gemfile.lock index 23a9dce6c..0a40c2027 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -523,6 +523,11 @@ GEM omniauth-github (2.0.0) omniauth (~> 2.0) omniauth-oauth2 (~> 1.7.1) + omniauth-google-oauth2 (1.0.0) + jwt (>= 2.0) + oauth2 (~> 1.1) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.7.1) omniauth-oauth (1.2.0) oauth omniauth (>= 1.0, < 3) @@ -966,6 +971,7 @@ DEPENDENCIES omniauth-apple (~> 1.0) omniauth-facebook (~> 9.0) omniauth-github (~> 2.0) + omniauth-google-oauth2 (~> 1.0) omniauth-rails_csrf_protection (~> 1.0) omniauth-twitter (~> 1.4) parallel (~> 1.21) diff --git a/app/assets/images/google_oauth2.svg b/app/assets/images/google_oauth2.svg new file mode 100644 index 000000000..eec3cc840 --- /dev/null +++ b/app/assets/images/google_oauth2.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/stylesheets/components/buttons.scss b/app/assets/stylesheets/components/buttons.scss index 73f53803c..3de9f4d60 100644 --- a/app/assets/stylesheets/components/buttons.scss +++ b/app/assets/stylesheets/components/buttons.scss @@ -24,6 +24,10 @@ --brand-email-bg: #24292e; --brand-email-color: #fff; --brand-email-bg-hover: #000; + + --brand-google_oauth2-bg: #1da1f2; + --brand-google_oauth2-color: #fff; + --brand-google_oauth2-bg-hover: #0096f2; } // Basic styling @@ -325,6 +329,17 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to --color-inverted-hover: var(--brand-email-color); } +.crayons-btn--brand-google_oauth2 { + --bg: var(--brand-google_oauth2-bg); + --bg-hover: var(--brand-google_oauth2-bg-hover); + --color: var(--brand-google_oauth2-color); + --color-hover: var(--brand-google_oauth2-color); + --bg-inverted: var(--brand-google_oauth2-bg); + --bg-inverted-hover: var(--brand-google_oauth2-bg-hover); + --color-inverted: var(--brand-google_oauth2-color); + --color-inverted-hover: var(--brand-google_oauth2-color); +} + // Icon alone .crayons-btn--icon, .crayons-btn--icon-rounded { diff --git a/app/helpers/authentication_helper.rb b/app/helpers/authentication_helper.rb index a7728d405..49d6ab77b 100644 --- a/app/helpers/authentication_helper.rb +++ b/app/helpers/authentication_helper.rb @@ -4,9 +4,7 @@ module AuthenticationHelper end def authentication_available_providers - Authentication::Providers.available.map do |provider_name| - Authentication::Providers.const_get(provider_name.to_s.titleize) - end + Authentication::Providers.available_providers end def authentication_enabled_providers diff --git a/app/lib/constants/settings/authentication.rb b/app/lib/constants/settings/authentication.rb index 1d14231b8..3503bd83a 100644 --- a/app/lib/constants/settings/authentication.rb +++ b/app/lib/constants/settings/authentication.rb @@ -59,6 +59,14 @@ module Constants description: "The \"Client Secret\" portion of the GitHub Oauth Apps portal", placeholder: "" }, + google_oauth2_key: { + description: "The \"Client ID\" portion of the OAuth 2.0 page on the Google Cloud Platform portal", + placeholder: "" + }, + google_oauth2_secret: { + description: "The \"Client Secret\" portion of the OAuth 2.0 page on the Google Cloud Platform portal", + placeholder: "" + }, invite_only_mode: { description: "Only users invited by email can join this community.", placeholder: "" diff --git a/app/models/settings/authentication.rb b/app/models/settings/authentication.rb index 5726d669f..46d5eff17 100644 --- a/app/models/settings/authentication.rb +++ b/app/models/settings/authentication.rb @@ -21,6 +21,8 @@ module Settings setting :forem_secret, type: :string setting :github_key, type: :string, default: ApplicationConfig["GITHUB_KEY"] setting :github_secret, type: :string, default: ApplicationConfig["GITHUB_SECRET"] + setting :google_oauth2_key, type: :string + setting :google_oauth2_secret, type: :string setting :invite_only_mode, type: :boolean, default: false setting :providers, type: :array, default: %w[] setting :require_captcha_for_email_password_registration, type: :boolean, default: false diff --git a/app/services/authentication/providers.rb b/app/services/authentication/providers.rb index 082a6409a..2faeaab2b 100644 --- a/app/services/authentication/providers.rb +++ b/app/services/authentication/providers.rb @@ -11,7 +11,7 @@ module Authentication module Providers # Retrieves a provider that is both available and enabled def self.get!(provider_name) - name = provider_name.to_s.titleize + name = provider_name.to_s.camelize unless available?(provider_name) raise( @@ -32,12 +32,16 @@ module Authentication def self.available Authentication::Providers::Provider.subclasses.map do |subclass| - subclass.name.demodulize.downcase.to_sym + subclass.name.demodulize.underscore.downcase.to_sym end.sort end + def self.available_providers + Authentication::Providers::Provider.subclasses.sort_by(&:name) + end + def self.available?(provider_name) - Authentication::Providers.const_defined?(provider_name.to_s.titleize) + Authentication::Providers.const_defined?(provider_name.to_s.camelize) end # Returns enabled providers diff --git a/app/services/authentication/providers/google_oauth2.rb b/app/services/authentication/providers/google_oauth2.rb new file mode 100644 index 000000000..9060378ba --- /dev/null +++ b/app/services/authentication/providers/google_oauth2.rb @@ -0,0 +1,55 @@ +module Authentication + module Providers + # Google OAuth 2.0 authentication provider, uses omniauth-google-oauth2 as backend + class GoogleOauth2 < Provider + OFFICIAL_NAME = "Google".freeze + SETTINGS_URL = "https://console.cloud.google.com/apis/credentials".freeze + + def new_user_data + { + name: info.name, + email: info.email || "", + remote_profile_image_url: Users::SafeRemoteProfileImageUrl.call(@info.image), + google_oauth2_username: user_nickname + } + end + + def existing_user_data + { + google_oauth2_username: info.name + } + end + + # We're overriding this method because Google doesn't have a concept nickname or username. + # Instead: we'll construct one based on the user's name with some randomization thrown in based + # on uid, which is guaranteed to be present and unique from Google. + def user_nickname + [ + info.name.sub(" ", "_"), + Digest::SHA512.hexdigest(auth_payload.uid), + ].join("_")[0...25] + end + + def self.official_name + OFFICIAL_NAME + end + + def self.settings_url + SETTINGS_URL + end + + def self.sign_in_path(**kwargs) + ::Authentication::Paths.sign_in_path( + "google_oauth2", + **kwargs, + ) + end + + protected + + def cleanup_payload(auth_payload) + auth_payload + end + end + end +end diff --git a/app/services/authentication/providers/provider.rb b/app/services/authentication/providers/provider.rb index 42760dd10..470aa80c1 100644 --- a/app/services/authentication/providers/provider.rb +++ b/app/services/authentication/providers/provider.rb @@ -34,7 +34,7 @@ module Authentication end def self.provider_name - name.demodulize.downcase.to_sym + name.demodulize.underscore.to_sym end def self.user_username_field diff --git a/app/workers/metrics/record_daily_notifications_worker.rb b/app/workers/metrics/record_daily_notifications_worker.rb index 4dc5d9e79..4fa1e0b2a 100644 --- a/app/workers/metrics/record_daily_notifications_worker.rb +++ b/app/workers/metrics/record_daily_notifications_worker.rb @@ -9,6 +9,7 @@ module Metrics welcome_notification_customize_feed welcome_notification_twitter_connect welcome_notification_github_connect + welcome_notification_google_connect welcome_notification_customize_experience welcome_notification_discuss_and_ask welcome_notification_download_app diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 37c1d91ab..f1d2cc146 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -18,6 +18,11 @@ GITHUB_OMNIAUTH_SETUP = lambda do |env| env["omniauth.strategy"].options[:scope] = "user:email" end +GOOGLE_OAUTH2_OMNIAUTH_SETUP = lambda do |env| + env["omniauth.strategy"].options[:client_id] = Settings::Authentication.google_oauth2_key + env["omniauth.strategy"].options[:client_secret] = Settings::Authentication.google_oauth2_secret +end + FACEBOOK_OMNIAUTH_SETUP = lambda do |env| env["omniauth.strategy"].options[:client_id] = Settings::Authentication.facebook_key env["omniauth.strategy"].options[:client_secret] = Settings::Authentication.facebook_secret @@ -321,6 +326,7 @@ Devise.setup do |config| # Fun fact, unless Twitter is last, it doesn't work for some reason. config.omniauth :facebook, setup: FACEBOOK_OMNIAUTH_SETUP config.omniauth :github, setup: GITHUB_OMNIAUTH_SETUP + config.omniauth :google_oauth2, setup: GOOGLE_OAUTH2_OMNIAUTH_SETUP config.omniauth :apple, setup: APPLE_OMNIAUTH_SETUP config.omniauth :forem, setup: FOREM_OMNIAUTH_SETUP, strategy_class: OmniAuth::Strategies::Forem config.omniauth :twitter, setup: TWITTER_OMNIAUTH_SETUP diff --git a/db/migrate/20211220170907_add_google_o_auth_fields.rb b/db/migrate/20211220170907_add_google_o_auth_fields.rb new file mode 100644 index 000000000..9a64b7e54 --- /dev/null +++ b/db/migrate/20211220170907_add_google_o_auth_fields.rb @@ -0,0 +1,6 @@ +class AddGoogleOAuthFields < ActiveRecord::Migration[6.1] + def change + add_column :users, :google_oauth2_username, :string + add_column :users, :google_oauth2_created_at, :datetime + end +end diff --git a/db/migrate/20211220171024_add_google_o_auth_fields_index.rb b/db/migrate/20211220171024_add_google_o_auth_fields_index.rb new file mode 100644 index 000000000..c15349c2b --- /dev/null +++ b/db/migrate/20211220171024_add_google_o_auth_fields_index.rb @@ -0,0 +1,7 @@ +class AddGoogleOAuthFieldsIndex < ActiveRecord::Migration[6.1] + disable_ddl_transaction! + + def change + add_index :users, :google_oauth2_username, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 87526bf7d..f1c616557 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1209,6 +1209,8 @@ ActiveRecord::Schema.define(version: 2021_12_22_040359) do t.string "forem_username" t.datetime "github_repos_updated_at", default: "2017-01-01 05:00:00" t.string "github_username" + t.datetime "google_oauth2_created_at" + t.string "google_oauth2_username" t.datetime "invitation_accepted_at" t.datetime "invitation_created_at" t.integer "invitation_limit" @@ -1270,6 +1272,7 @@ ActiveRecord::Schema.define(version: 2021_12_22_040359) do t.index ["facebook_username"], name: "index_users_on_facebook_username" t.index ["feed_fetched_at"], name: "index_users_on_feed_fetched_at" t.index ["github_username"], name: "index_users_on_github_username", unique: true + t.index ["google_oauth2_username"], name: "index_users_on_google_oauth2_username" t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true t.index ["invitations_count"], name: "index_users_on_invitations_count" t.index ["invited_by_id"], name: "index_users_on_invited_by_id" diff --git a/db/seeds.rb b/db/seeds.rb index 820b37e32..af335e5de 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -369,6 +369,8 @@ seeder.create_if_none(Broadcast) do "Consider connecting it.", github_connect: "You're on a roll! 🎉 Do you have a GitHub account? " \ "Consider connecting it so you can pin any of your repos to your profile.", + google_oauth2_connect: "You're on a roll! 🎉 Do you have a Google account? " \ + "Consider connecting it.", apple_connect: "You're on a roll! 🎉 Do you have an Apple account? " \ "Consider connecting it.", customize_feed: diff --git a/lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb b/lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb new file mode 100644 index 000000000..a449d908b --- /dev/null +++ b/lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb @@ -0,0 +1,17 @@ +module DataUpdateScripts + class InsertGoogleOuauth2ConnectBroadcastMessage + def run + return if Broadcast.find_by(title: "Welcome Notification: google_oauth2_connect") + + message = "You're on a roll! 🎉 Do you have a Google account? " \ + "Consider connecting it." + + Broadcast.create!( + title: "Welcome Notification: google_oauth2_connect", + processed_html: message, + type_of: "Welcome", + active: true, + ) + end + end +end diff --git a/spec/factories/broadcasts.rb b/spec/factories/broadcasts.rb index eaa5b3299..be4c80b93 100644 --- a/spec/factories/broadcasts.rb +++ b/spec/factories/broadcasts.rb @@ -39,6 +39,15 @@ FactoryBot.define do end end + factory :google_oauth2_connect_broadcast do + title { "Welcome Notification: google_oauth2_connect" } + type_of { "Welcome" } + processed_html do + "You're on a roll! 🎉 Do you have a Google account? Consider " \ + "connecting it." + end + end + factory :facebook_connect_broadcast do title { "Welcome Notification: facebook_connect" } type_of { "Welcome" } diff --git a/spec/helpers/authentication_helper_spec.rb b/spec/helpers/authentication_helper_spec.rb index c92a79ee5..69a1788fe 100644 --- a/spec/helpers/authentication_helper_spec.rb +++ b/spec/helpers/authentication_helper_spec.rb @@ -34,7 +34,7 @@ RSpec.describe AuthenticationHelper, type: :helper do allow(Authentication::Providers).to receive(:enabled).and_return(providers) allow(user).to receive(:identities).and_return(user.identities.where(provider: providers)) - expect(helper.signed_up_with(user)).to match(/GitHub and Twitter/) + expect(helper.signed_up_with(user)).to match(/Google and Twitter/) expect(helper.signed_up_with(user)).to match(/use any of those/) end diff --git a/spec/lib/data_update_scripts/insert_google_ouauth2_connect_broadcast_message_spec.rb b/spec/lib/data_update_scripts/insert_google_ouauth2_connect_broadcast_message_spec.rb new file mode 100644 index 000000000..d6f677721 --- /dev/null +++ b/spec/lib/data_update_scripts/insert_google_ouauth2_connect_broadcast_message_spec.rb @@ -0,0 +1,19 @@ +require "rails_helper" +require Rails.root.join( + "lib/data_update_scripts/20220107215659_insert_google_ouauth2_connect_broadcast_message.rb", +) + +describe DataUpdateScripts::InsertGoogleOuauth2ConnectBroadcastMessage do + it "works without a Broadcast" do + expect do + described_class.new.run + end.to change(Broadcast, :count).by(1) + end + + it "works when a Broadcast already exists" do + create(:google_oauth2_connect_broadcast) + expect do + described_class.new.run + end.not_to change(Broadcast, :count) + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 76dd00371..cd5dc94b9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -453,7 +453,7 @@ RSpec.describe User, type: :model do case provider_name when :apple expect(new_user.username).to match(/valid_username_\w+/) - when :facebook + when :facebook, :google_oauth2 expect(new_user.username).to match(/fname_lname_\S*\z/) else expect(new_user.username).to eq("valid_username") @@ -472,7 +472,7 @@ RSpec.describe User, type: :model do case provider_name when :apple expect(new_user.username).to match(/invalidusername_\w+/) - when :facebook + when :facebook, :google_oauth2 expect(new_user.username).to match(/fname_lname_\S*\z/) else expect(new_user.username).to eq("invalidusername") diff --git a/spec/services/authentication/providers_spec.rb b/spec/services/authentication/providers_spec.rb index 370548989..0420c98c8 100644 --- a/spec/services/authentication/providers_spec.rb +++ b/spec/services/authentication/providers_spec.rb @@ -24,15 +24,35 @@ RSpec.describe Authentication::Providers, type: :service do ) expect(is_subclass_of).to be(true) end + + it "handles spaces in provider names" do + allow(Settings::Authentication).to receive(:providers).and_return(%w[google_oauth2]) + + expect do + described_class.get!(:google_oauth2) + end.not_to raise_error + end end describe ".available" do it "lists the available providers" do - available_providers = %i[apple facebook forem github twitter] + available_providers = %i[apple facebook forem github google_oauth2 twitter] expect(described_class.available).to eq(available_providers) end end + describe ".availble_providers" do + it "lists the available providers" do + available_providers = [Authentication::Providers::Forem, + Authentication::Providers::Facebook, + Authentication::Providers::Apple, + Authentication::Providers::Twitter, + Authentication::Providers::GoogleOauth2, + Authentication::Providers::Github].sort_by(&:name) + expect(described_class.available_providers).to eq(available_providers) + end + end + describe ".enabled" do context "when one of the available providers is disabled" do it "only lists those that remain enabled" do diff --git a/spec/services/broadcasts/welcome_notification/generator_spec.rb b/spec/services/broadcasts/welcome_notification/generator_spec.rb index 429831dc0..b87a5022b 100644 --- a/spec/services/broadcasts/welcome_notification/generator_spec.rb +++ b/spec/services/broadcasts/welcome_notification/generator_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do let!(:facebook_connect_broadcast) { create(:facebook_connect_broadcast) } let!(:apple_connect_broadcast) { create(:apple_connect_broadcast) } let!(:forem_connect_broadcast) { create(:forem_connect_broadcast) } + let!(:google_oauth2_connect_broadcast) { create(:google_oauth2_connect_broadcast) } let!(:customize_feed_broadcast) { create(:customize_feed_broadcast) } let!(:discuss_and_ask_broadcast) { create(:discuss_and_ask_broadcast) } let!(:customize_ux_broadcast) { create(:customize_ux_broadcast) } @@ -70,6 +71,7 @@ RSpec.describe Broadcasts::WelcomeNotification::Generator, type: :service do twitter_connect_broadcast, apple_connect_broadcast, forem_connect_broadcast, + google_oauth2_connect_broadcast, ].include?(user.notifications.last.notifiable) expect(not_github).to be(true) diff --git a/spec/support/omniauth_helpers.rb b/spec/support/omniauth_helpers.rb index 878e98286..06cf759c5 100644 --- a/spec/support/omniauth_helpers.rb +++ b/spec/support/omniauth_helpers.rb @@ -135,6 +135,55 @@ module OmniauthHelpers }, ).freeze + OMNIAUTH_PAYLOAD_GOOGLE_OAUTH2 = OmniAuth::AuthHash::InfoHash.new( + { + "provider" => "google_oauth2", + "uid" => "100000000000000000000", + "info" => { + "name" => "fname lname", + "email" => "john@example.com", + "first_name" => "fname", + "last_name" => "lname", + "image" => "https://dummyimage.com/400x400.jpg", + "urls" => { + "google" => "https://plus.google.com/+JohnSmith" + } + }, + "credentials" => { + "token" => "TOKEN", + "refresh_token" => "REFRESH_TOKEN", + "expires_at" => 1_496_120_719, + "expires" => true + }, + "extra" => { + "id_token" => "ID_TOKEN", + "id_info" => { + "azp" => "APP_ID", + "aud" => "APP_ID", + "sub" => "100000000000000000000", + "email" => "john@example.com", + "email_verified" => true, + "at_hash" => "HK6E_P6Dh8Y93mRNtsDB1Q", + "iss" => "accounts.google.com", + "iat" => 1_496_117_119, + "exp" => 1_496_120_719 + }, + "raw_info" => { + "sub" => "100000000000000000000", + "name" => "fname lname", + "given_name" => "fname", + "family_name" => "lname", + "profile" => "https://plus.google.com/+JohnSmith", + "picture" => "https://dummyimage.com/400x400.jpg", + "email" => "john@example.com", + "email_verified" => "true", + "locale" => "en", + "hd" => "company.com" + } + } + }, + ).freeze + def omniauth_setup_invalid_credentials(provider) OmniAuth.config.mock_auth[provider] = :invalid_credentials end @@ -198,6 +247,10 @@ module OmniauthHelpers OmniAuth.config.mock_auth[:apple] = OMNIAUTH_PAYLOAD_APPLE.dup end + def omniauth_mock_google_oauth2_payload + OmniAuth.config.mock_auth[:google_oauth2] = OMNIAUTH_PAYLOAD_GOOGLE_OAUTH2.dup + end + def omniauth_mock_github_payload info = OMNIAUTH_BASIC_INFO[:info].merge( image: "https://dummyimage.com/400x400.jpg", diff --git a/spec/system/authentication/creator_config_edit_spec.rb b/spec/system/authentication/creator_config_edit_spec.rb index 10cfbda49..13a2fef31 100644 --- a/spec/system/authentication/creator_config_edit_spec.rb +++ b/spec/system/authentication/creator_config_edit_spec.rb @@ -16,8 +16,8 @@ RSpec.describe "Creator config edit", type: :system, js: true do click_on("Show info", match: :first) end - Authentication::Providers.available.each do |provider| - element = find(".config-authentication__item--label", text: /#{provider}/i) + Authentication::Providers.available_providers.each do |provider| + element = find(".config-authentication__item--label", text: /#{provider.official_name}/i) expect(element).not_to be_nil end end diff --git a/vendor/cache/omniauth-google-oauth2-1.0.0.gem b/vendor/cache/omniauth-google-oauth2-1.0.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..dba15dc313c3c93afcbb92c041ae37ecb032df09 GIT binary patch literal 22528 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 P{#oNs0)G