diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8b5afb7d1..aad92c0f2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -186,12 +186,6 @@ Rails/ApplicationController: - 'app/controllers/images_controller.rb' - 'app/controllers/labels_controller.rb' -# Offense count: 1 -# Cop supports --auto-correct. -Rails/ApplicationMailer: - Exclude: - - 'app/mailers/usergroup_mailer.rb' - # Offense count: 7 # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible diff --git a/app/controllers/concerns/with_email_auth.rb b/app/controllers/concerns/with_email_auth.rb new file mode 100644 index 000000000..b49cd4e35 --- /dev/null +++ b/app/controllers/concerns/with_email_auth.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Based on https://github.com/weg-li/weg-li/blob/master/app/controllers/sessions_controller.rb +# Original author: https://github.com/phoet + +module WithEmailAuth + def email; end + + def email_login + email = normalize_email(params[:email]) + if email.present? && valid_looking_email?(email) + token = EmailAuthToken.generate(email) + from = Whitelabel[:email] + label_name = t("label.#{Whitelabel[:label_id]}.name") + label_link = Whitelabel[:canonical_url] + UserMailer.login_link(email, token, from, I18n.locale, + label_name, label_link).deliver_later + + redirect_to root_path, notice: t('email_auth.email_sent', email:) + else + flash.now[:alert] = t('email_auth.invalid_email') + + render :email, status: :unprocessable_entity + end + end + + protected + + def normalize_email(email) + email.to_s.strip.downcase + end + + def valid_looking_email?(email) + email.match(/\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i) + end +end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index eedc5102b..87105b75d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class SessionsController < ApplicationController + include WithEmailAuth + def offline_login user = User.find_by(nickname: params[:nickname]) sign_in(user) @@ -14,7 +16,8 @@ def create begin authorization = Authorization.handle_authorization(current_user, request.env['omniauth.auth']) sign_in(authorization.user) - options = { notice: t('flash.logged_in', name: current_user.name) } + user_name = current_user.missing_name? ? '' : current_user.name + options = { notice: t('flash.logged_in', name: user_name) } rescue User::DuplicateNickname => e options = { alert: t('flash.duplicate_nick', name: e.nickname) } end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4625cd581..73b2e5e23 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -12,7 +12,12 @@ def index; end def show; end - def edit; end + def edit + return unless current_user.missing_name? + + user.name = nil + user.errors.add(:name, :required) + end def calendar respond_to do |format| diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2ac053949..588bb83f9 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -13,7 +13,13 @@ def cache_image_path(model) end def login_providers - %w[twitter github google_oauth2] + %w[twitter github google_oauth2 email] + end + + def icon_for_provider(provider) + return 'envelope' if provider == 'email' + + provider end def whitelabel_stylesheet_link_tag @@ -81,6 +87,10 @@ def hint(close = true) end end + def user_name(user) + user.display_name + end + private def markdown_parser diff --git a/app/helpers/link_helper.rb b/app/helpers/link_helper.rb index 15983d934..97f59a8ac 100644 --- a/app/helpers/link_helper.rb +++ b/app/helpers/link_helper.rb @@ -2,14 +2,14 @@ module LinkHelper def link_to_user(user, image: false, image_class: nil) - link_to(user, title: user.name) do - image ? user_image(user, image_class:) + user.name : user.name + link_to(user, title: user_name(user)) do + image ? user_image(user, image_class:) + user_name(user) : user_name(user) end end def user_image(user, image_class: nil) image_class ||= 'small-user-image' - image_tag(cache_image_path(user), title: user.name, class: image_class) + image_tag(cache_image_path(user), title: user_name(user), class: image_class) end def link_to_job(job) diff --git a/app/lib/email_auth_token.rb b/app/lib/email_auth_token.rb new file mode 100644 index 000000000..b82b9ae52 --- /dev/null +++ b/app/lib/email_auth_token.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Based on https://github.com/weg-li/weg-li/blob/master/app/lib/token.rb +# Original author: https://github.com/phoet + +class EmailAuthToken + def self.generate( + email, + expiration: 15.minutes, + secret: Rails.application.secrets.secret_key_base + ) + now_seconds = Time.now.to_i + payload = { iss: email, iat: now_seconds, exp: now_seconds + expiration } + token = ::JWT.encode(payload, secret, 'HS256') + Base64.encode64(token) + end + + def self.decode(string, secret: Rails.application.secrets.secret_key_base) + token = Base64.decode64(string) + JWT.decode(token, secret, true, algorithm: 'HS256').first + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 000000000..26148f2d6 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb new file mode 100644 index 000000000..3ddfb94e9 --- /dev/null +++ b/app/mailers/user_mailer.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Based on https://github.com/weg-li/weg-li/blob/master/app/mailers/user_mailer.rb +# Original author: https://github.com/phoet + +class UserMailer < ApplicationMailer + def login_link(email, token, from, locale, label_name, label_link) # rubocop:disable Metrics/ParameterLists + @token = token + @from = from + @label_name = label_name + @label_link = label_link + + I18n.with_locale(locale) do + mail from: @from, to: email, subject: t('email_auth.subject', label: label_name) + end + end +end diff --git a/app/mailers/usergroup_mailer.rb b/app/mailers/usergroup_mailer.rb index b57ca1e09..e04fdb8b5 100644 --- a/app/mailers/usergroup_mailer.rb +++ b/app/mailers/usergroup_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class UsergroupMailer < ActionMailer::Base +class UsergroupMailer < ApplicationMailer def invitation_mail(event) @event = event options = { diff --git a/app/models/user.rb b/app/models/user.rb index 0e7865dfb..f425c6189 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class User < ApplicationRecord +class User < ApplicationRecord # rubocop:disable Metrics/ClassLength include Slug extend ApiHandling slugged_by(:nickname) @@ -84,10 +84,49 @@ def handle_google_oauth2_attributes(hash) self.image = hash['info']['image'] end + def handle_email_attributes(hash) + received_email = hash['info']['email'] + + self.nickname = nickname_from_email(received_email) unless nickname + self.name = name_from_email(received_email) unless name + self.image = image_from_email(received_email) unless image + self.email = received_email + end + + def hash_for_email(email) + Digest::SHA256.new.hexdigest(email) + end + + def nickname_from_email(email) + hash_for_email(email) + end + + def hide_nickname? + nickname == nickname_from_email(email) + end + + EMPTY_NAME = '********' + + def name_from_email(_email) + EMPTY_NAME + end + + def image_from_email(email) + "https://www.gravatar.com/avatar/#{hash_for_email(email)}" + end + def with_provider?(provider) authorizations.map(&:provider).include?(provider) end + def missing_name? + name == EMPTY_NAME + end + + def display_name + missing_name? ? '-' : name + end + class DuplicateNickname < StandardError attr_reader :nickname diff --git a/app/views/application/_hint.slim b/app/views/application/_hint.slim index 175a13570..6544c5f62 100644 --- a/app/views/application/_hint.slim +++ b/app/views/application/_hint.slim @@ -25,3 +25,11 @@ li.mb-4 span.job-toggle= link_to fa_icon("chevron-up"), '#', title: t("hint.click_to_refresh") if index == 0 .job.ml-4== job_description(job) + +- if current_user&.missing_name? + = hint(false) do + .highlights + => fa_icon("exclamation-triangle") + strong=> t("flash.update_profile_details") + - unless current_page?(edit_user_path(current_user)) + = link_to t("login.edit_profile"), edit_user_path(current_user) diff --git a/app/views/application/_nav.slim b/app/views/application/_nav.slim index 889186c3d..4276d24f9 100644 --- a/app/views/application/_nav.slim +++ b/app/views/application/_nav.slim @@ -43,7 +43,7 @@ nav.navbar.sticky-top.navbar-expand-lg#nav .dropdown-menu.dropdown-menu-right(aria-labelledby="loginDropdown") - login_providers.each do |provider| = button_to(label_auth_url(provider), class: 'dropdown-item') do - = fa_icon(provider, class: 'fa-fw', text: t("login.#{provider}_login")) + = fa_icon(icon_for_provider(provider), class: 'fa-fw', text: t("login.#{provider}_login")) li.nav-item.dropdown.pr-4 diff --git a/app/views/sessions/email.slim b/app/views/sessions/email.slim new file mode 100644 index 000000000..51eaa5757 --- /dev/null +++ b/app/views/sessions/email.slim @@ -0,0 +1,9 @@ +section + h3= t("email_auth.header") + + .d-flex.justify-content-center + = form_tag email_login_path do |form| + legend= t('email_auth.enter_email') + - email = signed_in? ? current_user.email : params[:email] + = email_field_tag :email, email, placeholder: 'me@example.com', required: true + = submit_tag t('email_auth.submit'), class: 'btn-primary' diff --git a/app/views/sessions/index.slim b/app/views/sessions/index.slim index cf525383c..2b710e0a8 100644 --- a/app/views/sessions/index.slim +++ b/app/views/sessions/index.slim @@ -6,4 +6,4 @@ section - login_providers.each do |provider| li.list-group-item = button_to(label_auth_url(provider)) do - = fa_icon(provider, class: 'fa-fw dropdown-list-icon', text: t("login.#{provider}_login")) + = fa_icon(icon_for_provider(provider), class: 'fa-fw dropdown-list-icon', text: t("login.#{provider}_login")) diff --git a/app/views/user_mailer/login_link.text.erb b/app/views/user_mailer/login_link.text.erb new file mode 100644 index 000000000..6ed826a9c --- /dev/null +++ b/app/views/user_mailer/login_link.text.erb @@ -0,0 +1,11 @@ +<%= t("email_auth.body.salute") %>, + +<%= t("email_auth.body.intro", label: @label_name) %>, + +<%= provider_callback_url(provider: :email, token: @token, host: @label_link) %> + +<%= t("email_auth.body.final_details") %> + +-- +<%= @label_name %> +<%= @label_link %> diff --git a/app/views/users/edit.slim b/app/views/users/edit.slim index 158b8890f..bb1734e34 100644 --- a/app/views/users/edit.slim +++ b/app/views/users/edit.slim @@ -27,7 +27,7 @@ section - login_providers.each do |provider| li = button_to label_auth_url(provider), title: t("login.#{provider}_login"), class: "btn btn-#{existing_providers.include?(provider) ? 'disabled' : 'secondary'}", disabled: existing_providers.include?(provider) do - = fa_icon provider, class: 'fa-fw dropdown-list-icon' + = fa_icon icon_for_provider(provider), class: 'fa-fw dropdown-list-icon' => t("login.#{provider}_login") - if existing_providers.include? provider = fa_icon 'check', class: 'fa-fw dropdown-list-icon' diff --git a/app/views/users/show.slim b/app/views/users/show.slim index 71c4cebf6..0f18ec0da 100644 --- a/app/views/users/show.slim +++ b/app/views/users/show.slim @@ -22,7 +22,8 @@ .card-body h2.card-title = user_image(user) - = "#{user.name} (#{user.nickname})" + = "#{user_name(user)}" + = " (#{user.nickname})" unless user.hide_nickname? small.text-muted span>= I18n.tw("profile.freelancer") if user.freelancer? span>= "(#{t("profile.available")})" if user.available? diff --git a/config/environments/test.rb b/config/environments/test.rb index f70643207..31d6a198a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -56,4 +56,7 @@ # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true + + # For testing async jobs + config.active_job.queue_adapter = :test end diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 4558a7cae..7b7e6061d 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,3 +1,5 @@ +require_relative '../../lib/omni_auth/strategies/email' + OmniAuth.config.logger = Rails.logger Rails.application.config.middleware.use OmniAuth::Builder do @@ -14,4 +16,5 @@ env['omniauth.strategy'].options[:client_secret] = ENV["#{name}_SECRET"] end, } + provider :email end diff --git a/config/locales/de.yml b/config/locales/de.yml index 79f2ac77e..20f985c01 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -172,3 +172,17 @@ de: previous: "<" next: ">" truncate: "..." + email_auth: + header: "Mit E-Mail anmelden" + enter_email: "Bitte gib eine gültige E-Mail-Adresse ein" + submit: "Autorisieren" + email_sent: "Eine E-Mail wurde an %{email} mit Details zum Einloggen gesendet" + invalid_email: "Bitte gib eine gültige E-Mail-Adresse ein" + subject: "Anmeldung bei %{label}" + body: + salute: "Hallo" + intro: "Verwende diesen Link, um dich bei %{label} anzumelden" + good_bye: "Mit freundlichen Grüßen" + final_details: >- + Dieser Link ist nur 15 Minuten gültig. + Wenn du nicht auf den Link klicken kannst, kopiere ihn einfach und füge ihn in die Adresszeile deines Browsers ein. diff --git a/config/locales/en.yml b/config/locales/en.yml index 4ff1bb6dd..f544408f6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -160,6 +160,7 @@ en: topic_added: "Added new Topic." topic_updated: "Updated the Topic." add_email: "Please add an email address, so that we can get in contact!" + update_profile_details: "Please, fill in your name so that we can refer to you!" mobile: settings: "Settings" back: "Back" @@ -172,3 +173,17 @@ en: previous: "<" next: ">" truncate: "..." + email_auth: + header: "Login with email" + enter_email: "Please, enter your email" + submit: "Authorize" + email_sent: "An email has been sent to %{email} with details for logging in" + invalid_email: "Please enter a valid email address" + subject: "Login to %{label}" + body: + salute: "Hi" + intro: "Use this link to log into %{label}" + good_bye: "Best" + final_details: >- + This link will be valid just for 15 minutes. + If you cannot click on the link, just copy and paste it into your browser address bar. diff --git a/config/locales/es.yml b/config/locales/es.yml index 10bd1737d..a2da8fe57 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -57,7 +57,7 @@ es: route: "Mapa" material: "Material" description: "Infos" - no_location: "¡Estamos bustando sitio!" + no_location: "¡Estamos buscando sitio!" home: the_usergroup: "%{usergroup} es un grupo de usuarios, grupo de interés o simplemente de personas interesadas en Ruby. Contacta con nosotros en la siguiente reunión! Todo el mundo es bienvenido, incluso si no tienes mucha experiencia con Ruby." like_to_talk: "Quieres dar una charla en el grupo, o quieres proponer un tema para una?" @@ -173,3 +173,17 @@ es: previous: "<" next: ">" truncate: "..." + email_auth: + header: "Acceder por Email" + enter_email: "Por favor, introduce tu email" + submit: "Autorizar" + email_sent: "Te hemos enviado un email a %{email} con los detalles para acceder" + invalid_email: "Por favor, asegúrate de que el email es correcto" + subject: "Accede a %{label}" + body: + salute: "Hola" + intro: "Usa este enlace para acceder a %{label}" + good_bye: "Saludos" + final_details: >- + Este enlace es válido sólo durante 15 minutos. + Si no puedes pulsar sobre el enlace, puedes copiarlo y pegarlo en la barra de tu navegador. diff --git a/config/locales/pl.yml b/config/locales/pl.yml index c41cdae23..de5a89a67 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -181,3 +181,17 @@ pl: - czwartek - piątek - sobotę + email_auth: + header: "Zaloguj się przez email" + enter_email: "Proszę, wprowadź swój email" + submit: "Autoryzuj" + email_sent: "Email został wysłany na adres %{email} z detalami do logowania" + invalid_email: "Proszę wprowadzić prawidłowy adres e-mail" + subject: "Logowanie do %{label}" + body: + salute: "Cześć" + intro: "Użyj tego linku, aby zalogować się do %{label}" + good_bye: "Najlepsze życzenia" + final_details: >- + Ten link będzie ważny tylko przez 15 minut. + Jeśli nie możesz kliknąć na link, skopiuj go i wklej w pasek adresu przeglądarki. diff --git a/config/routes.rb b/config/routes.rb index 0b718bf84..8a04b5b83 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,12 +55,17 @@ scope '/auth' do get '/', to: 'sessions#index', as: :login - get '/:provider/callback', to: 'sessions#create' + get '/:provider/callback', to: 'sessions#create', as: :provider_callback get '/failure', to: 'sessions#failure' get '/destroy_session', to: 'sessions#destroy', as: :destroy_session get '/offline_login/:nickname', to: 'sessions#offline_login' if Rails.env.development? end + scope "/sessions" do + get "/email", to: "sessions#email" + post "/email_login", to: "sessions#email_login" + end + constraints MainDomainConstraint.new do get '/', to: 'labels#index', as: :labels end diff --git a/lib/omni_auth/strategies/email.rb b/lib/omni_auth/strategies/email.rb new file mode 100644 index 000000000..9e55e930c --- /dev/null +++ b/lib/omni_auth/strategies/email.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Based on https://github.com/weg-li/weg-li/blob/master/app/lib/omni_auth/strategies/email.rb +# Original author: https://github.com/phoet + +require 'base64' + +module OmniAuth + module Strategies + class Email + include OmniAuth::Strategy + + def request_phase + redirect '/sessions/email' + end + + def callback_phase + token = request.params['token'] + fail!(:authenticity_error) if token.blank? + + # Not catching the exception until this is more tested + # This way we'll get it reported in AppSignal for diagnosing + decoded_token = EmailAuthToken.decode(token) + + @email = decoded_token['iss'].to_s.downcase + fail!(:authenticity_error) if @email.blank? + + super + end + + uid { Digest::SHA256.new.hexdigest(@email) } + + info { { 'email' => @email } } + end + end +end diff --git a/readme.md b/readme.md index ab4e03d5e..77f88f2f1 100644 --- a/readme.md +++ b/readme.md @@ -175,12 +175,36 @@ In order to register for events, users need to log in first. The app uses OAuth for that. At this point, it accepts Twitter [^1], GitHub and Google as valid OAuth providers. +It also accepts email as a method for registering and logging in, using a custom OAuth provider based on one time passwords (OTP) sent to that email. + When a user is not currently logged in, selecting one of the login providers creates an account for him/her, attaching this provider as a valid auth mechanism. When a user is already logged in, selecting another provider will add that auth mechanism to the existing user. [^1]: As of 2024 Twitter/X has deprecated API v1.1 and Twitter login is not working anymore +### Required used information + +At this point, the app marks `nickname`, `name` and `image` as required, not allowing a user to be created if any of those are empty. `email` is not required. + +For external OAuth sources, values for those 3 properties are obtained from the providers. + +- When a user is created through them, it gets these (and other) properties from that source. + +- When an existing user adds one of those providers, some of these values are overwritten (`name` and `image`), while `nickname` stays always the same. + +For users registered through email OTP, none of these values are available on creation. In order to deal with the validations, they're initialized as follows: + +- `nickname` is generated by hashing the email address. + +- `name` is filled in with a known marker for "missing name" declared in `User::EMPTY_NAME` (currently `"********"`). It doesn't use the email address as a placeholder in order to avoid exposing it, as the user name is displayed in several pages in the application. + +- `image` is set to the Gravatar URL for the email. + +In order to urge users registered through email to provide a name that can be used in the app, after log in, a user with such an "empty" name is redirected to the profile edit page and asked to provide one. + +As with any other source, if such a user later adds another provider, the `name` and `image` coming from those will take over the existing ones. + ### Recovering login for existing users who only had Twitter auth As a consequence of X deprecating API v1.1, Twitter authentication is not working anymore and registered users that only had defined Twitter as their auth method are left out in the cold. diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 76a6ca06a..5cb69a294 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -29,10 +29,38 @@ end context 'GET :destroy' do - it 'destroys a user session' do + it 'destroys a user session', :aggregate_failures do get :destroy expect(response).to redirect_to(root_path) expect(flash[:notice]).not_to be_nil end end + + context 'for email auth' do + before do + request.env['omniauth.auth'] = EMAIL_AUTH_HASH + request.host = 'hamburg.onruby.de' + end + + context 'POST :email_login' do + it 'sends the email and redirect to index', :aggregate_failures do + expect { post :email_login, params: { email: 'user@example.org' } } + .to have_enqueued_job(ActionMailer::DeliveryJob) + + expect(response).to redirect_to(root_path) + end + + it 'does not send the email if param missing', :aggregate_failures do + expect { post :email_login } + .not_to have_enqueued_job(ActionMailer::DeliveryJob) + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'does not send the email looks bad', :aggregate_failures do + expect { post :email_login, params: { email: 'user@org' } } + .not_to have_enqueued_job(ActionMailer::DeliveryJob) + expect(response).to have_http_status(:unprocessable_entity) + end + end + end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 6b777eedf..0b161f7a0 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -34,6 +34,22 @@ expect(flash[:alert]).not_to be_nil expect(response).to redirect_to(root_path) end + + context 'if the user has an unfilled name' do + render_views + + it 'clears the name input and sets it to error' do + allow(@controller).to receive_messages(current_user: user) + user.update(name: User::EMPTY_NAME) + + get :edit, params: { id: user } + + doc = Nokogiri::HTML(response.body) + input = doc.at_css('.form-group.user_name.form-group-invalid input') + expect(input).not_to be_nil + expect(input[:value].to_s.strip).to eq('') + end + end end context 'GET :calendar' do diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb new file mode 100644 index 000000000..d15c0b891 --- /dev/null +++ b/spec/mailers/user_mailer_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe UserMailer do + describe '#login_link' do + subject(:mail) do + described_class.login_link(email, token, from, locale, label_name, label_link).deliver_now + end + + let(:email) { 'user@example.com' } + let(:token) { '12345678' } + let(:from) { 'no-reply@example.com' } + let(:locale) { :en } + let(:label_name) { 'My RUG' } + let(:label_link) { 'http://rug.org' } + + it 'renders the headers' do + expect(mail.subject).to eq('Login to My RUG') + expect(mail.to).to eq([email]) + expect(mail.from).to eq([from]) + end + + it 'renders the body' do + link = provider_callback_url(provider: :email, + token:, host: label_link) + + expect(mail.body.encoded).to include(link) + expect(mail.body.encoded).to include(label_name) + expect(mail.body.encoded).to include(label_link) + end + + context 'when using different locale' do + let(:locale) { :es } + + it 'uses the correct locale for the subject' do + expect(mail.subject).to eq('Accede a My RUG') + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 636b6f876..d218f666c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -57,6 +57,12 @@ expect(user.email).to eql('phoetmail@googlemail.com') end + it 'sets email and empty name for users created by email' do + user = User.create_from_hash!(EMAIL_AUTH_HASH) + expect(user.email).to eql('user@example.org') + expect(user.name).to eql(User::EMPTY_NAME) + end + it 'raises an error for same nickname but different auths' do User.create_from_hash!(TWITTER_AUTH_HASH) expect do @@ -85,6 +91,12 @@ expect(it.url).to eql('http://blog.nofail.de') end end + + it 'updates only the email from email-auth-hash', :aggregate_failures do + expect { user.update_from_auth!(EMAIL_AUTH_HASH) } + .not_to(change { user.attributes.slice(:name, :github, :image, :location, :description, :url) }) + expect(user.email).to eq('user@example.org') + end end context 'finder' do diff --git a/spec/support/factories/authorizations.rb b/spec/support/factories/authorizations.rb index 053f10b85..a5110c672 100644 --- a/spec/support/factories/authorizations.rb +++ b/spec/support/factories/authorizations.rb @@ -53,6 +53,14 @@ }, }.freeze +EMAIL_AUTH_HASH = { + 'provider' => 'email', + 'uid' => 'f8a846d3199e68bb5a7d9ba9dab54ddc9c28e382b5c4490b396214808d26aeb9', + 'info' => { + 'email' => 'user@example.org', + }, +}.freeze + FactoryBot.define do factory :authorization do user diff --git a/spec/views/users/show.html_spec.rb b/spec/views/users/show.html_spec.rb index cdc87ebcb..648a0dd0a 100644 --- a/spec/views/users/show.html_spec.rb +++ b/spec/views/users/show.html_spec.rb @@ -2,12 +2,30 @@ describe 'users/show' do let(:user) { build(:user, id: 123) } + let(:dom) { Nokogiri::HTML(rendered) } - it 'renders successfully' do - allow(view).to receive_messages(current_user: user, user:) + before { allow(view).to receive_messages(current_user: user, user:) } + it 'renders successfully' do render - expect(rendered).to include(user.nickname) + expect(dom.at_css('.card .card-body .card-title').inner_text).to include(user.nickname) + end + + context 'when user was just created through email OTP' do + let(:user) { User.create_from_hash!(EMAIL_AUTH_HASH) } + + it 'hides nickname' do + render + + expect(dom.at_css('.card .card-body .card-title').inner_text).not_to include(user.nickname) + end + + it 'shows name placeholder', :aggregate_failures do + render + + expect(dom.at_css('.card .card-body .card-title').inner_text).not_to include(user.name) + expect(dom.at_css('.card .card-body .card-title').inner_text).to include('-') + end end end