Back to Repositories

Validating AllOrNoneOf Parameter Constraints in ruby-grape/grape

This test suite validates the AllOrNoneOfValidator functionality in the Grape API framework, focusing on parameter validation rules that require either all specified parameters to be present or none of them. The tests cover various scenarios including nested parameters, custom error messages, and complex validation logic.

Test Coverage Overview

The test suite provides comprehensive coverage of the AllOrNoneOfValidator functionality:

  • Basic parameter validation scenarios
  • Nested parameter validation in hashes and arrays
  • Custom error message handling
  • Edge cases with mixed parameters
  • Deep nesting validation scenarios

Implementation Analysis

The testing approach utilizes RSpec’s describe/context structure to organize test cases logically. It implements let_it_be for efficient test setup and employs subject-based testing patterns. The implementation leverages Grape’s API class and validation framework to test parameter constraints.

Technical Details

Testing tools and configuration:

  • RSpec as the testing framework
  • Grape::API for API implementation
  • Custom error handling with rescue_from
  • Boolean type parameters
  • HTTP status code validation
  • JSON response parsing

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Clear test organization using describe and context blocks
  • Consistent subject naming conventions
  • Thorough error case coverage
  • DRY test setup using shared examples
  • Explicit expectation statements
  • Comprehensive edge case testing

ruby-grape/grape

spec/grape/validations/validators/all_or_none_validator_spec.rb

            
# frozen_string_literal: true

describe Grape::Validations::Validators::AllOrNoneOfValidator 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, :wine, type: Grape::API::Boolean
        all_or_none_of :beer, :wine
      end
      post do
      end

      params do
        optional :beer, :wine, :other, type: Grape::API::Boolean
        all_or_none_of :beer, :wine
      end
      post 'mixed-params' do
      end

      params do
        optional :beer, :wine, type: Grape::API::Boolean
        all_or_none_of :beer, :wine, message: 'choose all or none'
      end
      post '/custom-message' do
      end

      params do
        requires :item, type: Hash do
          optional :beer, :wine, type: Grape::API::Boolean
          all_or_none_of :beer, :wine
        end
      end
      post '/nested-hash' do
      end

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

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

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

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

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

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

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

    context 'when a subset of restricted params are present' do
      let(:path) { '/' }
      let(:params) { { beer: 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' => ['provide all or none of parameters']
        )
      end
    end

    context 'when custom message is specified' do
      let(:path) { '/custom-message' }
      let(:params) { { beer: 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' => ['choose all or none']
        )
      end
    end

    context 'when no restricted params are present' do
      let(:path) { '/' }
      let(:params) { { somethingelse: true } }

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

    context 'when restricted params are nested inside required hash' do
      let(:path) { '/nested-hash' }
      let(:params) { { item: { beer: 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]' => ['provide all or none of parameters']
        )
      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 }] } }

      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[1][beer],items[1][wine]' => ['provide all or none of parameters']
        )
      end
    end

    context 'when mutually exclusive params are deeply nested' do
      let(:path) { '/deeply-nested-array' }
      let(:params) { { items: [{ nested_items: [{ beer: 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]' => [
            'provide all or none of parameters'
          ]
        )
      end
    end
  end
end