Skip to content

Instantly share code, notes, and snippets.

@AlexB52
Last active March 8, 2024 05:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AlexB52/b991403047b0b89530490477fbf572c0 to your computer and use it in GitHub Desktop.
Save AlexB52/b991403047b0b89530490477fbf572c0 to your computer and use it in GitHub Desktop.
How to secure a unique random code in Rails without hitting a race condition
# Made as an answer to the question is rails forums
# https://discuss.rubyonrails.org/t/how-would-you-handle-gracefully-validating-a-generated-unique-code-until-its-guaranteed-to-be-valid/84919/9
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails"
gem "sqlite3"
gem "debug"
end
require "active_record"
require "minitest/autorun"
require "debug"
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
# ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :license_reserved_numbers, id: false, force: true do |t|
t.string :number, primary_key: true
end
create_table :licenses, force: true do |t|
t.string :number, null: false
t.index [:number], unique: true
end
add_foreign_key :licenses, :license_reserved_numbers, column: :number, primary_key: :number
end
class RandomCode
def initialize
@enum = ('a'..'z').each
end
def random_code
@enum.next
end
end
class License < ActiveRecord::Base
validates :number, presence: true, uniqueness: true
before_validation ->(license) { license[:number] = ReservedNumber.reserve }, on: :create
def number=(value)
raise NotImplementedError, 'number cannot be explicitly set'
end
end
class License::ReservedNumber < ActiveRecord::Base
validates :number, presence: true, uniqueness: true
def self.reserve(generator: RandomCode.new)
record = new(number: nil)
until record.save
record.number = generator.random_code
end
record.number
end
end
class TestLicenseNumber < Minitest::Test
def teardown
License.destroy_all
License::ReservedNumber.destroy_all
end
def test_presence_validations
subject = License::ReservedNumber.new
subject.valid?
assert_equal ['Number can\'t be blank'], subject.errors.full_messages
end
def test_unique_validation
License::ReservedNumber.create!(number: 'a code')
subject = License::ReservedNumber.create(number: 'a code')
subject.valid?
assert_equal ['Number has already been taken'], subject.errors.full_messages
end
end
class TestLicense < Minitest::Test
def teardown
License.destroy_all
License::ReservedNumber.destroy_all
end
def test_setting_the_number_directly
assert_raises(NotImplementedError) do
License.create!(number: 'a code')
end
assert_raises(NotImplementedError) do
license = License.new
license.number = 'a code'
end
end
def test_self_attribution_of_license_number
subject = License.create
assert_equal License::ReservedNumber.last.number, subject.number
end
def test_assignment_of_last_existing_code
('a'..'y').each { |number| License.create! }
assert_equal 'z', License.create!.number
end
def test_destroy_assigned_reserved_number
subject = License::ReservedNumber.create!(number: 'a code')
subject.destroy!
assert_raises(ActiveRecord::InvalidForeignKey) do
license = License.create!
License::ReservedNumber.find(license.number).destroy
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment