Don’t Let API Changes Hit You Like A Freight Train!

Most web applications, and many mobile applications, rely on 3rd party APIs like social login, cloud storage, email, messaging, CRM etc. The benefits are obvious, and for some applications the API integration is a core element. However, the API dependency does make applications more vulnerable to change — one small change to an API can break an entire app.

API changes are inevitable. Just like traditional software applications, APIs go through changes, revisions, and versions. Best practices are very well defined for extending, versioning and replacing APIs, and API vendors typically have an API management strategy in place. This is all great, it is expected.

The question is, as an API consumer, do you have a strategy in place to manage 3rd party API changes? If an API change breaks the application, it can have severe consequences; loss of data, revenue, and users, not to mention causing harm to the company’s reputation. Without a process in place, 3rd party API changes can hit you like a freight train!

Today we outline some steps that API consumers can take to monitor their APIs to ensure that no breaking change ever disrupts an otherwise serene user experience. As a case study, we’ll see what developers at CloudRail have done to safely mitigate 3rd party API usage, and walk through a simple Node.js app designed to aid authentication testing.

Monitoring Changes

The good news is that most API service providers give an early heads up before making any changes, allowing plenty of time to make fixes on your end. The question is, who’s getting notified about API changes?

The CloudRail team monitors the APIs they integrate in their SDKs, and if there are any changes, they can notify their users. But who is receiving the notification? Is it going to a group email, the engineering manager (who is already drowning in emails), or maybe a previous employee?

Lesson: Assign an API integration upkeep role to a specific team member. Don’t just leave it to whoever discovered the change, as that’s leaving things to chance and potentially spotty team member availability.

Automated Testing

Automated testing is always a key ingredient in monitoring the functionality of an application. While automated testing is most often performed on internal software, it should not be limited to in-house code, rather, it should be applied to all integrations, including 3rd party libraries and API dependencies.

Catch broken integrations with end-to-end testing that simulates user requests and tests all features. It’s important to remember, though, automated testing is meant to validate functionality — it’s not a way to identify issues.

Some features can be hard to automate, which makes testing challenging. The beauty of testing an application’s API (as opposed to an application’s front end) is that the API doesn’t care where the requests are made — REST APIs operating over HTTP will send the same JSON responses and error messages to a mobile application, web application, or third party service like a Postman test client or a command line client like Newman.

Some tests require a more complex test flow, like OAuth authentication. Let’s say we have an application that is using Facebook Social Login with Dropbox file storage, and we want to get a list of files stored in Dropbox. We first need to authenticate through a flow where we give the application permission to access the Dropbox account once credentials are given.

This authentication process requires clicks and redirects to work, which means we have to simulate user interaction. This can be done with testing tools like Selenium Webdriver. In the following I’ll make a small Node.js application, with a third party component to handle authentication, and then test it with Selenium Webdriver.

Building the Application

The application is very simple; the routes only consist of “/” (root), “/authorize” and “/auth”. Root loads a page with a link to authorization, and auth is a callback for the authentication. CloudRail’s unified API is used to handle Dropbox’s authentication. Here’s the source code:


var express   =  require("express");
var app       = express();
var url       = "[ The applications URL ]"

const cloudrail = require("cloudrail-si");
cloudrail.Settings.setKey("[ App Key ]");

app.get('/',function(req,res){
  // Insert link to authorization on the webpage
  res.send("Authorize");
});

app.get('/auth',function(req,res){
  // Return status
  res.setHeader('Content-Type', 'application/json');
  res.send(JSON.stringify({ status: "OK", code: 200 }));
});

app.get('/authorize',function(req,res){
  
  // Setup CloudRail RedirectReceiver
  var redirectReceiver = (url, state, callback) => {
      res.redirect(url);
   };

   // Setup Dropbox service
  const dropbox = new cloudrail.services.Dropbox(
    redirectReceiver,
    "11uz2nxnttx24ks",
    "p9llxxo6mh6rg26",
    url + "/auth",
    "someState"
  );

  // Perform login
  dropbox.login(
    (error, result) => { }
  );
});

// Listen to requests on port 3000
app.listen(3000,function(){
  console.log("Listening on PORT 3000");
});


The code is very simple, and only handles the login process. Once the authentication is done, the DropBox service can be used to get the file list, upload and download files, and much more. But for this test example we’re only focusing on the authentication.

Testing

As previously mentioned, we’re using Selenium Webdriver for this example, but other test tools can do the same. Some of the test tools that can be used for testing, besides Selenium, includes SoapUI, RIATest and Ghost Inspector, and more language/framework specific tools like Mocha and QUnit. Which tool to choose is very much a question of needs and personal preferences, though some have limitations. For example, Ghost Inspector can’t handle redirects to a localhost, so the application must be available on a public domain.

We’re assuming Eclipse, Selenium, WebDriver, and Java language binding is installed, and a new project is created. There are plenty of tutorials available for these steps. This tutorial from Guru99 does a good job explaining the setup. Even though it’s written for Windows users, the steps for Mac users are very similar, and this tutorial can be used by Mac users as well.

What I want to do is load the root page, click a link to authorize a Dropbox account (logging in with the user’s credentials, and then allow access) and return a callback with the auth code. These few lines of code accomplishes that:


package newpackage;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class MyClass {
	public static void main(String[] args) {
		
    Integer TIMEOUT 			= 20;
    String APP_URL			  = "[ URL to the Node.js root ]";
    String USER_EMAIL 		= "[ Dropbox User Email]";
    String USER_PASSWORD	= "[ Dropbox User Password]";

    WebDriver driver 		= new FirefoxDriver();

    // Load the root page, which contains an authorization link
    driver.get(APP_URL);

    // Find the link and click it
    WebElement element = driver.findElement(By.name("authorize"));
    element.click();

    // Wait for the Email input field to become visible
    WebElement elementEmail = (new WebDriverWait(driver, TIMEOUT))
          .until(ExpectedConditions.elementToBeClickable(By.xpath("//input[@class='text-input-input autofocus']")));

    // Insert the user's email address into the input field
    elementEmail.sendKeys(USER_EMAIL);

    // Wait for the Password input field to become visible
    WebElement elementPassword = (new WebDriverWait(driver, TIMEOUT))
        .until(ExpectedConditions.elementToBeClickable(By.xpath("//input[@class='password-input text-input-input']")));

    // Insert the user's password into the input field
    elementPassword.sendKeys(USER_PASSWORD);

    // Find the "Sign in" button and click it
    WebElement elementSignIn = driver.findElement(By.xpath("//button[@class='login-button button-primary']"));
    elementSignIn.click();

    // Wait for the Allow button to be on the page and clickable, then click the button
    WebElement elementAllow = (new WebDriverWait(driver, TIMEOUT))
          .until(ExpectedConditions.elementToBeClickable(By.xpath("//button[@name='allow_access']")));
    elementAllow.click();


    // Here data returned by /auth can be checked, and either pass or fail this test.


    //Close the browser
    driver.quit();
  }
}


When you run the code, Selenium opens up a Firefox browser, and simulates user inputs.

The test itself is automated, and simulates a user without any manual steps, but it still requires someone to click the Run button in Eclipse. This part can of course be automated too. Jenkins is a test automation server that can run scheduled tests, including Selenium WebDriver tests (requires plugin). It can also pull code from GitHub, or other SVN repositories, so Jenkins can be scheduled to pull code from a repository every night, and run the test scripts.

Concluding Remarks

API changes can break your application, so have a process in place to be prepared for changes. Automated end-to-end testing can validate the application is working, including 3rd party APIs. If some API changes slip through the cracks, they will be discovered very early with frequent (daily) automated testing. Automated tests, meaning the use of test scripts and tools to simulate users, delivers consistent results, and doesn’t require user interaction.

The example above shows how to test an OAuth flow, which can be a challenge because of the redirects. Because it’s challenging, we found it an interesting use case example. In this case, where a 3rd party library is used to handle authentication, the test is not only a test of the authentication, but also the integration with the library. When successful, the application initiates the authentication process through the library’s functions, and returns the access token to the application’s callback route. This confirms that the flow from the application, through the 3rd party library, authentication, and callback back to the application again all works.

Testing should cover all use cases, and simulate all possible user actions. This typically includes, but not limited to, CRUD operations (Create, Read, Update, Delete) with UI verification. Tests should also consider corner cases and error checking; for example, test returns when improper user credentials are provided, or if an invalid entry is given in an input field, and other error-prone operations.

In general, ensure your tests reflect real world use cases. With automated end-to-end testing, when API changes do creep up, your team will be readily notified and can respond accordingly.