mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-14 03:30:26 +00:00
febdc530e1
* feat(auth): add Copilot login Add experimental GitHub Copilot device login and copilot/* provider support for users with Copilot access but no OpenAI account. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(copilot): use responses for GPT-5 Route Copilot GPT-5 models through the Responses API because gpt-5.5 is not available on /chat/completions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(copilot): honor device flow timing * docs(copilot): add auth helper docstrings * fix(auth): address copilot review feedback --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
352 lines
9.2 KiB
Go
352 lines
9.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCredentialManager(t *testing.T) {
|
|
// Create a temporary directory for testing
|
|
tempDir, err := os.MkdirTemp("", "kit-auth-test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
// Create a credential manager with a test path
|
|
cm := &CredentialManager{
|
|
credentialsPath: filepath.Join(tempDir, "credentials.json"),
|
|
}
|
|
|
|
// Test initial state - no credentials
|
|
hasAuth, err := cm.HasAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasAnthropicCredentials failed: %v", err)
|
|
}
|
|
if hasAuth {
|
|
t.Error("Expected no credentials initially")
|
|
}
|
|
|
|
// Test setting credentials
|
|
testAPIKey := "sk-ant-test-key-12345678901234567890"
|
|
err = cm.SetAnthropicCredentials(testAPIKey)
|
|
if err != nil {
|
|
t.Fatalf("SetAnthropicCredentials failed: %v", err)
|
|
}
|
|
|
|
// Test that credentials are now present
|
|
hasAuth, err = cm.HasAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasAnthropicCredentials failed: %v", err)
|
|
}
|
|
if !hasAuth {
|
|
t.Error("Expected credentials to be present")
|
|
}
|
|
|
|
// Test retrieving credentials
|
|
creds, err := cm.GetAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("GetAnthropicCredentials failed: %v", err)
|
|
}
|
|
if creds == nil {
|
|
t.Fatal("Expected credentials to be returned")
|
|
return
|
|
}
|
|
if creds.APIKey != testAPIKey {
|
|
t.Errorf("Expected API key %s, got %s", testAPIKey, creds.APIKey)
|
|
}
|
|
if creds.CreatedAt.IsZero() {
|
|
t.Error("Expected CreatedAt to be set")
|
|
}
|
|
|
|
// Test removing credentials
|
|
err = cm.RemoveAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("RemoveAnthropicCredentials failed: %v", err)
|
|
}
|
|
|
|
// Test that credentials are gone
|
|
hasAuth, err = cm.HasAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasAnthropicCredentials failed: %v", err)
|
|
}
|
|
if hasAuth {
|
|
t.Error("Expected no credentials after removal")
|
|
}
|
|
|
|
// Test that file is removed when empty
|
|
if _, err := os.Stat(cm.credentialsPath); !os.IsNotExist(err) {
|
|
t.Error("Expected credentials file to be removed when empty")
|
|
}
|
|
}
|
|
|
|
func TestValidateAnthropicAPIKey(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
apiKey string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid key",
|
|
apiKey: "sk-ant-test-key-12345678901234567890",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty key",
|
|
apiKey: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong prefix",
|
|
apiKey: "sk-test-key-12345678901234567890",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "too short",
|
|
apiKey: "sk-ant-short",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "with whitespace",
|
|
apiKey: " sk-ant-test-key-12345678901234567890 ",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateAnthropicAPIKey(tt.apiKey)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateAnthropicAPIKey() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAnthropicAPIKey(t *testing.T) {
|
|
// Create a temporary directory for testing
|
|
tempDir, err := os.MkdirTemp("", "kit-auth-test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
// Save original environment
|
|
originalAPIKey := os.Getenv("ANTHROPIC_API_KEY")
|
|
originalXDGConfig := os.Getenv("XDG_CONFIG_HOME")
|
|
defer func() {
|
|
_ = os.Setenv("ANTHROPIC_API_KEY", originalAPIKey)
|
|
_ = os.Setenv("XDG_CONFIG_HOME", originalXDGConfig)
|
|
}()
|
|
|
|
// Set up test environment
|
|
_ = os.Setenv("XDG_CONFIG_HOME", tempDir)
|
|
_ = os.Unsetenv("ANTHROPIC_API_KEY")
|
|
|
|
// Test 1: Flag value takes precedence
|
|
flagKey := "sk-ant-flag-key-12345678901234567890"
|
|
apiKey, source, err := GetAnthropicAPIKey(flagKey)
|
|
if err != nil {
|
|
t.Fatalf("GetAnthropicAPIKey failed: %v", err)
|
|
}
|
|
if apiKey != flagKey {
|
|
t.Errorf("Expected flag key %s, got %s", flagKey, apiKey)
|
|
}
|
|
if source != "command-line flag" {
|
|
t.Errorf("Expected source 'command-line flag', got %s", source)
|
|
}
|
|
|
|
// Test 2: Stored credentials when no flag
|
|
cm, err := NewCredentialManager()
|
|
if err != nil {
|
|
t.Fatalf("NewCredentialManager failed: %v", err)
|
|
}
|
|
|
|
storedKey := "sk-ant-stored-key-12345678901234567890"
|
|
err = cm.SetAnthropicCredentials(storedKey)
|
|
if err != nil {
|
|
t.Fatalf("SetAnthropicCredentials failed: %v", err)
|
|
}
|
|
|
|
apiKey, source, err = GetAnthropicAPIKey("")
|
|
if err != nil {
|
|
t.Fatalf("GetAnthropicAPIKey failed: %v", err)
|
|
}
|
|
if apiKey != storedKey {
|
|
t.Errorf("Expected stored key %s, got %s", storedKey, apiKey)
|
|
}
|
|
if source != "stored API key" {
|
|
t.Errorf("Expected source 'stored API key', got %s", source)
|
|
}
|
|
|
|
// Test 3: Environment variable when no flag or stored credentials
|
|
err = cm.RemoveAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("RemoveAnthropicCredentials failed: %v", err)
|
|
}
|
|
|
|
envKey := "sk-ant-env-key-12345678901234567890"
|
|
_ = os.Setenv("ANTHROPIC_API_KEY", envKey)
|
|
|
|
apiKey, source, err = GetAnthropicAPIKey("")
|
|
if err != nil {
|
|
t.Fatalf("GetAnthropicAPIKey failed: %v", err)
|
|
}
|
|
if apiKey != envKey {
|
|
t.Errorf("Expected env key %s, got %s", envKey, apiKey)
|
|
}
|
|
if source != "ANTHROPIC_API_KEY environment variable" {
|
|
t.Errorf("Expected source 'ANTHROPIC_API_KEY environment variable', got %s", source)
|
|
}
|
|
|
|
// Test 4: No credentials available
|
|
_ = os.Unsetenv("ANTHROPIC_API_KEY")
|
|
|
|
_, _, err = GetAnthropicAPIKey("")
|
|
if err == nil {
|
|
t.Error("Expected error when no credentials available")
|
|
}
|
|
}
|
|
|
|
func TestCredentialStorePersistence(t *testing.T) {
|
|
// Create a temporary directory for testing
|
|
tempDir, err := os.MkdirTemp("", "kit-auth-test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
credentialsPath := filepath.Join(tempDir, "credentials.json")
|
|
|
|
// Create first manager and store credentials
|
|
cm1 := &CredentialManager{credentialsPath: credentialsPath}
|
|
testAPIKey := "sk-ant-test-key-12345678901234567890"
|
|
|
|
err = cm1.SetAnthropicCredentials(testAPIKey)
|
|
if err != nil {
|
|
t.Fatalf("SetAnthropicCredentials failed: %v", err)
|
|
}
|
|
|
|
// Create second manager and verify credentials persist
|
|
cm2 := &CredentialManager{credentialsPath: credentialsPath}
|
|
|
|
creds, err := cm2.GetAnthropicCredentials()
|
|
if err != nil {
|
|
t.Fatalf("GetAnthropicCredentials failed: %v", err)
|
|
}
|
|
if creds == nil {
|
|
t.Fatal("Expected credentials to persist")
|
|
return
|
|
}
|
|
if creds.APIKey != testAPIKey {
|
|
t.Errorf("Expected API key %s, got %s", testAPIKey, creds.APIKey)
|
|
}
|
|
|
|
// Verify file permissions
|
|
info, err := os.Stat(credentialsPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to stat credentials file: %v", err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Errorf("Expected file permissions 0600, got %v", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestCopilotCredentials(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "kit-auth-test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
cm := &CredentialManager{
|
|
credentialsPath: filepath.Join(tempDir, "credentials.json"),
|
|
}
|
|
|
|
creds := &CopilotCredentials{
|
|
Type: "oauth",
|
|
GitHubToken: "github-token",
|
|
CopilotAccessToken: "copilot-token",
|
|
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
|
|
if err := cm.SetCopilotOAuthCredentials(creds); err != nil {
|
|
t.Fatalf("SetCopilotOAuthCredentials failed: %v", err)
|
|
}
|
|
|
|
hasAuth, err := cm.HasCopilotCredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasCopilotCredentials failed: %v", err)
|
|
}
|
|
if !hasAuth {
|
|
t.Fatal("Expected Copilot credentials")
|
|
}
|
|
|
|
token, err := cm.GetValidCopilotAccessToken()
|
|
if err != nil {
|
|
t.Fatalf("GetValidCopilotAccessToken failed: %v", err)
|
|
}
|
|
if token != creds.CopilotAccessToken {
|
|
t.Fatalf("Expected Copilot token %q, got %q", creds.CopilotAccessToken, token)
|
|
}
|
|
|
|
if err := cm.RemoveCopilotCredentials(); err != nil {
|
|
t.Fatalf("RemoveCopilotCredentials failed: %v", err)
|
|
}
|
|
hasAuth, err = cm.HasCopilotCredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasCopilotCredentials after removal failed: %v", err)
|
|
}
|
|
if hasAuth {
|
|
t.Fatal("Expected no Copilot credentials after removal")
|
|
}
|
|
}
|
|
|
|
func TestRemoveCredentialsPreservesOtherProviders(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "kit-auth-test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
cm := &CredentialManager{
|
|
credentialsPath: filepath.Join(tempDir, "credentials.json"),
|
|
}
|
|
|
|
if err := cm.SetOpenAIOAuthCredentials(&OpenAICredentials{
|
|
Type: "oauth",
|
|
AccessToken: "openai-token",
|
|
RefreshToken: "refresh-token",
|
|
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
AccountID: "account",
|
|
CreatedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatalf("SetOpenAIOAuthCredentials failed: %v", err)
|
|
}
|
|
if err := cm.SetCopilotOAuthCredentials(&CopilotCredentials{
|
|
Type: "oauth",
|
|
GitHubToken: "github-token",
|
|
CopilotAccessToken: "copilot-token",
|
|
ExpiresAt: time.Now().Add(time.Hour).Unix(),
|
|
CreatedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatalf("SetCopilotOAuthCredentials failed: %v", err)
|
|
}
|
|
|
|
if err := cm.RemoveCopilotCredentials(); err != nil {
|
|
t.Fatalf("RemoveCopilotCredentials failed: %v", err)
|
|
}
|
|
|
|
hasOpenAI, err := cm.HasOpenAICredentials()
|
|
if err != nil {
|
|
t.Fatalf("HasOpenAICredentials failed: %v", err)
|
|
}
|
|
if !hasOpenAI {
|
|
t.Fatal("Expected OpenAI credentials to remain after removing Copilot credentials")
|
|
}
|
|
}
|