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
|