Sanitize user-generated filenames and only send files inside a given directory
If in your application your users pass along params that result in filenames, like invoices/generated?number=123
. This could be your (very careless) controller method:
def generated
send_file File.join(Rails.root, 'shared', 'invoices', params[:number])
end
This allows your users not only to access those files but also any files your application can read, like this:
invoices/generated?number=../../../../../etc/passwd
# => send_file '/etc/passwd'
You do not want this. In most cases you should prefer a show
met...
How to use Active Job to decouple your background processing from a gem
In a web application you sometimes have tasks that can not be processed during a request but need to go to the background.
There are several gems that help to you do that, like Sidekiq or Resque.
With newer Rails you can also use ActiveJob as interface for a background processing library. See here for a list of supported queueing adapters.
For ...
Find an ActiveRecord by any column (useful for Cucumber steps)
The attached patch lets you find a record by a string or number in any column:
User.find_by_anything('carla')
User.find_by_anything('email@domain.de')
User.find_by_anything(10023)
There's also a bang variant that raises ActiveRecord::NotFound
if no record matches the given value:
User.find_by_anything!('carla')
Boolean and binary columns are excluded from the search because that would be crazy.
I recommend copying the attachment to features/support/find_by_anything.rb
, since it is most useful in Cucumber step def...
Prefer using Dir.mktmpdir when dealing with temporary directories in Ruby
Ruby's standard library includes a class for creating temporary directories. Similar to Tempfile it creates a unique directory name.
Note:
- You need to use a block or take care of the cleanup manually
- You can create a prefix and suffix e.g.
Dir.mktmpdir(['foo', 'bar']) => /tmp/foo20220912-14561-3g93n1bar
- You can choose a different base directory than
Dir.tmpdir
e.g. `Dir.mktmpdir('foo', Rails.root.join('tmp')) => /home/user/rails_example/tmp/foo20220912-14...
SAML Single Logout (SLO)
There are two ways a logout in SAML can happen: Service Provider (SP) initiated and Identity Provider (IDP) initiated logout. I'll explain how to implement both flows with devise_saml_authenticatable
.
Note
SAML also supports a
SOAP
and anArtifact
binding to do this. This guide only refers toPOST
andRedirect
bindings.devise_saml_authenticatable
does not supportSOAP
andArtifact
bindings.
SP initiated logout (using the Redirect Binding)
When the user clicks on Logout
within the app, the app can trigger...
Returning an empty ActiveRecord scope
Returning an empty scope can come in handy, e.g. as a default object. In Rails 4 you can achieve this by calling none
on your ActiveRecord model.
MyModel.none # returns an empty ActiveRecord::Relation object
For older Rails versions you can use the attached initializer to get a none
scope.
How to discard a surrounding Bundler environment
tl;dr: Ruby's Bundler environment is passed on to system calls, which may not be what you may want as it changes gem and binary lookup. Use Bundler.with_original_env
to restore the environment's state before Bundler was launched. Do this whenever you want to execute shell commands inside other bundles.
Example outline
Consider this setup:
my_project/Gemfile # says: gem 'rails', '~> 3.0.0'
my_project/foo/Gemfile # says: gem 'rails', '~> 3.2.0'
And, just to confirm this, these are the installed Rails versions for each ...
Jasmine: Fixing common errors during initialization
Due to the way we setup Jasmine tests in our projects, you may run into various errors when Jasmine boots.
Setting jasmineRequire
on undefined
Jasmine 4 may fail with an error like this:
Uncaught TypeError: Cannot set properties of undefined (setting 'jasmineRequire')
This is due to issues in Jasmine's [environment detection](https://github.com/jasmine/jasmine/blob/502cb24bb89212917a3c943b593fd918ffc481cb/lib/jasmine-core/...
Faking and testing the network with WebMock
An alternative to this technique is using VCR. VCR allows you to record and replay real HTTP responses, saving you the effort to stub out request/response cycles in close details. If your tests do require close inspection of requests and responses, Webmock is still the way.
WebMock is an alternative to FakeWeb when testing code that uses the network. You sh...
Creating a self-signed certificate for local HTTPS development
Your development server is usually running on an insecure HTTP connection which is perfectly fine for development.
If you need your local dev server to be accessible via HTTPS for some reason, you need both a certificate and its key. For a local hostname, you need to create those yourself.
This card explains how to do that and how to make your browser trust the certificate so it does not show warnings for your own certificate.
Easy: self-signed certificate
To just create a certificate for localhost
, you can use the following command....
Too many parallel test processes may amplify flaky tests
By default parallel_tests will spawn as many test processes as you have CPUs. If you have issues with flaky tests, reducing the number of parallel processes may help.
Important
Flaky test suites can and should be fixed. This card is only relevant if you need to run a flaky test suite that you cannot fix for some reason. If you have no issues...
Stabilize integrations tests with flakyness introduced by Turbo / Stimulus / Hotwire
If you run a Rails app that is using Turbo, you might observe that your integration tests are unstable depending on the load of your machine. We have a card "Fixing flaky E2E tests" that explains various reasons for that in detail.
Turbo currently ships with three modules:
- Turbo Drive accelerates links and form submissions by negating the need for full page reloads.
- Turbo Frames decompose pages into independent contexts, which scope navigation and can be lazily loaded.
- T...
How to not repeat yourself in Cucumber scenarios
It is good programming practice to Don't Repeat Yourself (or DRY). In Ruby on Rails we keep our code DRY by sharing behavior by using inheritance, modules, traits or partials.
When you reuse behavior you want to reuse tests as well. You are probably already reusing examples in unit tests. Unfortunately it is much harder to reuse code when writing integration tests with Cucumber, where you need to...
ActiveRecord: Creating many records works faster in a transaction
When you need to insert many records into the same table, performance may become an issue.
What you can do to save time is to open a transaction and save multiple records within that transaction:
transaction do
500.times { Model.create! }
end
Although you will still trigger 500 INSERT
statements, they will complete considerably faster.
When I tried it out with a simple model and 500 iterations, the loop completed in 1.5 seconds vs. 6 seconds without a transaction.
Alternative
Another fast way to insert many ...
Controlling how your website appears on social media feeds
When a user shares your content, a snippet with title, image, link and description appears in her timeline. By default social networks will use the window title, the first image, the current URL and some random text snippet for this purpose. This is often not what you want.
Luckily Facebook, Twitter, etc. lets you control how your content appears in the activity streams. They even have agreed on a common format to do this: OpenGraph <meta>
tags that go into your HTML's <head>
:
<meta property="og:url" content="http://start.m...
How to avoid multiple versions of a package in yarn
To avoid multiple versions of a package, you can manually maintain a resolutions
section in your package.json
. We recommend you to do this for packages like jQuery. Otherwise the jQuery library attached to window
might not include the functions of your packages that depend on jQuery.
Note: This is only an issue in case you want to use a package functionality from window
e.g. $(...).datepicker()
from your dev console or any other javascript within the application.
Background
By default yarn will create a folder node_modules
...
Databases don't order rows unless you tell them so
There is no such thing as a "default order" of rows in database tables.
For instance, when you paginate a result set: When using LIMIT
, it is important to use an ORDER BY
clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. You might be asking for the tenth through twentieth rows, but tenth through twentieth in what ordering? The ordering is unknown, unless you specified ORDER BY
.
In Rails, if you use Record.first
or Record.last
, it will default to orderin...
Clean up application servers when deploying
Our development process makes us deploy very often. As the number of releases grows, junk clogs up the hard drive of our application servers:
- Old release code
- Old
tmp
folders with compiled view templates etc. - Precompiled assets (Javascripts, images...) that no longer exist. When using the asset pipeline, Capistrano will symlink the
public/assets
directory toshared/assets
. This is cool since we can still serve previous assets after a new release, in the window where browser caches might still have references to old assets. But i...
In MySQL, a zero number equals any string
In MySQL comparing zero to a string 0 = "any string"
is always true!
So when you want to compare a string with a value of an integer column, you have to cast your integer value into a string like follows:
SELECT * from posts WHERE CAST(posts.comments_count AS CHAR) = '200'
Of course this is usually not what you want to use for selecting your data as this might cause some expensive database operations. No indexes can be used and a full table scan will always be triggered.
If possible, cast the compared value in your application to...
Configuring Git with .gitconfig
Basic configuration
Please keep this config simple. It should be a starting point for new developers learning Git.
[user]
name = Your Name
email = your.name@domain.com
[branch]
sort = -committerdate
[color]
ui = auto
[color "branch"]
current = yellow reverse
local = yellow
remote = green
[color "diff"]
whitespace = white reverse
meta = blue reverse
frag = blue reverse
old = red
new = green
[color "status"]
added = green
changed = yellow
untracked = cyan
[interactive]
singlekey = true # Do not requir...
A different testing approach with Minitest and Fixtures
Slow test suites are a major pain point in projects, often due to RSpec
and FactoryBot
. Although minitest
and fixtures are sometimes viewed as outdated, they can greatly improve test speed.
We adopted a project using minitest and fixtures, and while it required some initial refactoring and establishing good practices, the faster test suite was well worth it! Stick with me to explore how these tools might actually be a good practice.
So, why is this setup faster? Partially, it's because minitest is more lightweight than RSpec
, which...
RSpec: Leverage the power of Capybara Finders and Matchers for view specs
View specs are a powerful tool to test several rendering paths by their cases instead of using a more costing feature spec. This is especially useful because they become quite convenient when used with Capybara::Node::Finders and Capybara::RSpecMatchers. This allows to wirte view unit specs as you can isolate specific part...
Case sensitivity in PostgreSQL
PostgreSQL, unlike MySQL, treats strings as case sensitive in all circumstances.
This includes
- comparison with
=
andLIKE
- collision detection in unique indexes
Usually this is fine, but some strings (like emails and usernames) should typically be treated as case insensitive.
There are a few workarounds available:
- use the citext extension (not recommended)
- use
ILIKE
instead ofLIKE
- use Postgres'
lower()
function - add an index on
lower(email)
Probably th...
Using multiple MySQL versions on the same linux machine using docker
We had a card that described how to install multiple mysql versions using mysql-sandbox
. Nowadays with the wide adoption of docker it might be easier to use a MySQL docker image for this purpose.
Create a new mysql instance
docker run --name projectname_db -e MYSQL_ROOT_PASSWORD=secret -p "33008:3306" -d --restart unless-stopped mysql:5.7
The port 33008 is a freely chosen free port on the host machine that will be used to establish a...