- Read Tutorial
With our Client
validations in place now we can implement an API key generator.
Our API key doesn't have to be overly complex, mainly because microservices typically only need to interact with a limited number of applications. If you are building out an extensive API, such as the Google Maps or Yelp APIs you want need to create an entire application dedicated specifically to creating and managing API keys.
However microservices simply need to ensure that we have some data elements that are too difficult to guess and will be stored securely.
Creating the Generator Spec
Let's begin with creating an expectation for what the feature needs to do. In plain English we can describe it by saying:
- The API Key generator should create a random string of characters
- The API Key should be added to the
client
when created
So we have two requirements, let's create some specs for them. So where should our first spec go? In looking through the options none of them really are a fit for our generator. So let's create a new directory called /features
and place it in the /spec
folder:
mkdir spec/features
Now let's create a file dedicated to testing the key generation process.
touch spec/features/api_key_spec.rb
Side note: RSpec will run the code in any files in the
/spec
directory that has a file name ending in_spec.rb
. That's why we didn't have to add any configuration code to let it know where to find the files, even though we created a brand new directory.
With that file added let's open it up and add in our expectation:
# spec/features/api_key_spec.rb require 'rails_helper' RSpec.describe 'API Key' do describe 'creation' do it 'can be created' do key = ApiKey.generator expect(key).to be_kind_of(String) end end end
This is a pretty basic test, we're asking it to create a new instance of ApiKey
and expect that it returns a String
value.
Implementing the Generator
Running the test will give us an error because we don't have an ApiKey
class or module in the project. Before we create one, let's discuss a little about OOP design. It would be very easy to load our Client
class with this code. However that would break the rule of Single Responsibility. The single responsibility rules states that classes and modules should only have one focus. Our Client
class manages the data for clients
. If we asked the class to take on the process of generating an API Key it may not seem like a big deal right now. However, if in the future we are asked to extend the functionality of the API Key generator we would need to fill the Client
class with code unrelated to the Client
directly. This would be a poor OOP design choice.
Instead, let's create a new module that will manage the API Key generation process and store it in the /lib
directory.
touch lib/api_key.rb
Let's put the most basic implementation in place to get the test passing, this will include simply hard coding a String
value:
# lib/api_key.rb module ApiKey def self.generator "I384fHtD1h9XZvs4fGPJUgtt" end end
If you're wondering why it's considered a best practice to process TDD this way it's for a very practical reason. Imagine that We went through the process of building the full module, including the generation of a unique api key. If we ran the test and it failed it would take much longer to debug. The error could have to do with the generator, but it's just as likely due to a spelling mistake or something trivial like that. In this case, by adding in the most basic implementation possible we're able to confirm that we setup the module and method properly.
If you run the test you'll see that it's now passing. Obviously this needs to be refactored since we're simply hard coding the value. Let's add in the code that will create a random sequence of characters so that the method returns a different value each time:
# lib/api_key.rb module ApiKey def self.generator SecureRandom.base64.tr('+/=', 'Qrt') end end
If you're wondering about the SecureRandom class I recommend you checking out the documentation. Essentially it's way of generating a random string of 16 characters. We're passing in the base64
method to ensure that it generates various character types, instead of just creating random numbers like the rand
method would do.
If you run the tests now you'll see that we're still green, so our refactor worked properly and our new module is returning a string.
We could have build a fake module to have a test stub, however for such a basic feature I don't think it's needed. If we're asked to build in more custom behavior at a later time I'd most likely create a stub like we did with the FakeSms
module.
Defining the Callback Spec
Looking back at our list of requirements you'll see that this feature has one more item that needs to be built.
- The API Key should be added to the
client
when created
This spec will go in the model tests since it's testing behavior directly related to the Client
model. We'll create a new describe
block called callbacks
that will store this type of functionality.
Essentially we want the API Key module to be called each time a new Client
is created and to store that value in the database in the api_key
column.
Our spec will look something like this:
# spec/models/client_spec.rb describe 'callbacks' do it 'will have an api_key automatically assigned when created' do client = Client.create(source_app: "app_name") expect(client.api_key).to_not be_nil end end
This will create a new Client
without an api_key
and expect the value to not be nil
.
After running the test you'll see that this fails.
Implementing the Callback Spec
Callbacks in Rails have developed a bad name. Over the years many developers have abused the tool and caused some messy code bases. However, when used properly, callbacks are a powerful tool that let us access stages of an object lifecycle.
For this feature we'll use the before_validation
callback. This will let us set the api_key
prior to the validations running. Since we have a validation that requires the api_key
to be set upon creation, this process would fail if we don't use the proper callback.
The model should now look like this:
# app/models/client.rb class Client < ActiveRecord::Base has_many :notifications, foreign_key: 'source_app', primary_key: 'source_app' validates_presence_of :source_app, :api_key validates_uniqueness_of :source_app, :api_key before_validation :set_api_key private def set_api_key self.api_key = "I384fHtD1h9XZvs4fGPJUgtt" end end
Once again I'm using the most basic implementation in order to get the test passing. Similar to before I'm taking this course of action to ensure that I'm using the proper callback and code setup. Running the tests now show that they're passing, so that's a good sign.
Now we can slide our call to the ApiKey
module instead of the hardcoded string:
# app/models/client.rb def set_api_key self.api_key = ApiKey.generator end
Running the specs will show that everything is passing. Let's test this out in the console to ensure it's working properly.
In creating a few test clients you'll see that the system is working properly and generating a different key each time a Client
is created, so nice work, this feature is complete!
What's Next?
With the Client
class functioning properly we're ready to build in the authentication system that will check each Notification
request against the API key and app name parameters to ensure the application cannot be sent requests by unauthorized applications.