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'
...

Second Thoughts

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"

Cleaning up

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!

Adding GitHub

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.