Back to Repositories

Testing HashWithIndifferentAccess Parameter Building in Grape API

This test suite validates the HashWithIndifferentAccess parameter builder extension in Grape API. It ensures consistent parameter handling regardless of whether string or symbol keys are used, with comprehensive testing of nested parameter structures and route parameter interactions.

Test Coverage Overview

The test suite provides extensive coverage of parameter handling functionality in Grape API endpoints.

  • Tests parameter access using both string and symbol keys
  • Validates nested hash parameter structures
  • Verifies array parameter handling
  • Confirms route parameter behavior and precedence

Implementation Analysis

The testing approach focuses on validating the HashWithIndifferentAccess integration within Grape’s parameter handling system. It employs RSpec’s describe/context patterns and before blocks for setup, with explicit test cases for parameter access patterns and edge cases.

  • Uses subject-based testing for API class definition
  • Implements endpoint-specific and API-wide parameter builder configuration
  • Validates parameter access across different scopes

Technical Details

Testing infrastructure leverages:

  • RSpec for test structure and assertions
  • Grape API framework’s testing helpers
  • ActiveSupport::HashWithIndifferentAccess extension
  • HTTP request simulation for endpoint testing

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices in Ruby API development.

  • Comprehensive edge case coverage
  • Clear test organization and setup
  • Isolated test scenarios
  • Consistent assertion patterns
  • Proper use of RSpec’s expect syntax

ruby-grape/grape

spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb

            
# frozen_string_literal: true

describe Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder do
  subject { Class.new(Grape::API) }

  def app
    subject
  end

  describe 'in an endpoint' do
    describe '#params' do
      before do
        subject.params do
          build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass
        end

        subject.get do
          params.class
        end
      end

      it 'is of type Hash' do
        get '/'
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess')
      end
    end
  end

  describe 'in an api' do
    before do
      subject.include Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass
    end

    describe '#params' do
      before do
        subject.get do
          params.class
        end
      end

      it 'is a Hash' do
        get '/'
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('ActiveSupport::HashWithIndifferentAccess')
      end

      it 'parses sub hash params' do
        subject.params do
          build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass

          optional :a, type: Hash do
            optional :b, type: Hash do
              optional :c, type: String
            end
            optional :d, type: Array
          end
        end

        subject.get '/foo' do
          [params[:a]['b'][:c], params['a'][:d]]
        end

        get '/foo', a: { b: { c: 'bar' }, d: ['foo'] }
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('["bar", ["foo"]]')
      end

      it 'params are indifferent to symbol or string keys' do
        subject.params do
          build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass
          optional :a, type: Hash do
            optional :b, type: Hash do
              optional :c, type: String
            end
            optional :d, type: Array
          end
        end

        subject.get '/foo' do
          [params[:a]['b'][:c], params['a'][:d]]
        end

        get '/foo', 'a' => { b: { c: 'bar' }, 'd' => ['foo'] }
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('["bar", ["foo"]]')
      end

      it 'responds to string keys' do
        subject.params do
          build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder # rubocop:disable RSpec/DescribedClass
          requires :a, type: String
        end

        subject.get '/foo' do
          [params[:a], params['a']]
        end

        get '/foo', a: 'bar'
        expect(last_response.status).to eq(200)
        expect(last_response.body).to eq('["bar", "bar"]')
      end
    end

    it 'does not overwrite route_param with a regular param if they have same name' do
      subject.namespace :route_param do
        route_param :foo do
          get { params.to_json }
        end
      end

      get '/route_param/bar', foo: 'baz'
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('{"foo":"bar"}')
    end

    it 'does not overwrite route_param with a defined regular param if they have same name' do
      subject.namespace :route_param do
        params do
          requires :foo, type: String
        end
        route_param :foo do
          get do
            [params[:foo], params['foo']]
          end
        end
      end

      get '/route_param/bar', foo: 'baz'
      expect(last_response.status).to eq(200)
      expect(last_response.body).to eq('["bar", "bar"]')
    end
  end
end