Back to Repositories

Testing Reserved Word Validation for URL Slugs in friendly_id

This test suite validates the reserved word functionality in FriendlyId, ensuring proper handling of reserved slugs in URL-friendly identifiers. It covers validation, error handling, and customization of reserved word behavior in ActiveRecord models.

Test Coverage Overview

The test suite comprehensively covers reserved word handling in FriendlyId slugs.

Key areas tested include:
  • Reserved word validation for common terms like ‘new’ and ‘edit’
  • Case-insensitive reserved word matching
  • Slug candidate rejection and fallback behavior
  • Custom conflict resolution for reserved words

Implementation Analysis

The testing approach uses a Journalist model as a test fixture, implementing FriendlyId with slugged and reserved modules. The suite employs transaction blocks for database isolation and custom method overrides to test specific scenarios.

Key patterns include:
  • Dynamic slug candidate generation
  • Error message propagation between attributes
  • Configurable reserved word handling

Technical Details

Testing infrastructure includes:
  • Minitest framework with TestCaseClass
  • ActiveRecord integration
  • FriendlyId::Test module inclusion
  • Transaction-wrapped test cases
  • Custom assertion helpers

Best Practices Demonstrated

The test suite exemplifies robust testing practices for URL slug generation and validation.

Notable practices include:
  • Isolated test cases with clear assertions
  • Comprehensive edge case coverage
  • Clean setup and teardown using transactions
  • Flexible test configurations
  • Clear error handling verification

norman/friendly_id

test/reserved_test.rb

            
require "helper"

class ReservedTest < TestCaseClass
  include FriendlyId::Test

  class Journalist < ActiveRecord::Base
    extend FriendlyId
    friendly_id :slug_candidates, use: [:slugged, :reserved], reserved_words: %w[new edit]

    after_validation :move_friendly_id_error_to_name

    def move_friendly_id_error_to_name
      errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
    end

    def slug_candidates
      name
    end
  end

  def model_class
    Journalist
  end

  test "should reserve words" do
    %w[new edit NEW Edit].each do |word|
      transaction do
        assert_raises(ActiveRecord::RecordInvalid) { model_class.create! name: word }
      end
    end
  end

  test "should move friendly_id error to name" do
    with_instance_of(model_class) do |record|
      record.errors.add :name, "xxx"
      record.errors.add :friendly_id, "yyy"
      record.move_friendly_id_error_to_name
      assert record.errors[:name].present? && record.errors[:friendly_id].blank?
      assert_equal 2, record.errors.count
    end
  end

  test "should reject reserved candidates" do
    transaction do
      record = model_class.new(name: "new")
      def record.slug_candidates
        [:name, "foo"]
      end
      record.save!
      assert_equal "foo", record.friendly_id
    end
  end

  test "should be invalid if all candidates are reserved" do
    transaction do
      record = model_class.new(name: "new")
      def record.slug_candidates
        ["edit", "new"]
      end
      assert_raises(ActiveRecord::RecordInvalid) { record.save! }
    end
  end

  test "should optionally treat reserved words as conflict" do
    klass = Class.new(model_class) do
      friendly_id :slug_candidates, use: [:slugged, :reserved], reserved_words: %w[new edit], treat_reserved_as_conflict: true
    end

    with_instance_of(klass, name: "new") do |record|
      assert_match(/new-([0-9a-z]+-){4}[0-9a-z]+\z/, record.slug)
    end
  end
end