# frozen_string_literal: true RSpec.describe Dispatch::Adapter::Claude::PKCE do describe ".generate" do subject(:pair) { described_class.generate } it "returns a hash with :verifier and :challenge keys" do expect(pair).to have_key(:verifier) expect(pair).to have_key(:challenge) end it "verifier is 43 characters of URL-safe base64 without padding" do verifier = pair[:verifier] expect(verifier.length).to eq(43) expect(verifier).to match(/\A[A-Za-z0-9\-_]+\z/) expect(verifier).not_to include("=") end it "challenge is 43 characters of SHA-256(verifier) base64url without padding" do challenge = pair[:challenge] expect(challenge.length).to eq(43) expect(challenge).to match(/\A[A-Za-z0-9\-_]+\z/) expect(challenge).not_to include("=") end it "challenge matches SHA-256(verifier) manually computed" do verifier = pair[:verifier] expected_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(verifier)).delete("=") expect(pair[:challenge]).to eq(expected_challenge) end it "two consecutive calls return different verifiers" do pair1 = described_class.generate pair2 = described_class.generate expect(pair1[:verifier]).not_to eq(pair2[:verifier]) end it "two consecutive calls return different challenges" do pair1 = described_class.generate pair2 = described_class.generate expect(pair1[:challenge]).not_to eq(pair2[:challenge]) end end describe ".base64url" do it "encodes bytes as URL-safe base64 without padding" do bytes = "\xFF\xFE\xFD" result = described_class.base64url(bytes) expect(result).not_to include("=") expect(result).not_to include("+") expect(result).not_to include("/") end it "produces URL-safe characters only" do result = described_class.base64url(SecureRandom.bytes(32)) expect(result).to match(/\A[A-Za-z0-9\-_]+\z/) end end end