Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/graphql_devise/default_operations/mutations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'graphql_devise/mutations/login'
require 'graphql_devise/mutations/logout'
require 'graphql_devise/mutations/resend_confirmation'
require 'graphql_devise/mutations/resend_confirmation_with_token'
require 'graphql_devise/mutations/send_password_reset'
require 'graphql_devise/mutations/send_password_reset_with_token'
require 'graphql_devise/mutations/sign_up'
Expand All @@ -24,6 +25,7 @@ module DefaultOperations
send_password_reset: { klass: GraphqlDevise::Mutations::SendPasswordReset, authenticatable: false },
send_password_reset_with_token: { klass: GraphqlDevise::Mutations::SendPasswordResetWithToken, authenticatable: false },
resend_confirmation: { klass: GraphqlDevise::Mutations::ResendConfirmation, authenticatable: false },
resend_confirmation_with_token: { klass: GraphqlDevise::Mutations::ResendConfirmationWithToken, authenticatable: false },
confirm_registration_with_token: { klass: GraphqlDevise::Mutations::ConfirmRegistrationWithToken, authenticatable: true }
}.freeze
end
Expand Down
44 changes: 44 additions & 0 deletions lib/graphql_devise/mutations/resend_confirmation_with_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module GraphqlDevise
module Mutations
class ResendConfirmationWithToken < Base
argument :email, String, required: true
argument :confirm_url, String, required: true

field :message, String, null: false

def resolve(email:, confirm_url:)
check_redirect_url_whitelist!(confirm_url)

resource = find_confirmable_resource(email)

if resource
yield resource if block_given?

if resource.confirmed? && !resource.pending_reconfirmation?
raise_user_error(I18n.t('graphql_devise.confirmations.already_confirmed'))
end

resource.send_confirmation_instructions(
redirect_url: confirm_url,
template_path: ['graphql_devise/mailer']
)

{ message: I18n.t('graphql_devise.confirmations.send_instructions', email: email) }
else
raise_user_error(I18n.t('graphql_devise.confirmations.user_not_found', email: email))
end
end

private

def find_confirmable_resource(email)
email_insensitive = get_case_insensitive_field(:email, email)
resource = find_resource(:unconfirmed_email, email_insensitive) if resource_class.reconfirmable
resource ||= find_resource(:email, email_insensitive)
resource
end
end
end
end
7 changes: 4 additions & 3 deletions spec/dummy/app/graphql/dummy_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ class DummySchema < GraphQL::Schema
public_introspection: true,
resource_loaders: [
GraphqlDevise::ResourceLoader.new(
'User',
User,
only: [
:login,
:confirm_account,
:send_password_reset,
:resend_confirmation,
:resend_confirmation_with_token,
:check_password_token
]
),
GraphqlDevise::ResourceLoader.new('Guest', only: [:logout]),
GraphqlDevise::ResourceLoader.new('SchemaUser')
GraphqlDevise::ResourceLoader.new(Guest, only: [:logout]),
GraphqlDevise::ResourceLoader.new(SchemaUser)
]
)

Expand Down
137 changes: 137 additions & 0 deletions spec/requests/mutations/resend_confirmation_with_token_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'Resend confirmation with token' do
include_context 'with graphql query request'

let(:confirmed_at) { nil }
let!(:user) { create(:user, confirmed_at: nil, email: 'mwallace@wallaceinc.com') }
let(:email) { user.email }
let(:id) { user.id }
let(:confirm_url) { 'https://google.com' }
let(:query) do
<<-GRAPHQL
mutation {
userResendConfirmationWithToken(
email:"#{email}",
confirmUrl:"#{confirm_url}"
) {
message
}
}
GRAPHQL
end

context 'when confirm_url is not whitelisted' do
let(:confirm_url) { 'https://not-safe.com' }

it 'returns a not whitelisted confirm url error' do
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)

expect(json_response[:errors]).to containing_exactly(
hash_including(
message: "Redirect to '#{confirm_url}' not allowed.",
extensions: { code: 'USER_ERROR' }
)
)
end
end

context 'when params are correct' do
context 'when using the gem schema' do
it 'sends an email to the user with confirmation url and returns a success message' do
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
)

email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
confirm_link = email.css('a').first['href']
confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]

expect(User.confirm_by_token(confirm_token)).to eq(user)
end
end

context 'when using a custom schema' do
let(:custom_path) { '/api/v1/graphql' }

it 'sends an email to the user with confirmation url and returns a success message' do
expect { post_request(custom_path) }.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
)

email = Nokogiri::HTML(ActionMailer::Base.deliveries.last.body.encoded)
confirm_link = email.css('a').first['href']
confirm_token = confirm_link.match(/\?confirmationToken\=(?<token>.+)\z/)[:token]

expect(User.confirm_by_token(confirm_token)).to eq(user)
end
end

context 'when email address uses different casing' do
let(:email) { 'mWallace@wallaceinc.com' }

it 'honors devise configuration for case insensitive fields' do
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
)
end
end

context 'when the user has already been confirmed' do
before { user.confirm }

it 'does *NOT* send an email and raises an error' do
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
expect(json_response[:errors]).to contain_exactly(
hash_including(
message: 'Email was already confirmed, please try signing in',
extensions: { code: 'USER_ERROR' }
)
)
end
end
end

context 'when the email was changed' do
let(:confirmed_at) { 2.seconds.ago }
let(:email) { 'new-email@wallaceinc.com' }
let(:new_email) { email }

before do
user.update_with_email(
email: new_email,
schema_url: 'http://localhost/test',
confirmation_success_url: 'https://google.com'
)
end

it 'sends new confirmation email' do
expect { post_request }.to change(ActionMailer::Base.deliveries, :count).by(1)
expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(new_email)
expect(json_response[:data][:userResendConfirmationWithToken]).to include(
message: 'You will receive an email with instructions for how to confirm your email address in a few minutes.'
)
end
end

context "when the email isn't in the system" do
let(:email) { 'notthere@gmail.com' }

it 'does *NOT* send an email and raises an error' do
expect { post_request }.to not_change(ActionMailer::Base.deliveries, :count)
expect(json_response[:data][:userResendConfirmationWithToken]).to be_nil
expect(json_response[:errors]).to contain_exactly(
hash_including(
message: "Unable to find user with email '#{email}'.",
extensions: { code: 'USER_ERROR' }
)
)
end
end
end