At one of our current projects we started testing our key functionalities with Cypress for our React front-end. As we started writing our tests we stumbled upon a problem, how could we have a clear and easy way to setup and clear the data created/modified in the test in a secure way?
We couldn´t do it directly in our API as our front end is fed by several APIs. We also wanted our tests not to be written in Ruby so the cypress-on-rails gem wasn't an option.
Our main goal was to do this without having to add unwanted functionality to our Rails API.
Our first approach was to create a Rails controller that would only be enabled for Cypress testing. We chose to secure it via environment variables and an authentication token.
This option had one big pro: it was very easy to create and easy to maintain, it was only a very big controller in the middle of our production code.
Sadly, there were a number of drawbacks:
- Code meant as test helper was in the middle of the production and business code.
- We had to be really careful about the controller's security. Having a controller able to delete cascade several of our entities was a huge risk to have.
The last point was really important to us. A mistake in the APIs deployment could lead to someone wiping half of our database!.
That's why we looked into another option: using Rails engines.
The pros:
- Active Record models from our main app can be referenced from the engine.
- Endpoints used by Cypress won't be available in production.
- The code of the endpoints won't even be available in the production bundle.
This really lowered the chances that a mistake in deployment configuration left the endpoints available.
The code
Lets create a new Rails project first:
$ rails new cypress_api --api
Inside our newly created API, lets create our Engine to declare our cypress-specific endpoints:
$ rails plugin new cypress --mountable
This will create a cypress folder at the root level with our new Rails engine and will also append the gem configuration in our main Gemfile.
Run bundle install and remove the TODOs from the engines gemspec to be able to start the app.
Now, lets create a new controller inside the main app to signal the API is active. Write this code into app/controllers/status_controller.rb:
class StatusController < ActionController::API
def status
render json: {"status": "OK"}
end
end
Let's do the same inside the engine, but with a different message. Inside cypress/app/controllers/cypress/status_controller.rb:
module Cypress
class StatusController < ActionController::API
def status
render json: {"status": "Cypress OK"}
end
end
end
Also, add the corresponding route config in config/routes.rb and cypress/config/routes.rb as well.
Last, we have to mount our engine's endpoints to our main app. The thing is, we can't do that statically as we don't want the endpoints to be available at all times. We have to do it dynamically.
To do this, we have to add this snippet to the Engines main class in cypress/lib/cypress/engine.rb:
config.cypress_engine = ActiveSupport::OrderedOptions.new
initializer 'cypress_engine.configuration' do |app|
if app.config.cypress_engine[:mounted_path]
app.routes.append do
mount Cypress::Engine => app.config.cypress_engine[:mounted_path]
end
end
end
So what does this snippet do? If the config.cypress_engine option is declared anywhere in our environment files or application file the engines endpoints will be appended to the main app with a prefix.
For now, let's declare that option in our development.rb file.
config.cypress_engine.mounted_path = "/cypress"
We are now declaring that our Cypress Engine endpoints will be mounted into our main app with the cypress prefix.
Let's try it out! Start Rails in development mode and access "/status" and "cypress/status". We can see that both endpoints are available!.
Now, lets start the app in production mode. If we try to access "/cypress/status" we can see that 404 is returned.
Great! Now the Cypress endpoints are not available in the production environment but is still bundled into our app. To omit adding the endpoints to our app while in production we can limit the Gem to only be installed while in development.
group :cypress do
gem "cypress", path: "cypress"
end
We can also add another level of security to our engine in case someone removes this config by mistake.
Add this snippet to cypress/lib/cypress/engine.rb below what we previously added:
initializer "cypress_engine.cypress_only" do
unless Rails.env.development?
abort <<-END.strip_heredoc
Cypress Engine is activated in the #{Rails.env} environment. This is
usually a mistake. To ensure it's only activated in cypress
mode, move it to the development group of your Gemfile:
gem 'cypress', group: :development
END
end
end
If the cypress engine is used in an environment other than development, Rails startup will be aborted.
You can find all the code in this repo:
tomsfernandez / Rails-Cypress-API-Example
Example code of a Cypress setup for Rails API with Engines for Dev.to blog post
(In the repo a new environment called "cypress" exists to add another level of security but it isn't really necessary)
Top comments (1)
Very helpful for my use-case, thanks for this.