Commit 711404fe authored by Sam Wiskow's avatar Sam Wiskow
Browse files

test(rule): add Spec 7 rule name validation scenarios

Adds rule_spec.rb covering all Rule name validation scenarios:
type check, empty name, format (dev/test raises, production passes),
length limit (dev/test raises, production passes), action validation,
match/characteristics normalization, and immutability.

95 examples, 0 failures. Full rubocop clean (193 files, no offenses).

Co-Authored-By: default avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent f89a0530
Loading
Loading
Loading
Loading
+143 −0
Original line number Diff line number Diff line
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Labkit::RateLimit::Rule do
  include StubENV

  before do
    stub_env("RAILS_ENV", "test")
  end

  def valid_rule(name: "api_user", limit: 100, period: 60, characteristics: [:user], **rest)
    described_class.new(name: name, limit: limit, period: period, characteristics: characteristics, **rest)
  end

  describe "name type validation (always enforced)" do
    it "raises when name is nil" do
      expect { valid_rule(name: nil) }
        .to raise_error(ArgumentError, /name must be a String or Symbol/)
    end

    it "raises when name is an Integer" do
      expect { valid_rule(name: 42) }
        .to raise_error(ArgumentError, /name must be a String or Symbol/)
    end

    it "accepts a Symbol name and converts it to String" do
      rule = valid_rule(name: :api_user)
      expect(rule.name).to eq("api_user")
    end

    it "raises when name is an empty string" do
      expect { valid_rule(name: "") }.to raise_error(ArgumentError, /must not be empty/)
    end

    it "raises when Symbol resolves to empty string" do
      expect { valid_rule(name: :"") }.to raise_error(ArgumentError, /must not be empty/)
    end
  end

  describe "name format and length validation" do
    context "when in dev/test environment" do
      it "raises on names with invalid characters" do
        expect { valid_rule(name: "Bad Name!") }
          .to raise_error(ArgumentError, /Invalid rule name/)
      end

      it "raises on names with uppercase letters" do
        expect { valid_rule(name: "ApiUser") }
          .to raise_error(ArgumentError, /Invalid rule name/)
      end

      it "raises on names with hyphens" do
        expect { valid_rule(name: "api-user") }
          .to raise_error(ArgumentError, /Invalid rule name/)
      end

      it "raises when name exceeds 64 characters" do
        expect { valid_rule(name: "a" * 65) }
          .to raise_error(ArgumentError, /Rule name too long/)
      end

      it "accepts a name at exactly 64 characters" do
        rule = valid_rule(name: "a" * 64)
        expect(rule.name.length).to eq(64)
      end
    end

    context "when in production environment" do
      before do
        stub_env("RAILS_ENV", "production")
      end

      it "does not raise on invalid format (deferred to Limiter sanitization)" do
        expect { valid_rule(name: "Bad Name!") }.not_to raise_error
      end

      it "does not raise when name exceeds 64 characters" do
        expect { valid_rule(name: "a" * 65) }.not_to raise_error
      end
    end
  end

  describe "action validation (always enforced)" do
    it "accepts :block" do
      expect(valid_rule(action: :block).action).to eq(:block)
    end

    it "accepts :log" do
      expect(valid_rule(action: :log).action).to eq(:log)
    end

    it "accepts string action and coerces to symbol" do
      expect(valid_rule(action: "block").action).to eq(:block)
    end

    it "raises on unknown action" do
      expect { valid_rule(action: :deny) }
        .to raise_error(ArgumentError, /Invalid action/)
    end
  end

  describe "match and characteristics normalization" do
    it "symbolizes match keys" do
      rule = valid_rule(match: { "endpoint" => "/api/v4" })
      expect(rule.match).to eq({ endpoint: "/api/v4" })
    end

    it "symbolizes characteristics" do
      rule = valid_rule(characteristics: %w[user ip])
      expect(rule.characteristics).to eq(%i[user ip])
    end

    it "wraps a single characteristic in an array" do
      rule = valid_rule(characteristics: :user)
      expect(rule.characteristics).to eq([:user])
    end

    it "defaults match to empty hash" do
      rule = valid_rule
      expect(rule.match).to eq({})
    end

    it "defaults action to :block" do
      rule = valid_rule
      expect(rule.action).to eq(:block)
    end
  end

  describe "immutability" do
    it "freezes the name" do
      expect(valid_rule.name).to be_frozen
    end

    it "freezes the match hash" do
      expect(valid_rule.match).to be_frozen
    end

    it "freezes the characteristics array" do
      expect(valid_rule.characteristics).to be_frozen
    end
  end
end