This is one of those blog posts that:
- Is for my own future reference - reminding myself how/why I did things.
- Answers questions that Google/Stack Overflow suggest many others are looking to solve.
It essentially covers one way of approaching building a production ready product that is appropriately and easily tested across development and Beta builds.
In my case the product is Run 48 which is a mobile (iOS) app providing a series of tools for runners that integrates with a web application (found at run48.com) and its API.
What is needed.
As far as the web app goes, I need to be able to test that the web app works, and that what I am building works. I am not talking unit testing (although I do recommend that you have unit tests) but rather general integration testing.
I do not want to have an additional web based staging environment as:
- It adds additional complexity, whilst in my opinion not adding value.
- The product in question needs to be reliable but it does not need to be 'if this breaks people die' level reliable.
So, I want two environments - Development (on my local machine), and Production.
In terms of the app, I want to have three 'versions' - Development, Beta, and Production (App Store). I do not want to maintain a second staging version of the API.
To maintain data integrity I do want a separate database for Development. Beta and Production app builds will use the production database. Messing around with data in the Development database is fine but the Production database is sacred. No playing.
Authorisation
Being a running product that heavily relies on data from Strava we implement authentication using Strava's API. In principle we can just use one 'API account' for development and production. The app only reads from Strava so there are no data integrity issues. Regardless we will set up two 'API accounts' just for clarity/complete separation.
Run48.mac
When developing on my mac I am hitting local files. They may have been modified, they may be buggy, they may outright not work. If I am testing user creation it needs to be in the Development database. I do not want to find some weird row with missing values that breaks API requests in my production database. Simple.
Somewhat hacky, but as a sole developer I can reliably do this. I use my macs machine name to discern if I am in a development environment.
In PHP you can do:
$machineName = php_uname('n');
If I am, various configuration values are set accordingly. These include the database connection parameters. Essentially "If on Mac, connect to development database".
I can build the web app, play around and make sure things work on my mac whilst only manipulating data is a Development database.
iOS Development builds
In terms of the iOS app. I want the development build (the build that is run on my device when I simply click 'Run' in XCode to again only manipulate data in the Development database.
My initial approach (and that which I use) was to simply have it connect to the Production API (the only version of the API) passing a specific debug header (X-Run48-Debug) to indicate to utilise the Development database.
I use the awesome Alamofire library for networking, so adding this header to my requests was as simple as creating a custom request adapter as follows:
class APIHeadersAdapter: RequestAdapter {
func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest
switch (Config.appConfiguration) {
case .Debug:
print("Running in debug mode. Appending debug header.")
urlRequest.setValue("debug", forHTTPHeaderField: "X-Run48-Debug")
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
default:
print("Running in beta/app store mode")
}
return urlRequest
}
}
Discerning which build is being run is courtesy of this answer from StackOverflow.
In terms of the server implementation, the basic configuration outlined in the previous section is in fact a tiny bit more complex - it checks if the request is being executed on my mac OR if this header is present. If either is the case - Development database.
As the API is relatively static, this is 'acceptably risky' to me. I don't push out changes to the API unless they have been thoroughly unit tested and I can do basic integration tests in the simulator on my mac. After all I am building/testing the app not the API. The API should be assumed working.
For another, cooler, more interesting, and probably better way of approaching this read the 'PAC proxy' section below.
iOS Beta and Production builds
The iOS Beta and Production builds do not pass the debug header (obviously) when they connect to the API. As a result all data stuff uses the production database. Simple.
PAC proxy
There are a few intricacies associated with using the Strava API. One of them is that you are only allowed one authorised redirect URL. My authorisation flow for the iOS app sends the user to the Strava app (if installed) and then redirects them somewhere (developer defined).
As far as I can tell, you can not redirect them to an App URL at the moment so I simply redirect the user to the web app where we handle the authorisation. The web app then redirects the user to the Run48 app.
The Strava API only allows you to pass custom data in the 'state' parameter of your authorisation request. You can only pass a string of data. In principle you could encode multiple pieces of data in some way but it is not ideal or clear. I use this field to identify the app user who is authenticating through Strava.
To be able to discern on the server side if this is a debug request is not simple:
- I can not tell Strava to set my debug header.
- I do not want to pass lots of data in the 'state' parameter (although I would if I had to).
In this case I want to fall back to 'the other way the web app discerns if the request is dev' - the request coming from my mac - run48.mac.
As mentioned above, for clarity I have two Strava API apps for dev and production so I can simply use the appropriate Strava confirguration for the build type in the app and redirect the user to Run48.com or Run48.mac depending on the build.. right?
Sadly it is not so easy as Run48.mac is a host setup on my mac, not my iPhone. It means nothing to my iPhone. I begun investigating..
I wanted to create the equivalent of a hosts
file on my phone to forward Run48.mac to my server running on my mac. I was aware that being on the same Wifi network I could connect to my localhost
from my phone by simply entering my laptops IP (see here for overview) but I was not sure if it was possible to mess with hosts
(or its equivalent).
I was aware from previous app builds that you could probably achieve what I wanted with a proxy app like Charles on the mac which would intercept requests and route them accordingly. This seemed a little too arduous.
I stumbled upon this answer which implied that it could be done with a certain app. The app cost money so I didn't buy it, but it implied that it could be done. I looked into the app and investigated how it achieved this. It mentioned proxies and PAC files.
I don't have a great understanding of proxying requests but the result of my investigation works and essentially involves telling your phone to look at your custom .pac file to discern how to route certain requests.
I found this article, PAC file best practices and the wikipedia page extremely helpful for coming up with this super simple .pac file.
function FindProxyForURL(url, host)
{
if (!localHostOrDomainIs(host, "run48.mac"))
return "DIRECT"; else return "PROXY my.laptop.ip";
}
This basically says route requests to run48.mac
to my laptops IP. Route all other requests normally.
Now you need to upload this to a publicly accessible webserver and do some basic config on your Wifi connection.
A how to on doing that is found here.
So now the Development build of my app (as far as Strava connections go) works like this:
- The app knows which build it is (see above) and uses the development Strava API configuration.
- We redirect to the Strava app so the user can do the authorisation. Strava redirects the user to
run48.mac
- My iPhone has loaded my custom .pac file from my web server and thus routes the request to
run48.mac
to my laptops IP (which is connected to the same Wifi network). - As the request is being handled by my mac the development database connection configuration is used.
- The authorisation request is handled and then we redirect to the Run 48 app.
Result = We have tested the development version of the Run 48 app and its connections with Strava whilst only touching our development database.
You may be thinking..
The eagle eyed may now be thinking why not use this approach for testing the development version of the app in its entirity. You could get rid of the need for a custom debug header. You are correct.
You could/probably should test the development API and its integration with the development app by using a custom proxy to route requests to the laptop local version of the API.
Why didn't I? Because I didn't realise I could until I needed to do it for the Strava integration. I likely will :)