Adding a new client

Ask questions Research chat →

https://theartandscienceofruby.com/adding-a-new-client/ · scraped

ruby

Attachments

Scraped Content

— 2030 words · 2026-02-14 02:57:33 UTC ·

Excerpt

Adding a new client We've got a rough specification for our first feature. Our health and safety company has a set of standard policy documents that they want to make available, read-only, to their clients. What are we building? So, using some of the core concepts we defined last time out, we should probably have something like this: We have an organisation representing our health and safety company. This organisation has some folders, each containing some documents. When a new client organisation is created, we want to make those policy document folders available, but read-only, to the new client. As we want to make the system as re-configurable as possible, instead of putting these rules into the "organisation modules", let's add in the concept of an "automation" that, once triggered by events elsewhere, performs the relevant actions. Specification-driven development I don't do "test-driven development", I do "specification-driven development". You could argue that it's ju
Adding a new client We've got a rough specification for our first feature. Our health and safety company has a set of standard policy documents that they want to make available, read-only, to their clients. What are we building? So, using some of the core concepts we defined last time out, we should probably have something like this: We have an organisation representing our health and safety company. This organisation has some folders, each containing some documents. When a new client organisation is created, we want to make those policy document folders available, but read-only, to the new client. As we want to make the system as re-configurable as possible, instead of putting these rules into the "organisation modules", let's add in the concept of an "automation" that, once triggered by events elsewhere, performs the relevant actions. Specification-driven development I don't do "test-driven development", I do "specification-driven development". You could argue that it's just semantics, but words matter. I want to specify what the system is supposed to do, write some code to make it happen and, importantly, stop writing code as soon as the system does what has been asked of it. A quick tidy-up and we're done. Plus the end result is usually less code which is simpler and easier to understand. And the system is fully documented. Hopefully, we'll see why that happens as the story unfolds. So let's write our specification. We'll use RSpec and Turnip and instead of driving a user-interface by remote controlling a browser, we'll implement a HTTP/JSON API. As background, this is a Rails app that uses Devise and Doorkeeper for handling authentication. Instead of dealing with all that stuff, I've written some little helper methods that generate an OAuth Access Token which we then pass into our API calls. The specification So, first we write our specification document. Ideally, we would talk this through with the customer themselves, listening to their descriptions of the process and typing it up into the slightly more formal "gherkin" syntax that Turnip uses. Here's a start - in spec/features/adding_client_organisation.feature Feature: Adding a client organisation Scenario: Creating the client organisation Given I have an admin account at a health and safety organisation And the organisation has a folder containing policy documents When I log in and add a client organisation Then I should see the new client organisation And I should see that the client organisation has read-only access to the policy documents folder When I look at the audit trail Then I should see a log of how access to this folder was granted Let me stress that we wrote this document in conversation with the customer. Previously I mentioned things like workflows and permissions and automations, but this document makes no mention of those. Because the customer does not care how we're going to implement this. They only care that their client gets access to these documents and they have some visibility into what the system is doing. Also let me stress that the customer will probably never look at this document again. So why do we restrict ourselves in this way? Because we do not want to start thinking about implementations at this stage. We are developers. We think in terms of classes and functions and data-structures and algorithms. But we are still figuring out what needs to happen, why it needs to happen, not how it's going to happen. Filling in the blanks The customer knows their own business and (usually) knows what they want. But they're not software developers and we know that they will not capture everything that the specification will need. For example, they are just going to take it for granted that none of this functionality is available if you're not logged in. We will need to ask them what happens if someone logs in as a normal staff member rather than an admin. Maybe ordinary staff members are permitted to add organisations, maybe they're not; maybe they're permitted to read the audit trail, maybe they're not. It's our duty, as developers, to think of these alternative scenarios, ask the right questions and ensure that the specification is complete. Again, the customer won't really care and will probably find us pedantic and annoying for asking these things but it needs to be done. In this case, we can flesh out our specification with a couple more scenarios. Scenario: Attempting to create a client organisation when not logged When I am not logged in Then I should not be able to add a client organisation Scecnario: Attempting to create a client organisation when not an admin Given I have a non-admin account at a health and safety organisation When I log in Then I should not be able to add a client organisation The implementation Now we've finished talking to the customer, we've got our specification written up, we can create spec/features/adding_client_organisation_steps.rb and take our first steps towards the implementation. module AddingClientOrganisationSteps step "I have an admin account at a health and safety organisation" do end step "the organisation has a folder containing policy documents" do end step "I log in and add a client organisation" do end step "I should see the new client organisation" do end step "I should see that the client organisation has read-only access to the policy documents folder" do end step "I look at the audit trail" do end step "I should see a log of how access to this folder was granted" do end step "I am not logged in" do end step "I should not be able to add a client organisation" do end step "I log in" do end end RSpec.configure { |c| c.include AddingClientOrganisationSteps } This isn't a typical RSpec file. This is a Turnip "steps" file. If we want it to do anything, we need to add a loader clause to our spec/rails_helper.rb below the require "rspec/rails" line. require "support/builders" require "support/authorisation" Dir.glob("spec/features/*_steps.rb") do |f| load f, true end The first two lines require my set of helper functions (one for OAuth2 and one for managing the database). The next loads our steps file, so when turnip finds our .feature file, it knows what to do. We also need to add --require turnip/rspec to our .rspec file in the project root. Getting started Let's start with the simplest case - trying to add a new organisation when we're not logged in. step "I am not logged in" do # do nothing end step "I should not be able to add a client organisation" do post "/api/organisations", params: {name: "TinyCo"} expect(response).to be_unauthorized end An important point to note here; when we're writing out the RSpec part of the specification, we're putting ourselves in the shoes of the developer who is going to be using our system. When you start looking at the documentation for a new API, do you ever scratch your head and wonder why they made it work that way? Here, we have the opportunity to write out how we want it to work in an ideal world. In this case, everyone is used to POSTing to an end-point to create an item - so we should stick with those expectations. When we run this spec, it obviously fails. So the next step is to add in a route and a controller. # config/routes.rb Rails.application.routes.draw do use_doorkeeper devise_for :users get "up", to: "rails/health#show", as: :rails_health_check namespace :api do resources :organisations, only: [:create] end end # app/controllers/api/organisations_controller.rb class Api::OrganisationsController < ApplicationController before_action :doorkeeper_authorize! def create head :created end end The routes file has the standard stuff for loading doorkeeper, devise and the health-check end-point. Then we use an "API" namespace and define the route for accessing organisations. The organisations controller uses doorkeeper to verify the OAuth access token and has a single create action. And also note that we don't need to implement the create action - our single spec doesn't care what create actually does, it only cares that we are not allowed to call it. The simplest thing that will work The next spec is if we are logged in as a non-admin user. This is where things get a bit more complicated, as we've got some setup work to do. Returning to our spec file, we need to create a non-admin user record, get their OAuth access token and pass that in our POST request. I'm not going to go through the mechanics of creating the Devise user model (or getting it to work with doorkeeper). Instead, I've got some helper methods (defined in spec/support/builders.rb and spec/support/authentication.rb) for setting up our data and getting an access token. This results in the following in our steps file: step "I have a non admin account at a health and safety organisation" do @me = a_user status: "admin" @my_organisation = an_account name: "H&S Company" end step "I log in" do @my_access_token = an_access_token_for user: @me @auth_headers = auth_headers_for(@my_access_token) end The a_user function is a builder - we can optionally supply a name, email address, password and status for our user and it creates a valid ActiveRecord model for us. def a_user first_name = "Alice", last_name = "Aardvark", email: nil, password: nil, status: "standard" password ||= "Password123!" email ||= "#{first_name.downcase}.#{last_name.downcase}@example.com" User.create!(first_name: first_name, last_name: last_name, email: email, password: password, password_confirmation: password, status: status) end We repeat this for an_account. We don't really care what an account looks like at this time, so just generate a model with a single name field and implement the an_account function to create a record. The doorkeeper helpers create a user-specific access token that is attached to an application. The final helper generates the "Authorization" header that is passed in our HTTP requests. def an_access_token user: nil, application: nil user ||= a_user application ||= a_doorkeeper_application Doorkeeper::AccessToken.create! resource_owner_id: user.id, application_id: application.id end def a_doorkeeper_application name: nil, redirect_uri: nil name ||= "Test Application" redirect_uri ||= "https://example.com" Doorkeeper::Application.create! name: name, redirect_uri: redirect_uri end def auth_headers_for(access_token) {Authorization: "Bearer " + access_token.token} end There's one more bit of setup to do. We have some authentication headers but our previous step that attempted to create an organisation passed no headers. So let's revisit those steps. step "I am not logged in" do @auth_headers = {} end step "I should not be able to add a client organisation" do post "/api/organisations", params: {name: "TinyCo"}, headers: @auth_headers expect(response).to be_unauthorized end With all this setup done, let's try running our spec again. And, as expected, it fails with Failure/Error: expect(response).to be_unauthorized. This is because our controller is only checking to see if we've got an access token and no more. But now we have an access token, we need to add in the check that we're actually an admin user. There are many ways to do this, and I'm sure we will revisit this later, but for now, let's do the simplest thing that makes the spec pass. Returning to our organisations controller, we need to test if the current user is an admin - if they are, we return :created (a 201 status code), if they are not, we return :unauthorized (a 401 status code). The current_user method tells us who is currently logged in and is added to every controller in our application, thanks to Devise. But before we can use it, we need to link doorkeeper and devise. So in config/initializers/doorkeeper.rb we add the following into the configuration clause: resource_owner_authenticator do current_user || warden.authenticate!(scope: :user) end Then we make our organisations controller check the current user's status. I'm using a Rails enum for this (see the earlier footnote) meaning Rails gives us an admin? method. We test the current user and return the appropriate status code. class Api::OrganisationsController < Api::BaseController before_action :doorkeeper_authorize! def create head(current_user.admin? ? :created : :unauthorized) end end Run the specs again and now they pass. Once more, we've done the simplest thing, the least amount of work, to meet the specification. But we've still got the biggest specification, that needs folders being set up, that creates our organisation and triggers the automations, to come. The next stage is going to be much more in-depth as we need to define a whole load of models, as well as putting together a framework for our automations and their triggers.

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation