summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/adapter/claude/pkce_spec.rb
blob: 0ac90ec9203136bbd565324090001c60e90477de (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 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