summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/adapter/claude/token_store_spec.rb
blob: 3db38a218696bdbc5932a2c6cde216f136d9ac3a (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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# frozen_string_literal: true

require "fileutils"
require "tmpdir"

RSpec.describe Dispatch::Adapter::Claude::TokenStore do
  let(:tmpdir) { Dir.mktmpdir("token_store_test") }
  let(:store_path) { File.join(tmpdir, "claude_oauth.json") }
  let(:store) { described_class.new(path: store_path) }

  after { FileUtils.rm_rf(tmpdir) }

  describe "#path" do
    it "returns the configured path" do
      expect(store.path).to eq(store_path)
    end
  end

  describe "#load" do
    it "returns nil when the file does not exist" do
      expect(store.load).to be_nil
    end

    it "returns the parsed hash after save" do
      creds = {
        "access_token" => "sk-ant-oat01-abc",
        "refresh_token" => "rt-xyz",
        "expires_at_ms" => 1_735_689_600_000,
        "account_id" => "acct-123",
        "email" => "[email protected]"
      }
      store.save(creds)
      expect(store.load).to eq(creds)
    end

    it "returns nil when the file contains invalid JSON" do
      FileUtils.mkdir_p(File.dirname(store_path))
      File.write(store_path, "not valid json{{{{")
      expect(store.load).to be_nil
    end

    it "returns nil when the file is empty" do
      FileUtils.mkdir_p(File.dirname(store_path))
      File.write(store_path, "")
      expect(store.load).to be_nil
    end
  end

  describe "#save" do
    let(:creds) do
      {
        "access_token" => "sk-ant-oat01-test",
        "refresh_token" => "refresh-test",
        "expires_at_ms" => 9_999_999_999_999,
        "account_id" => nil,
        "email" => nil
      }
    end

    it "creates parent directories if they do not exist" do
      nested_path = File.join(tmpdir, "sub", "dir", "claude_oauth.json")
      nested_store = described_class.new(path: nested_path)
      nested_store.save(creds)
      expect(File.exist?(nested_path)).to be(true)
    end

    it "sets file mode to 0600" do
      store.save(creds)
      mode = File.stat(store_path).mode & 0o777
      expect(mode).to eq(0o600)
    end

    it "round-trips the credentials hash" do
      store.save(creds)
      expect(store.load).to eq(creds)
    end

    it "overwrites existing credentials on subsequent saves" do
      store.save(creds)
      new_creds = creds.merge("access_token" => "sk-ant-oat01-new")
      store.save(new_creds)
      expect(store.load["access_token"]).to eq("sk-ant-oat01-new")
    end

    it "does not leave a .tmp file after successful save" do
      store.save(creds)
      expect(File.exist?("#{store_path}.tmp")).to be(false)
    end

    it "concurrent saves from two threads produce a valid file" do
      results = []
      threads = 2.times.map do |i|
        Thread.new do
          store.save(creds.merge("access_token" => "token-#{i}"))
          results << :ok
        rescue StandardError => e
          results << e
        end
      end
      threads.each(&:join)

      expect(results.all? { |r| r == :ok }).to be(true)
      # File should be valid JSON after both writes
      loaded = store.load
      expect(loaded).to be_a(Hash)
      expect(loaded).to have_key("access_token")
    end
  end

  describe "#delete" do
    it "removes the file when it exists" do
      store.save({ "access_token" => "tok" })
      expect(File.exist?(store_path)).to be(true)
      store.delete
      expect(File.exist?(store_path)).to be(false)
    end

    it "does not raise when the file does not exist" do
      expect { store.delete }.not_to raise_error
    end

    it "load returns nil after delete" do
      store.save({ "access_token" => "tok" })
      store.delete
      expect(store.load).to be_nil
    end
  end
end