Back to Repositories

Validating Parameter Length Constraints in ruby-grape/grape

This test suite validates the length validation functionality in the Grape API framework, focusing on parameter length constraints and validation behaviors. It thoroughly examines various length validation scenarios and error handling for API endpoint parameters.

Test Coverage Overview

The test suite provides comprehensive coverage of length validation scenarios in Grape API parameters.

Key areas tested include:
  • Minimum and maximum length constraints
  • Single constraint validations (min-only or max-only)
  • Exact length matching
  • Zero-length handling
  • Custom error messages
  • Invalid constraint combinations

Implementation Analysis

The testing approach uses RSpec’s describe/context/it blocks to organize test cases systematically. It leverages let_it_be for efficient test setup and employs HTTP POST requests to validate endpoint behaviors.

The implementation demonstrates proper error handling patterns and status code verification for both valid and invalid scenarios.

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • Grape API framework for endpoint definitions
  • Custom validator class (LengthValidator)
  • HTTP status codes (201 for success, 400 for validation errors)
  • Response body validation for error messages

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Descriptive context blocks for clear test organization
  • Comprehensive edge case coverage
  • Consistent error message validation
  • Isolated test cases for each validation scenario
  • Clear separation of success and failure cases

ruby-grape/grape

spec/grape/validations/validators/length_validator_spec.rb

            
# frozen_string_literal: true

describe Grape::Validations::Validators::LengthValidator do
  let_it_be(:app) do
    Class.new(Grape::API) do
      params do
        requires :list, length: { min: 2, max: 3 }
      end
      post 'with_min_max' do
      end

      params do
        requires :list, type: [Integer], length: { min: 2 }
      end
      post 'with_min_only' do
      end

      params do
        requires :list, type: [Integer], length: { max: 3 }
      end
      post 'with_max_only' do
      end

      params do
        requires :list, type: Integer, length: { max: 3 }
      end
      post 'type_is_not_array' do
      end

      params do
        requires :list, type: Hash, length: { max: 3 }
      end
      post 'type_supports_length' do
      end

      params do
        requires :list, type: [Integer], length: { min: -3 }
      end
      post 'negative_min' do
      end

      params do
        requires :list, type: [Integer], length: { max: -3 }
      end
      post 'negative_max' do
      end

      params do
        requires :list, type: [Integer], length: { min: 2.5 }
      end
      post 'float_min' do
      end

      params do
        requires :list, type: [Integer], length: { max: 2.5 }
      end
      post 'float_max' do
      end

      params do
        requires :list, type: [Integer], length: { min: 15, max: 3 }
      end
      post 'min_greater_than_max' do
      end

      params do
        requires :list, type: [Integer], length: { min: 3, max: 3 }
      end
      post 'min_equal_to_max' do
      end

      params do
        requires :list, type: [JSON], length: { min: 0 }
      end
      post 'zero_min' do
      end

      params do
        requires :list, type: [JSON], length: { max: 0 }
      end
      post 'zero_max' do
      end

      params do
        requires :list, type: [Integer], length: { min: 2, message: 'not match' }
      end
      post '/custom-message' do
      end

      params do
        requires :code, length: { is: 2 }
      end
      post 'is' do
      end

      params do
        requires :code, length: { is: -2 }
      end
      post 'negative_is' do
      end

      params do
        requires :code, length: { is: 2, max: 10 }
      end
      post 'is_with_max' do
      end
    end
  end

  describe '/with_min_max' do
    context 'when length is within limits' do
      it do
        post '/with_min_max', list: [1, 2]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length is exceeded' do
      it do
        post '/with_min_max', list: [1, 2, 3, 4, 5]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length within 2 and 3')
      end
    end

    context 'when length is less than minimum' do
      it do
        post '/with_min_max', list: [1]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length within 2 and 3')
      end
    end
  end

  describe '/with_max_only' do
    context 'when length is less than limits' do
      it do
        post '/with_max_only', list: [1, 2]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length is exceeded' do
      it do
        post '/with_max_only', list: [1, 2, 3, 4, 5]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length less than or equal to 3')
      end
    end
  end

  describe '/with_min_only' do
    context 'when length is greater than limit' do
      it do
        post '/with_min_only', list: [1, 2]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length is less than limit' do
      it do
        post '/with_min_only', list: [1]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length greater than or equal to 2')
      end
    end
  end

  describe '/zero_min' do
    context 'when length is equal to the limit' do
      it do
        post '/zero_min', list: '[]'
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length is greater than limit' do
      it do
        post '/zero_min', list: [{ key: 'value' }]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end
  end

  describe '/zero_max' do
    context 'when length is within the limit' do
      it do
        post '/zero_max', list: '[]'
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length is greater than limit' do
      it do
        post '/zero_max', list: [{ key: 'value' }]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length less than or equal to 0')
      end
    end
  end

  describe '/type_is_not_array' do
    context 'does not raise an error' do
      it do
        expect do
          post 'type_is_not_array', list: 12
        end.not_to raise_error
      end
    end
  end

  describe '/type_supports_length' do
    context 'when length is within limits' do
      it do
        post 'type_supports_length', list: { key: 'value' }
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length exceeds the limit' do
      it do
        post 'type_supports_length', list: { key: 'value', key1: 'value', key3: 'value', key4: 'value' }
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length less than or equal to 3')
      end
    end
  end

  describe '/negative_min' do
    context 'when min is negative' do
      it do
        expect { post 'negative_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero')
      end
    end
  end

  describe '/negative_max' do
    context 'it raises an error' do
      it do
        expect { post 'negative_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero')
      end
    end
  end

  describe '/float_min' do
    context 'when min is not an integer' do
      it do
        expect { post 'float_min', list: [12] }.to raise_error(ArgumentError, 'min must be an integer greater than or equal to zero')
      end
    end
  end

  describe '/float_max' do
    context 'when max is not an integer' do
      it do
        expect { post 'float_max', list: [12] }.to raise_error(ArgumentError, 'max must be an integer greater than or equal to zero')
      end
    end
  end

  describe '/min_greater_than_max' do
    context 'raises an error' do
      it do
        expect { post 'min_greater_than_max', list: [1, 2] }.to raise_error(ArgumentError, 'min 15 cannot be greater than max 3')
      end
    end
  end

  describe '/min_equal_to_max' do
    context 'when array meets expectations' do
      it do
        post 'min_equal_to_max', list: [1, 2, 3]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when array is less than min' do
      it do
        post 'min_equal_to_max', list: [1, 2]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length within 3 and 3')
      end
    end

    context 'when array is greater than max' do
      it do
        post 'min_equal_to_max', list: [1, 2, 3, 4]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list is expected to have length within 3 and 3')
      end
    end
  end

  describe '/custom-message' do
    context 'is within limits' do
      it do
        post '/custom-message', list: [1, 2, 3]
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'is outside limit' do
      it do
        post '/custom-message', list: [1]
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('list not match')
      end
    end
  end

  describe '/is' do
    context 'when length is exact' do
      it do
        post 'is', code: 'ZZ'
        expect(last_response.status).to eq(201)
        expect(last_response.body).to eq('')
      end
    end

    context 'when length exceeds the limit' do
      it do
        post 'is', code: 'aze'
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('code is expected to have length exactly equal to 2')
      end
    end

    context 'when length is less than the limit' do
      it do
        post 'is', code: 'a'
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('code is expected to have length exactly equal to 2')
      end
    end

    context 'when length is zero' do
      it do
        post 'is', code: ''
        expect(last_response.status).to eq(400)
        expect(last_response.body).to eq('code is expected to have length exactly equal to 2')
      end
    end
  end

  describe '/negative_is' do
    context 'when `is` is negative' do
      it do
        expect { post 'negative_is', code: 'ZZ' }.to raise_error(ArgumentError, 'is must be an integer greater than zero')
      end
    end
  end

  describe '/is_with_max' do
    context 'when `is` is combined with max' do
      it do
        expect { post 'is_with_max', code: 'ZZ' }.to raise_error(ArgumentError, 'is cannot be combined with min or max')
      end
    end
  end
end