# 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" => "user@example.com" } 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