In the last episode we started to implement the feature to allow a user to connect multiple social profiles to their Techlahoma account. Today we’ll finish that feature.
Currently, when we run cucumber we're seeing this:
$ cucumber -r features features/users/multiple_auth.feature:9
...
Then she should see "Would you like to make this public?" # features/step_definitions/users_steps.rb:24
expected to find text "Would you like to make this public?" in "Welcome Test User! Sign Out Home#index Find me in app/views/home/index.html.erb" (RSpec::Expectations::ExpectationNotMetError)
features/users/multiple_auth.feature:17:in `Then she should see "Would you like to make this public?"'
...
This step in the scenario covers the fact that we want to encourage people to make their account connections public when they return to Techlahoma after the OAuth dance. Essentially we just want to show the user their new authentication, and allow them to update it. This is a sub-set of the standard REST actions, so lets use a dedicated AuthenticationsController
to handle the job.
rails g controller authentications
Then we should add routes for the edit
and update
actions in config/routes.rb
.
resources :authentications, :only => [:edit,:update]
Then we can just stub the methods in the controller
class AuthenticationsController < ApplicationController
def edit
end
def create
end
end
Then we can add a template at app/views/authentications/edit.html.erb
Would you like to make this public?
Now we just need to redirect to this route when we have a newly created authentication. So the new create
method looks like this.
def create
authentication = Authentication.from_omniauth(env["omniauth.auth"],current_user)
user = authentication.user
session[:user_id] = user.id
if authentication.created_at > Time.now - 5.seconds
redirect_to edit_authentication_path(authentication)
else
redirect_to root_url, notice: "Signed in!"
end
end
Now cucumber lets us know that its time time actually build the part that will allow the user to make the connection public.
...
When she makes the provider public # features/users/multiple_auth.feature:18
Undefined step: "she makes the provider public" (Cucumber::Undefined)
features/users/multiple_auth.feature:18:in `When she makes the provider public'
...
Since the time that I first wrote the feature scenario I've had a chance to discuss this scenario with Rob. We agreed that the initial scenario called for functionality that we really don't need. We intentionally choose to use 3rd party authentication providers so that we can't be responsible for leaking passwords in a worst case scenario. Following on the idea that we don't want to be keeping secrets on behalf of our users we decided that we don't need to allow users to show and hide a social profile connection. Our policy will be that Techlahoma is a public community forum, and that everything that happens there is public. If you don't want people to see your Twitter profile on your Techlahoma page, just don't connect your Twitter account.
That means we get to trim down the scenario. Here's the new scenario after removing all of the steps that have to do with making a connection public.
Scenario: Add Twitter
Given a user signed in with GitHub
When she visits her profile
Then she should see "Add your Twitter account"
When she adds her Twitter account
Then User.count should == 1
Then Authentication.count should == 2
Then she should see "Remove Twitter"
Now we need to clean up a thing or two from earlier in this post. First we can clean up the redirect logic in the SessionsController#create
method and just redirect to the profile path.
def create
authentication = Authentication.from_omniauth(env["omniauth.auth"],current_user)
user = authentication.user
session[:user_id] = user.id
redirect_to profile_path, notice: "Signed in!"
end
After redirecting the user back to their profile we'll want to show them a "Remove Twitter" link in place of the current "Add your Twitter account" link. We'll have that link point to a destroy
on the AuthenticationsController
. We can also remove the edit and update methods from that controller and their routes from routes.rb
. So after making some changes the AuthenticationsController
looks like this.
class AuthenticationsController < ApplicationController
def destroy
end
end
And the authentications route looks like this.
resources :authentications, :only => [:destroy]
The profile template needs to determine if the current user has a Twitter authentication or not, so lets imagine that we have a method called authentication_for
and we'll use it in our template to do this:
<% if !current_user.authentication_for('twitter') %>
<%= link_to "Add your Twitter account", "/auth/twitter" %>
<% else %>
<%= link_to 'Remove Twitter', current_user.authentication_for('twitter'), :confirm => 'Are you sure?', :method => :delete %>
<% end %>
We can add a simple authentication_for
method to the User
model.
def authentication_for provider
authentications.where(:providere => provider).first
end
Now our entire scenario passes! Hooray! But wait, we haven't actually implemented the action for removing an account…. Our scenario was incomplete, so we need to add a few lines to it. We can just tack this onto the end of it.
When she removes her Twitter account
Then she should see "Add your Twitter account"
We already have a step definition for the second new line, and the definition for the first new one is easy to write.
When(/^she removes her Twitter account$/) do
click_on "Remove Twitter"
end
Now we just need to implement the delete method.
def destroy
authentication = current_user.authentications.find(params[:id])
authentication.destroy
redirect_to profile_path
end
Scenario complete!
Now we need to update the "Add GitHub" scenario to match the new requirements, and to be a complete scenario.
Scenario: Add GitHub
Given a user signed in with Twitter
When she visits her profile
Then she should see "Add your GitHub account"
When she adds her GitHub account
Then she should see "Remove GitHub"
When she removes her GitHub account
Then she should see "Add your GitHub account"
To implement all of the steps in this scenario we'll update some existing steps to be more flexible. We'll also extract some of the code in the application to be more general and to account for both Twitter and GitHub.
For the first step we can alter the existing step definition for "Given a user signed in with GitHub".
Given(/^a user signed in with (.*?)$/) do |service|
visit "/signin"
click_on "Sign In With #{service}"
end
Now the first failing step is Then she should see "Add your GitHub account"
. The existing profile template is a good pattern for what we want for a single service. So let's move the contents of that file into a new partial file called app/views/profile/_service_options.html.erb
. We'll also change all of the references to Twitter to use variables instead.
<% if !current_user.authentication_for(service) %>
<%= link_to "Add your #{friendly} account", "/auth/#{service}" %>
<% else %>
<%= link_to "Remove #{friendly}", current_user.authentication_for(service), :data => {:confirm => 'Are you sure?'}, :method => :delete %>
<% end %>
Now in profile/index.html.erb
we can just call that partial once for Twitter and once for GitHub.
<%= render 'service_options', :service => 'twitter', :friendly => 'Twitter' %>
<br/>
<%= render 'service_options', :service => 'github', :friendly => 'GitHub' %>
For the next step, When she adds her GitHub account
, we can again alter an existing step.
When(/^she adds her (.*?) account$/) do |service|
click_on "Add your #{service} account"
end
And finally, we can alter the When she removes her Twitter account
step to also handle GitHub.
When(/^she removes her (.*?) account$/) do |service|
click_on "Remove #{service}"
end
And that's it! Now we can allow users to log in with either Twitter or GitHub, and then after logging in they can add the other service to their account.