This week I’m on a short break from school for the holidays and one of my goals for the week has been to get comfortable with the Strava API. Strava is my social media of choice and it also holds buckets of data for athletes, which makes it an interesting datasource to build an application off of. The best part about the Strava API is how good their documentation is. It is well organized, provides generic curl examples and describes the API interactions in a clear way that is easy for a beginner programmer like me to understand.
The first feature I wanted to build was to allow a user to create an account on my app, then allow them to authenticate that account with their own profile on Strava. Additionally, I needed to work out how to test this process. I had already messed around a little with using Faraday to access the API through my command-line, but the production app will need a full test suite. Before we get into testing let’s look at the Strava API authentication flow.
Strava Authentication Flow
The API docs outline the flow very well:
- Redirect user to the Strava authentication URL to sign-in and view the authorization page. A few parameters are passed in the URL to identify the app and give Strava a url to redirect back to.
- If the user authorizes the app, then Strava uses the provided url to redirect back to the app and passes a code as a parameter. If the user cancels the authorization they are still redirected back to the app, but are not given a code.
- The app must then take that code, along with the app ID and app secret token, then send it back to Strava.
- If the code, app ID and app secret token are all valid, then Strava returns a user authentication token along with the user’s basic profile.
- My app then saves the user authentication token as well as the Strava profile ID in the user profile.
Test Environment Setup
Make sure the test environment database is setup (I’m using PostreSQL) and it is migrated. These are the gems I’m using to contact the API and run my test suite:
- Faraday — Creates the API calls for me and manages the additional params I need to pass to the API
- JSON — Parses the JSON responses from the API into OpenStruct objects to allow the response to be access with basic Ruby instance methods
- Byebug — Command line debugging tool (default to rails)
- RSpec-Rails — RSpec Testing Framework for Rails
- Capybara —Feature testing tool that allows me to interact with my site like a user would
- Launchy — For debugging allows me to use ‘save_and_open_page’ which lets me see what Capybara sees
- Database_Cleaner — Let’s me reset my database for each test condition
- Capybara-Mechanize — Cool gem that allows capybara to visit foreign pages outside my app
These all live inside my Gemfile. I have Faraday and JSON in the main gem list. Everything else is inside my development & test group:
capybara-mechanize gems need a little bit of configuration in order to work correctly, so inside of my
rails_helper.rb file I’ve added these lines:
Test Script Setup
At this point, my app allows a user to create a new account with their name, email and password. Additionally, users are required to be signed in to add Strava to their account and they can only add it from their personal user/show page. Let’s go through the test setup line by line to show how I’m doing things.
These are basic describe/context blocks except for line #3. Here I specify what driver I want Capybara to use. By default, Capybara uses the Rack driver, which doesn’t allow the test suite to access any outside domains. By including the capybara-mechanize gem I can now specify the driver to be mechanize, which does allow capybara to visit outside domains. This is useful, because the default driver works fine for most cases and I don’t know what kind of side affects using the mechanize driver would have on the rest of my test suite.
Creates a basic user profile and mock
current_user as this profile, nothing too special here. Note that I’m using
create! instead of
create. This way I get an error message if the
Go to this user’s show page and expect to see a link to
“Add Strava to Account”. This is step 1 in the Strava Authentication Flow. Additionally, I’m demonstrating that the user does not have a
strava_access_token. These verifications could also belong in the user model test, but I put them here just to make sure everything is working as expected.
Click link to
“Add Strava to Account”. This will send me to https://www.strava.com/oauth/token. If I’m not using mechanize I get the error message below. Capybara ignores the protocol and domain, then expects a route to be present for /oauth/token.
I can use Launchy here to see what Capybara sees after clicking
‘Add Strava to Account’ by inserting ‘
save_and_open_page’ on line 18. Turns out I’m redirected to a login page for Strava. I need to login to my account in order for Capybara to go through the authorization process.
The Capybara methods still work on external websites and can interact with those pages. I inspected the login page to see how strava named the email and passwords fields. Next, I set environment variables in my terminal to store my email/password combo for my test environment. This is important because I don’t want to be posting my test environment credentials all over the internet when I’m posting here or on github. These can be set with an export command in the terminal.
After submitting the login credentials on line 22, I put another ‘save_and_open_page’ on line 23 to see where I’m redirected to (see page below). I see that I’m now at the authorize page, so I want Capybara to click ‘Authorize’. This is step 2 of the Strava Authentication Flow.
This is where my app does a little magic under the hood and performs steps 3–5 in the Strava Authentication Flow. The end result is that I expect to be redirected back to the user show page and for the user to now have their
strava_access_token saved. One thing to note here, I was originally calling
user.strava_athlete_id instead of
User.find(user.id).strava_athlete_id and my test was failing even though the user was getting it’s credentials saved. Turns out,
user was still associated with the state of user that I assigned it on line 4, so I needed to pull the user directly from the database.
This is probably the longest test script I’ve ever written, but that is expected when I’m testing a process with so much user interaction. If there are better ways to test this process let me know in the comments. Figuring out this test took me way longer than I’d like to admit, so if there is an easier way to do it I’d be all over that.