Introduction

In the last article in the series we covered an overview of our API and we set up our Rails application. Our main objectives for this article are to build out our Client and ApiKey models. We will also go over how to generate an API key. Let's get started.

Click here for a quick recap of what we are building

The first thing we need to do is create our Client and ApiKey models. First let's create the client model. The client model has 1 column, name. Run the command below to create the Client model now.

Terminal Commands:
  
rails g model Client name

Now let's create our ApiKey model. The ApiKey model has 2 columns, name and key. Run the command below to create it now.

Terminal Commands:
  
rails g model ApiKey client:references name key

Now let's migrate the database. Run the command below to do this now.

Terminal Commands:
  
rails db:migrate

What's next? It's time set up our factories. What is a factory? A factory is a piece of code that generates data for use in our tests. We can also use factories to ease the creation of seed data in our development environment. Factories become extremely useful as the application grows, since they can be used to keep business logic out of our tests. For example, instead of manually creating a client and then adding the API keys to it, you can simply call something like create(:client, :with_api_keys) and the code in your factory would take care of the rest. First let's create the Client Factory. Open up the client factory in spec/factories/clients.rb and modify it so that it looks like the code below.

spec/factories/clients.rb:
  
FactoryGirl.define do
  factory :client do
    sequence(:name) { |n| "Client #{n}" }
  end
end

The sequence function listed above is a helper provided by FactoryGirl that automatically counts up as you create more and more models. So calling create(:client) the first time would yield a client with the name "Client 1". Calling create(:client) the second time would yield a client named "Client 2", and so on.

Great, now let's set up the factory for the ApiKey model . Open up the ApiKey factory in spec/factories/api_keys.rb and modify it so that it looks like the code below.

spec/factories/api_keys.rb:
  
FactoryGirl.define do
  factory :api_key do
    association :client, strategy: :build
    sequence(:name) { |n| "User #{n}" }

    trait :admin do
      client { nil }
    end
  end
end

Let's break this down:

Line 3 specifies that ApiKey has an association with Client. When you run create(:api_key) an associated client would get automatically created unless you override it. The strategy: build option tells FactoryGirl not to create the association unless we use the create method or call save on the model. The default behavior is for FactoryGirl to create the association in the database even when we call build. This is not something we want to happen.

The trait function at lines 6-8 specify variations on the default factory. In this instance, calling create(:api_key, :admin) would use the admin trait to set the client_id to nil, hence giving us an API key that has admin capabilities.

Now that we have our database and factories ready, it's time to start implementing our API. Our first task is to set up the relationships between our models and add validation. Let's start with the Client model. We know that the client has many API keys, in addition, we should make name a required field. Knowing this let's write some tests. What tests do we need? First, we need to test whether the factory we just created is valid. It's a good idea to test all of your factories and make sure they are valid. This becomes important as your application grows in complexity. We also need to write a test for the relationship with API keys. Finally, we need to write a test to check that a name is required. Let's add these tests now. Open up the Client model spec in spec/models/client.rb and modify it so that it looks like the code below.

spec/models/client_spec.rb:
  
require 'rails_helper'

RSpec.describe Client, type: :model do
  let(:client) { build_stubbed(:client) }

  it 'has valid factories' do
    expect(build_stubbed(:client)).to be_valid
  end
  
  it 'has many api keys' do
    subject = Client.reflect_on_association(:api_keys)
    expect(subject).to_not be_nil
    expect(subject.class_name).to eq('ApiKey')
    expect(subject.macro).to eq(:has_many)
  end

  describe 'validations' do
    it 'requires that a name be present' do
      client.name = nil
      expect(client).to_not be_valid
    end
  end
end

Let's break this down:

Line 4 uses a method called let. The let method defines a specific piece of code that can be used by your tests. By default, the code specified by let doesn't actually run until it is referenced in the test. Note things get reset after each test, so even if you did something like client.name = 'foo' in 1 test, it wouldn't affect the way the other tests work. The let function also has a sibling called let!. The only difference between the two is the let! code gets run right away before the test starts.

At line 4 you will also notice that we use a function called build_stubbed here. The build_stubbed function assigns an id to the object, but doesn't actually save object to the database. Functions like persisted? would then return true if called. As a result, using build_stubbed can make your tests run much faster. Note that any attempts to manipulate the database will fail, therefore, build_stubbed is only useful the functionality you are testing doesn't perform database operations.

The first test at lines 6-8 checks whether our factory is valid. It's a good idea to always test all your factories to make sure they are valid on creation. This prevents problems with the factories or your code when things change.

The second test at lines 10-15 verify that the relationship to the ApiKey model is set up properly. We include this test since it's a good idea to always test for proper model relationships. The reflect_on_association method returns information about a specific association in your model. Line 12 checks whether the method returned nil or not. A nil return value would indicate that the relationship does not exist. Line 13 checks that the relationship is tied to the 'ApiKey' model. Finally, line 14 tests for the type of relationship.

The final test at lines 18-21 checks that the name is required. If we run our client model spec now via bundle exec rspec spec/models/client.rb we'd end up with a failing test. Let's fix that. Open up your client model at app/models/client.rb and modify it so that it looks like the code listed below.

app/models/client.rb:
  
class Client < ApplicationRecord
  validates :name, presence: true
  has_many :api_keys
end

If we re-run the client model spec now via bundle exec rspec spec/models/client.rb it passes! Let's move on.

The next thing we should work on is our ApiKey model, so let's talk about what we need for that. We know that our ApiKey model has 3 fields, client_id, name, and key. We also know that the client is optional while both name and key are required. We should also probably add a uniqueness validator for our API key. What else do we need? Well for starters, we need a way to generate an API key. For our key, we will generate a random 64 character string. We also need a way to tell whether the API key belongs to an admin or not. We could easily check if client_id is nil, but this could cause an issue if we decide to change the way we do authorization down the road. So let's create a method in our ApiKey model called admin? The admin? method will return true if the user is an admin or false if the user is not.

Now that we know what we need to do, let's start by writing some tests. Open up the ApiKey model spec in spec/models/api_key_spec.rb and modify it so that it looks like the code listed below.

spec/models/api_key.rb:
  
require 'rails_helper'

RSpec.describe ApiKey, type: :model do
  let(:api_key) { build(:api_key) }

  it 'has valid factories' do
    expect(build_stubbed(:api_key)).to be_valid
    expect(build_stubbed(:api_key, :admin)).to be_valid
  end

  it 'has a relationship with Client' do
    subject = ApiKey.reflect_on_association(:client)
    expect(subject).to_not be_nil
    expect(subject.class_name).to eq('Client')
    expect(subject.macro).to eq(:belongs_to)
  end

  describe 'validations' do
    it 'does not require a client' do
      api_key.client_id = nil
      expect(api_key).to be_valid
    end
    
    it 'requires a name to be present' do
      api_key.name = nil
      expect(api_key).to_not be_valid
    end

    it 'requires a key to be present' do
      api_key.key = nil
      expect(api_key).to_not be_valid
    end

    it 'requires a unique key' do
      api_key2 = create(:api_key, key: api_key.key)
      expect(api_key).to_not be_valid
    end
  end

  it 'generates an API key with a size of 64 characters' do
    expect(api_key.key.length).to eq(64)
  end

  describe '#admin?' do
    it 'returns true when the user has not been assigned a client' do
      api_key.client = nil
      expect(api_key.admin?).to be_truthy
    end

    it 'returns false when the user has been assigned a client' do
      api_key.client_id = 1
      expect(api_key.admin?).to be_falsey
    end
  end
end

Let's break this down:

The first test at lines 6-9 checks that our factories are valid. Notice that we check both the default as well as the variation provided by the admin trait. As mentioned above, it's a good idea to test all of our factories.

The next test at lines 11-16 check whether the ApiKey model has a relationship with Client. You'll notice this code is similar to the test we wrote for client, with the exception of the class name being 'Client' and the relationship being belongs_to

The test at lines 19-22 checks that the client is optional and not required.

The tests at lines 24-32 check that key and name are required.

The test at lines 33-37 checks that the API key is unique.

The test at lines 40-42 checks that our API key is 64 characters long.

Finally, the tests at lines 45-53 define the behavior of our admin? helper.

Now that we have added the tests, let's make them pass. Open up the ApiKey model at app/models/api_key.rb and modify it so that it looks like the code below.

app/models/api_key.rb:
  
class ApiKey < ApplicationRecord
  belongs_to :client, required: false
  validates :key, :name, presence: true
  validates :key, uniqueness: true

  after_initialize do |api_key|
    api_key.key = SecureRandom.hex(32) if api_key.key.blank?
  end

  def admin?
    client_id.nil?
  end
end

Let's break this down:

Line 2 defines the relationship with Client. Notice the inclusion of the required: false parameter. This is because Rails, starting with version 5, now requires the presence of a model in the relationship unless you tell it not to.

Line 3 contains the validations that make both key and name required fields.

Line 4 contains a validation that requires key to be unique.

Lines 6-8 define an ActiveRecord callback called after_initialize. The after_initialize callback runs when an instance of the model is instantiated. This happens whenever the model is either loaded from the database or created using .new or .create. Because of this, line 47 generates our API key, but only if it's blank. It does this using the hex method on the SecureRandom class. SecureRandom.hex generates a random hexadecimal string of the specified length. The actual length of the string is twice of what is specified because it is in hex format (00-FF * 32 in this case). The string in question always contains the numbers 0-9 and a-f. Other letters or symbols are not used.

Lines 10-12 define the admin? helper function. If the client_id is set to nil, The admin? function returns true. Otherwise it returns false.

If you run your test suite now via rake or bundle exec rspec you should end up with 100% passing tests. That's all there is to this article. The next article will cover building the authorization system. In addition, we will make our first API request! If you have any questions or comments, please feel free to leave them below. Thanks for reading!