Back to Repositories

Validating AllowBlank Parameter Validation in ruby-grape/grape

This test suite validates the AllowBlankValidator functionality in the Grape API framework, focusing on parameter validation behavior for blank and empty values across different data types and parameter configurations.

Test Coverage Overview

The test suite comprehensively covers blank value validation for required and optional parameters, handling various data types including String, DateTime, Integer, Float, Symbol, and Boolean. Key test scenarios include:

  • Empty string validation
  • Whitespace handling
  • Nil value processing
  • Nested parameter validation in optional and required groups
  • Custom validation message handling

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with nested contexts and shared examples. The implementation validates both simple parameters and complex nested structures, employing Grape’s parameter validation DSL for defining validation rules.

Key patterns include request simulation, response status verification, and error message validation.

Technical Details

  • Testing Framework: RSpec
  • API Framework: Grape
  • Test Types: Unit tests with HTTP request simulation
  • Configuration: Default JSON format
  • Helper Methods: let_it_be for shared test setup

Best Practices Demonstrated

The test suite exemplifies several testing best practices including:

  • Comprehensive edge case coverage
  • Clear context organization
  • Consistent validation patterns
  • Detailed error scenario testing
  • Modular test structure with focused contexts

ruby-grape/grape

spec/grape/validations/validators/allow_blank_validator_spec.rb

            
# frozen_string_literal: true

describe Grape::Validations::Validators::AllowBlankValidator do
  let_it_be(:app) do
    Class.new(Grape::API) do
      default_format :json

      params do
        requires :name, allow_blank: false
      end
      get '/disallow_blank'

      params do
        optional :name, type: String, allow_blank: false
      end
      get '/opt_disallow_string_blank'

      params do
        optional :name, allow_blank: false
      end
      get '/disallow_blank_optional_param'

      params do
        requires :name, allow_blank: true
      end
      get '/allow_blank'

      params do
        requires :val, type: DateTime, allow_blank: true
      end
      get '/allow_datetime_blank'

      params do
        requires :val, type: DateTime, allow_blank: false
      end
      get '/disallow_datetime_blank'

      params do
        requires :val, type: DateTime
      end
      get '/default_allow_datetime_blank'

      params do
        requires :val, type: Date, allow_blank: true
      end
      get '/allow_date_blank'

      params do
        requires :val, type: Integer, allow_blank: true
      end
      get '/allow_integer_blank'

      params do
        requires :val, type: Float, allow_blank: true
      end
      get '/allow_float_blank'

      params do
        requires :val, type: Integer, allow_blank: true
      end
      get '/allow_integer_blank'

      params do
        requires :val, type: Symbol, allow_blank: true
      end
      get '/allow_symbol_blank'

      params do
        requires :val, type: Grape::API::Boolean, allow_blank: true
      end
      get '/allow_boolean_blank'

      params do
        requires :val, type: Grape::API::Boolean, allow_blank: false
      end
      get '/disallow_boolean_blank'

      params do
        optional :user, type: Hash do
          requires :name, allow_blank: false
        end
      end
      get '/disallow_blank_required_param_in_an_optional_group'

      params do
        optional :user, type: Hash do
          requires :name, type: Date, allow_blank: true
        end
      end
      get '/allow_blank_date_param_in_an_optional_group'

      params do
        optional :user, type: Hash do
          optional :name, allow_blank: false
          requires :age
        end
      end
      get '/disallow_blank_optional_param_in_an_optional_group'

      params do
        requires :user, type: Hash do
          requires :name, allow_blank: false
        end
      end
      get '/disallow_blank_required_param_in_a_required_group'

      params do
        requires :user, type: Hash do
          requires :name, allow_blank: false
        end
      end
      get '/disallow_string_value_in_a_required_hash_group'

      params do
        requires :user, type: Hash do
          optional :name, allow_blank: false
        end
      end
      get '/disallow_blank_optional_param_in_a_required_group'

      params do
        optional :user, type: Hash do
          optional :name, allow_blank: false
        end
      end
      get '/disallow_string_value_in_an_optional_hash_group'

      resources :custom_message do
        params do
          requires :name, allow_blank: { value: false, message: 'has no value' }
        end
        get

        params do
          optional :name, allow_blank: { value: false, message: 'has no value' }
        end
        get '/disallow_blank_optional_param'

        params do
          requires :name, allow_blank: true
        end
        get '/allow_blank'

        params do
          requires :val, type: DateTime, allow_blank: true
        end
        get '/allow_datetime_blank'

        params do
          requires :val, type: DateTime, allow_blank: { value: false, message: 'has no value' }
        end
        get '/disallow_datetime_blank'

        params do
          requires :val, type: DateTime
        end
        get '/default_allow_datetime_blank'

        params do
          requires :val, type: Date, allow_blank: true
        end
        get '/allow_date_blank'

        params do
          requires :val, type: Integer, allow_blank: true
        end
        get '/allow_integer_blank'

        params do
          requires :val, type: Float, allow_blank: true
        end
        get '/allow_float_blank'

        params do
          requires :val, type: Integer, allow_blank: true
        end
        get '/allow_integer_blank'

        params do
          requires :val, type: Symbol, allow_blank: true
        end
        get '/allow_symbol_blank'

        params do
          requires :val, type: Grape::API::Boolean, allow_blank: true
        end
        get '/allow_boolean_blank'

        params do
          requires :val, type: Grape::API::Boolean, allow_blank: { value: false, message: 'has no value' }
        end
        get '/disallow_boolean_blank'

        params do
          optional :user, type: Hash do
            requires :name, allow_blank: { value: false, message: 'has no value' }
          end
        end
        get '/disallow_blank_required_param_in_an_optional_group'

        params do
          optional :user, type: Hash do
            requires :name, type: Date, allow_blank: true
          end
        end
        get '/allow_blank_date_param_in_an_optional_group'

        params do
          optional :user, type: Hash do
            optional :name, allow_blank: { value: false, message: 'has no value' }
            requires :age
          end
        end
        get '/disallow_blank_optional_param_in_an_optional_group'

        params do
          requires :user, type: Hash do
            requires :name, allow_blank: { value: false, message: 'has no value' }
          end
        end
        get '/disallow_blank_required_param_in_a_required_group'

        params do
          requires :user, type: Hash do
            requires :name, allow_blank: { value: false, message: 'has no value' }
          end
        end
        get '/disallow_string_value_in_a_required_hash_group'

        params do
          requires :user, type: Hash do
            optional :name, allow_blank: { value: false, message: 'has no value' }
          end
        end
        get '/disallow_blank_optional_param_in_a_required_group'

        params do
          optional :user, type: Hash do
            optional :name, allow_blank: { value: false, message: 'has no value' }
          end
        end
        get '/disallow_string_value_in_an_optional_hash_group'
      end
    end
  end

  context 'invalid input' do
    it 'refuses empty string' do
      get '/disallow_blank', name: ''
      expect(last_response.status).to eq(400)

      get '/disallow_datetime_blank', val: ''
      expect(last_response.status).to eq(400)
    end

    it 'refuses only whitespaces' do
      get '/disallow_blank', name: '   '
      expect(last_response.status).to eq(400)

      get '/disallow_blank', name: "  \n "
      expect(last_response.status).to eq(400)

      get '/disallow_blank', name: "\n"
      expect(last_response.status).to eq(400)
    end

    it 'refuses nil' do
      get '/disallow_blank', name: nil
      expect(last_response.status).to eq(400)
    end

    it 'refuses missing' do
      get '/disallow_blank'
      expect(last_response.status).to eq(400)
    end
  end

  context 'custom validation message' do
    context 'with invalid input' do
      it 'refuses empty string' do
        get '/custom_message', name: ''
        expect(last_response.body).to eq('{"error":"name has no value"}')
      end

      it 'refuses empty string for an optional param' do
        get '/custom_message/disallow_blank_optional_param', name: ''
        expect(last_response.body).to eq('{"error":"name has no value"}')
      end

      it 'refuses only whitespaces' do
        get '/custom_message', name: '   '
        expect(last_response.body).to eq('{"error":"name has no value"}')

        get '/custom_message', name: "  \n "
        expect(last_response.body).to eq('{"error":"name has no value"}')

        get '/custom_message', name: "\n"
        expect(last_response.body).to eq('{"error":"name has no value"}')
      end

      it 'refuses nil' do
        get '/custom_message', name: nil
        expect(last_response.body).to eq('{"error":"name has no value"}')
      end
    end

    context 'with valid input' do
      it 'accepts valid input' do
        get '/custom_message', name: 'bob'
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty input when allow_blank is false' do
        get '/custom_message/allow_blank', name: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty input' do
        get '/custom_message/default_allow_datetime_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty when datetime allow_blank' do
        get '/custom_message/allow_datetime_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty when date allow_blank' do
        get '/custom_message/allow_date_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      context 'allow_blank when Numeric' do
        it 'accepts empty when integer allow_blank' do
          get '/custom_message/allow_integer_blank', val: ''
          expect(last_response.status).to eq(200)
        end

        it 'accepts empty when float allow_blank' do
          get '/custom_message/allow_float_blank', val: ''
          expect(last_response.status).to eq(200)
        end

        it 'accepts empty when integer allow_blank' do
          get '/custom_message/allow_integer_blank', val: ''
          expect(last_response.status).to eq(200)
        end
      end

      it 'accepts empty when symbol allow_blank' do
        get '/custom_message/allow_symbol_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty when boolean allow_blank' do
        get '/custom_message/allow_boolean_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts false when boolean allow_blank' do
        get '/custom_message/disallow_boolean_blank', val: false
        expect(last_response.status).to eq(200)
      end
    end

    context 'in an optional group' do
      context 'as a required param' do
        it 'accepts a missing group, even with a disallwed blank param' do
          get '/custom_message/disallow_blank_required_param_in_an_optional_group'
          expect(last_response.status).to eq(200)
        end

        it 'accepts a nested missing date value' do
          get '/custom_message/allow_blank_date_param_in_an_optional_group', user: { name: '' }
          expect(last_response.status).to eq(200)
        end

        it 'refuses a blank value in an existing group' do
          get '/custom_message/disallow_blank_required_param_in_an_optional_group', user: { name: '' }
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user[name] has no value"}')
        end
      end

      context 'as an optional param' do
        it 'accepts a missing group, even with a disallwed blank param' do
          get '/custom_message/disallow_blank_optional_param_in_an_optional_group'
          expect(last_response.status).to eq(200)
        end

        it 'accepts a nested missing optional value' do
          get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' }
          expect(last_response.status).to eq(200)
        end

        it 'refuses a blank existing value in an existing scope' do
          get '/custom_message/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' }
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user[name] has no value"}')
        end
      end
    end

    context 'in a required group' do
      context 'as a required param' do
        it 'refuses a blank value in a required existing group' do
          get '/custom_message/disallow_blank_required_param_in_a_required_group', user: { name: '' }
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user[name] has no value"}')
        end

        it 'refuses a string value in a required hash group' do
          get '/custom_message/disallow_string_value_in_a_required_hash_group', user: ''
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user is invalid, user[name] is missing"}')
        end
      end

      context 'as an optional param' do
        it 'accepts a nested missing value' do
          get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29' }
          expect(last_response.status).to eq(200)
        end

        it 'refuses a blank existing value in an existing scope' do
          get '/custom_message/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' }
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user[name] has no value"}')
        end

        it 'refuses a string value in an optional hash group' do
          get '/custom_message/disallow_string_value_in_an_optional_hash_group', user: ''
          expect(last_response.status).to eq(400)
          expect(last_response.body).to eq('{"error":"user is invalid"}')
        end
      end
    end
  end

  context 'valid input' do
    it 'allows missing optional strings' do
      get 'opt_disallow_string_blank'
      expect(last_response.status).to eq(200)
    end

    it 'accepts valid input' do
      get '/disallow_blank', name: 'bob'
      expect(last_response.status).to eq(200)
    end

    it 'accepts empty input when allow_blank is false' do
      get '/allow_blank', name: ''
      expect(last_response.status).to eq(200)
    end

    it 'accepts empty input' do
      get '/default_allow_datetime_blank', val: ''
      expect(last_response.status).to eq(200)
    end

    it 'accepts empty when datetime allow_blank' do
      get '/allow_datetime_blank', val: ''
      expect(last_response.status).to eq(200)
    end

    it 'accepts empty when date allow_blank' do
      get '/allow_date_blank', val: ''
      expect(last_response.status).to eq(200)
    end

    context 'allow_blank when Numeric' do
      it 'accepts empty when integer allow_blank' do
        get '/allow_integer_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty when float allow_blank' do
        get '/allow_float_blank', val: ''
        expect(last_response.status).to eq(200)
      end

      it 'accepts empty when integer allow_blank' do
        get '/allow_integer_blank', val: ''
        expect(last_response.status).to eq(200)
      end
    end

    it 'accepts empty when symbol allow_blank' do
      get '/allow_symbol_blank', val: ''
      expect(last_response.status).to eq(200)
    end

    it 'accepts empty when boolean allow_blank' do
      get '/allow_boolean_blank', val: ''
      expect(last_response.status).to eq(200)
    end

    it 'accepts false when boolean allow_blank' do
      get '/disallow_boolean_blank', val: false
      expect(last_response.status).to eq(200)
    end

    it 'accepts value when time allow_blank' do
      get '/disallow_datetime_blank', val: Time.now
      expect(last_response.status).to eq(200)
    end
  end

  context 'in an optional group' do
    context 'as a required param' do
      it 'accepts a missing group, even with a disallwed blank param' do
        get '/disallow_blank_required_param_in_an_optional_group'
        expect(last_response.status).to eq(200)
      end

      it 'accepts a nested missing date value' do
        get '/allow_blank_date_param_in_an_optional_group', user: { name: '' }
        expect(last_response.status).to eq(200)
      end

      it 'refuses a blank value in an existing group' do
        get '/disallow_blank_required_param_in_an_optional_group', user: { name: '' }
        expect(last_response.status).to eq(400)
      end
    end

    context 'as an optional param' do
      it 'accepts a missing group, even with a disallwed blank param' do
        get '/disallow_blank_optional_param_in_an_optional_group'
        expect(last_response.status).to eq(200)
      end

      it 'accepts a nested missing optional value' do
        get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29' }
        expect(last_response.status).to eq(200)
      end

      it 'refuses a blank existing value in an existing scope' do
        get '/disallow_blank_optional_param_in_an_optional_group', user: { age: '29', name: '' }
        expect(last_response.status).to eq(400)
      end
    end
  end

  context 'in a required group' do
    context 'as a required param' do
      it 'refuses a blank value in a required existing group' do
        get '/disallow_blank_required_param_in_a_required_group', user: { name: '' }
        expect(last_response.status).to eq(400)
      end

      it 'refuses a string value in a required hash group' do
        get '/disallow_string_value_in_a_required_hash_group', user: ''
        expect(last_response.status).to eq(400)
      end
    end

    context 'as an optional param' do
      it 'accepts a nested missing value' do
        get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29' }
        expect(last_response.status).to eq(200)
      end

      it 'refuses a blank existing value in an existing scope' do
        get '/disallow_blank_optional_param_in_a_required_group', user: { age: '29', name: '' }
        expect(last_response.status).to eq(400)
      end

      it 'refuses a string value in an optional hash group' do
        get '/disallow_string_value_in_an_optional_hash_group', user: ''
        expect(last_response.status).to eq(400)
      end
    end
  end
end