Back to Repositories

Validating Mutual Exclusion Parameter Validation in ruby-grape/grape

This test suite validates the MutualExclusionValidator functionality in the Grape API framework, focusing on parameter validation rules that ensure certain parameters cannot be used together.

Test Coverage Overview

The test suite comprehensively covers the MutualExclusionValidator’s parameter validation capabilities.

Key areas tested include:
  • Basic mutual exclusion validation for multiple parameters
  • Nested parameter validation in hashes and arrays
  • Custom error message handling
  • Optional and required parameter combinations
  • Deep nesting scenarios

Implementation Analysis

The testing approach uses RSpec’s describe/context structure to organize test scenarios systematically. It employs let_it_be for efficient test setup and validates both successful and error cases. The implementation leverages Grape’s API class and parameter validation framework, with particular focus on nested data structures and error handling.

Technical Details

Testing tools and configuration:
  • RSpec testing framework
  • Grape API framework
  • JSON response validation
  • HTTP status code verification
  • Custom error handling middleware

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Comprehensive edge case coverage
  • Clear test organization using context blocks
  • Consistent error message validation
  • Thorough nested parameter testing
  • Proper separation of concerns in test cases

ruby-grape/grape

spec/grape/validations/validators/mutual_exclusion_validator_spec.rb

            
# frozen_string_literal: true

describe Grape::Validations::Validators::MutualExclusionValidator do
  let_it_be(:app) do
    Class.new(Grape::API) do
      rescue_from Grape::Exceptions::ValidationErrors do |e|
        error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
      end

      params do
        optional :beer
        optional :wine
        optional :grapefruit
        mutually_exclusive :beer, :wine, :grapefruit
      end
      post do
      end

      params do
        optional :beer
        optional :wine
        optional :grapefruit
        optional :other
        mutually_exclusive :beer, :wine, :grapefruit
      end
      post 'mixed-params' do
      end

      params do
        optional :beer
        optional :wine
        optional :grapefruit
        mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine'
      end
      post '/custom-message' do
      end

      params do
        requires :item, type: Hash do
          optional :beer
          optional :wine
          optional :grapefruit
          mutually_exclusive :beer, :wine, :grapefruit
        end
      end
      post '/nested-hash' do
      end

      params do
        optional :item, type: Hash do
          optional :beer
          optional :wine
          optional :grapefruit
          mutually_exclusive :beer, :wine, :grapefruit
        end
      end
      post '/nested-optional-hash' do
      end

      params do
        requires :items, type: Array do
          optional :beer
          optional :wine
          optional :grapefruit
          mutually_exclusive :beer, :wine, :grapefruit
        end
      end
      post '/nested-array' do
      end

      params do
        requires :items, type: Array do
          requires :nested_items, type: Array do
            optional :beer, :wine, :grapefruit, type: Grape::API::Boolean
            mutually_exclusive :beer, :wine, :grapefruit
          end
        end
      end
      post '/deeply-nested-array' do
      end
    end
  end

  describe '#validate!' do
    subject(:validate) { post path, params }

    context 'when all mutually exclusive params are present' do
      let(:path) { '/' }
      let(:params) { { beer: true, wine: true, grapefruit: true } }

      it 'returns a validation error' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'beer,wine,grapefruit' => ['are mutually exclusive']
        )
      end

      context 'mixed with other params' do
        let(:path) { '/mixed-params' }
        let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }

        it 'returns a validation error' do
          validate
          expect(last_response.status).to eq 400
          expect(JSON.parse(last_response.body)).to eq(
            'beer,wine,grapefruit' => ['are mutually exclusive']
          )
        end
      end
    end

    context 'when a subset of mutually exclusive params are present' do
      let(:path) { '/' }
      let(:params) { { beer: true, grapefruit: true } }

      it 'returns a validation error' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'beer,grapefruit' => ['are mutually exclusive']
        )
      end
    end

    context 'when custom message is specified' do
      let(:path) { '/custom-message' }
      let(:params) { { beer: true, wine: true } }

      it 'returns a validation error' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'beer,wine' => ['you should not mix beer and wine']
        )
      end
    end

    context 'when no mutually exclusive params are present' do
      let(:path) { '/' }
      let(:params) { { beer: true, somethingelse: true } }

      it 'does not return a validation error' do
        validate
        expect(last_response.status).to eq 201
      end
    end

    context 'when mutually exclusive params are nested inside required hash' do
      let(:path) { '/nested-hash' }
      let(:params) { { item: { beer: true, wine: true } } }

      it 'returns a validation error with full names of the params' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'item[beer],item[wine]' => ['are mutually exclusive']
        )
      end
    end

    context 'when mutually exclusive params are nested inside optional hash' do
      let(:path) { '/nested-optional-hash' }

      context 'when params are passed' do
        let(:params) { { item: { beer: true, wine: true } } }

        it 'returns a validation error with full names of the params' do
          validate
          expect(last_response.status).to eq 400
          expect(JSON.parse(last_response.body)).to eq(
            'item[beer],item[wine]' => ['are mutually exclusive']
          )
        end
      end

      context 'when params are empty' do
        let(:params) { {} }

        it 'does not return a validation error' do
          validate
          expect(last_response.status).to eq 201
        end
      end
    end

    context 'when mutually exclusive params are nested inside array' do
      let(:path) { '/nested-array' }
      let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } }

      it 'returns a validation error with full names of the params' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'items[0][beer],items[0][wine]' => ['are mutually exclusive'],
          'items[1][wine],items[1][grapefruit]' => ['are mutually exclusive']
        )
      end
    end

    context 'when mutually exclusive params are deeply nested' do
      let(:path) { '/deeply-nested-array' }
      let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } }

      it 'returns a validation error with full names of the params' do
        validate
        expect(last_response.status).to eq 400
        expect(JSON.parse(last_response.body)).to eq(
          'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => ['are mutually exclusive']
        )
      end
    end
  end
end