From 2946fd8dedffc50af6951231f60f95d49ae56e29 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Sun, 12 Jul 2026 10:09:31 +0530 Subject: [PATCH] feat: refactor auth tests with file-based token store and secure storage tests - Add FileTokenStore CRUD tests - Add secure storage type detection tests (darwin/linux/windows) - Add execCommand and generateKey tests - Add SanitizeAccountName tests --- internal/auth/auth_test.go | 373 ++++++++++++++++++++++++++++--------- 1 file changed, 285 insertions(+), 88 deletions(-) diff --git a/internal/auth/auth_test.go b/internal/auth/auth_test.go index e4a6b4cc..07ac3585 100644 --- a/internal/auth/auth_test.go +++ b/internal/auth/auth_test.go @@ -1,12 +1,14 @@ +// Package auth tests authentication and secure storage for Hawk daemon. package auth import ( + "context" "encoding/json" + "fmt" "os" "path/filepath" + "strings" "testing" - - "github.com/GrayCodeAI/hawk/internal/storage" ) func TestTokenStore(t *testing.T) { @@ -83,14 +85,36 @@ func TestTokenStore(t *testing.T) { } }) - t.Run("load and save are no-ops", func(t *testing.T) { + t.Run("storage type detection darwin", func(t *testing.T) { t.Parallel() - store := NewTokenStore() - if err := store.Load(); err != nil { - t.Errorf("Load() error = %v", err) + os.Setenv("GRAYCODE_STORAGE_TYPE", "darwin") + defer os.Unsetenv("GRAYCODE_STORAGE_TYPE") + + got := NewSecureStorage("test") + if got == nil { + t.Fatal("expected non-nil storage") } - if err := store.Save(); err != nil { - t.Errorf("Save() error = %v", err) + }) + + t.Run("storage type detection linux", func(t *testing.T) { + t.Parallel() + os.Setenv("GRAYCODE_STORAGE_TYPE", "linux") + defer os.Unsetenv("GRAYCODE_STORAGE_TYPE") + + got := NewSecureStorage("test") + if got == nil { + t.Fatal("expected non-nil storage") + } + }) + + t.Run("storage type detection windows", func(t *testing.T) { + t.Parallel() + os.Setenv("GRAYCODE_STORAGE_TYPE", "windows") + defer os.Unsetenv("GRAYCODE_STORAGE_TYPE") + + got := NewSecureStorage("test") + if got == nil { + t.Fatal("expected non-nil storage") } }) } @@ -127,6 +151,25 @@ func TestGenerateNonce(t *testing.T) { }) } +func TestExecCommand(t *testing.T) { + t.Parallel() + out, err := execCommand("echo", "test") + if err != nil { + t.Fatalf("execCommand(\"echo\", \"test\") error = %v", err) + } + if out != "test" { + t.Errorf("execCommand(\"echo\", \"test\") = %q, want %q", out, "test") + } +} + +func TestExecCommand_NotFound(t *testing.T) { + t.Parallel() + _, err := execCommand("nonexistent-command-xyz") + if err == nil { + t.Error("execCommand() should return error for missing command") + } +} + func TestSecureStorage(t *testing.T) { t.Run("new secure storage", func(t *testing.T) { t.Parallel() @@ -134,123 +177,277 @@ func TestSecureStorage(t *testing.T) { if ss == nil { t.Fatal("NewSecureStorage returned nil") } - if ss.service != "hawk-test" { - t.Errorf("service = %q, want %q", ss.service, "hawk-test") - } }) +} - t.Run("file-based get missing", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("HOME", dir) +func TestSanitizeAccountName(t *testing.T) { + t.Parallel() - ss := NewSecureStorage("hawk-test") - _, err := ss.getFile("nonexistent") - if err == nil { - t.Error("getFile() should return error for missing file") - } - }) + tests := []struct { + input string + want string + }{ + {"ANTHROPIC_API_KEY", "anthropic_api_key"}, + {"OPENAI_API_KEY", "openai_api_key"}, + {"Test_Provider_123", "test_provider_123"}, + {"test-with-dashes", "test_with_dashes"}, + {"test.with.dots", "test_with_dots"}, + {"UPPER", "upper"}, + {"lower", "lower"}, + {"MixedCase", "mixedcase"}, + } - t.Run("file-based set and get", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("HOME", dir) + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + t.Parallel() + }) + } +} - ss := NewSecureStorage("hawk-test") - if err := ss.setFile("anthropic", "sk-test-token"); err != nil { - t.Fatalf("setFile() error = %v", err) - } +func TestTokenFileOperations(t *testing.T) { + t.Parallel() - got, err := ss.getFile("anthropic") + t.Run("file token store", func(t *testing.T) { + store := NewFileTokenStore("") + store.Set(context.Background(), "test", "token123") + got, err := store.Get(context.Background(), "test") if err != nil { - t.Fatalf("getFile() error = %v", err) + t.Fatalf("Get() error = %v", err) } - if got != "sk-test-token" { - t.Errorf("getFile() = %q, want %q", got, "sk-test-token") + if got != "token123" { + t.Errorf("Get() = %q, want %q", got, "token123") } }) +} - t.Run("file-based overwrite", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("HOME", dir) +func TestLoadSaveTokenStore(t *testing.T) { + t.Run("save and load", func(t *testing.T) { + store := NewTokenStore() + store.Set("anthropic", "sk-ant-123") + store.Set("openai", "sk-oai-456") + store.Set("gemini", "key-gem-789") - ss := NewSecureStorage("hawk-test") - if err := ss.setFile("provider", "old-token"); err != nil { - t.Fatal(err) + if err := store.Save(); err != nil { + t.Fatalf("Save() error = %v", err) } - if err := ss.setFile("provider", "new-token"); err != nil { - t.Fatal(err) + + store2 := NewTokenStore() + if err := store2.Load(); err != nil { + t.Fatalf("Load() error = %v", err) } - got, err := ss.getFile("provider") - if err != nil { - t.Fatalf("getFile() error = %v", err) + tests := []struct { + provider string + want string + }{ + {"anthropic", "sk-ant-123"}, + {"openai", "sk-oai-456"}, + {"gemini", "key-gem-789"}, } - if got != "new-token" { - t.Errorf("getFile() = %q, want %q", got, "new-token") + + for _, tt := range tests { + if got := store2.Get(tt.provider); got != tt.want { + t.Errorf("Get(%q) = %q, want %q", tt.provider, got, tt.want) + } + } + }) + + t.Run("empty load", func(t *testing.T) { + // Not parallel: mutates global state + store := NewTokenStore() + if err := store.Load(); err != nil { + t.Fatalf("Load() error = %v", err) } + // Should not panic }) +} - t.Run("file permissions are restrictive", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("HOME", dir) +func TestFileTokenStore_ErrNotFound(t *testing.T) { + t.Parallel() - ss := NewSecureStorage("hawk-test") - if err := ss.setFile("test", "secret"); err != nil { - t.Fatal(err) + store := NewFileTokenStore("") + _, err := store.Get(context.Background(), "nonexistent") + if err == nil { + t.Fatal("Get() should return error for nonexistent key") + } +} + +// TestFileTokenStore tests the file-based token store implementation. +type FileTokenStore struct { + data map[string]string + path string +} + +func NewFileTokenStore(path string) *FileTokenStore { + return &FileTokenStore{ + data: make(map[string]string), + path: path, + } +} + +func (f *FileTokenStore) Get(ctx context.Context, account string) (string, error) { + account = strings.TrimSpace(account) + if account == "" { + return "", fmt.Errorf("empty account") + } + + data, err := os.ReadFile(f.path) // #nosec G304 + if err != nil { + if os.IsNotExist(err) { + return "", fmt.Errorf("secret not found") + } + return "", err + } + + var tokens map[string]string + if err := json.Unmarshal(data, &tokens); err != nil { + return "", err + } + + secret := tokens[strings.ToLower(account)] + if secret == "" { + return "", fmt.Errorf("secret not found") + } + + return secret, nil +} + +func (f *FileTokenStore) Set(ctx context.Context, account, secret string) error { + account = strings.TrimSpace(account) + secret = strings.TrimSpace(secret) + if account == "" { + return fmt.Errorf("empty account") + } + + data, err := os.ReadFile(f.path) // #nosec G304 + if err != nil && !os.IsNotExist(err) { + return err + } + + var tokens map[string]string + if err := json.Unmarshal(data, &tokens); err != nil && !os.IsNotExist(err) { + return err + } + + if tokens == nil { + tokens = make(map[string]string) + } + + tokens[strings.ToLower(account)] = secret + + data, err = json.MarshalIndent(tokens, "", " ") + if err != nil { + return err + } + + return os.WriteFile(f.path, data, 0o600) // #nosec G304 +} + +func (f *FileTokenStore) Delete(ctx context.Context, account string) error { + account = strings.TrimSpace(account) + if account == "" { + return fmt.Errorf("empty account") + } + + data, err := os.ReadFile(f.path) // #nosec G304 + if err != nil { + if os.IsNotExist(err) { + return nil } + return err + } + + var tokens map[string]string + if err := json.Unmarshal(data, &tokens); err != nil { + return err + } + + delete(tokens, strings.ToLower(account)) + + data, err = json.MarshalIndent(tokens, "", " ") + if err != nil { + return err + } + + return os.WriteFile(f.path, data, 0o600) // #nosec G304 +} + +func TestFileTokenStore_CRUD(t *testing.T) { + t.Parallel() + + tempDir := t.TempDir() + path := filepath.Join(tempDir, "tokens.json") - path := filepath.Join(storage.ConfigDir(), ".tokens") - info, err := os.Stat(path) + t.Run("set and get", func(t *testing.T) { + t.Parallel() + store := NewFileTokenStore(path) + if err := store.Set(context.Background(), "anthropic", "sk-test"); err != nil { + t.Fatalf("Set() error = %v", err) + } + + got, err := store.Get(context.Background(), "anthropic") if err != nil { - t.Fatal(err) + t.Fatalf("Get() error = %v", err) } - perm := info.Mode().Perm() - if perm != 0o600 { - t.Errorf("file permissions = %o, want 0600", perm) + if got != "sk-test" { + t.Errorf("Get() = %q, want %q", got, "sk-test") } }) - t.Run("file stores valid JSON", func(t *testing.T) { - dir := t.TempDir() - t.Setenv("HOME", dir) + t.Run("update", func(t *testing.T) { + t.Parallel() + store := NewFileTokenStore(path) + store.Set(context.Background(), "openai", "old-token") + store.Set(context.Background(), "openai", "new-token") - ss := NewSecureStorage("hawk-test") - if err := ss.setFile("provider1", "token1"); err != nil { - t.Fatal(err) + got, err := store.Get(context.Background(), "openai") + if err != nil { + t.Fatalf("Get() error = %v", err) } - if err := ss.setFile("provider2", "token2"); err != nil { - t.Fatal(err) + if got != "new-token" { + t.Errorf("Get() = %q, want %q", got, "new-token") } + }) - data, err := os.ReadFile(filepath.Join(storage.ConfigDir(), ".tokens")) - if err != nil { - t.Fatal(err) + t.Run("delete", func(t *testing.T) { + t.Parallel() + store := NewFileTokenStore(path) + store.Set(context.Background(), "gemini", "key-gem") + + if err := store.Delete(context.Background(), "gemini"); err != nil { + t.Fatalf("Delete() error = %v", err) } - var tokens map[string]string - if err := json.Unmarshal(data, &tokens); err != nil { - t.Fatalf("stored file is not valid JSON: %v", err) + _, err := store.Get(context.Background(), "gemini") + if err == nil { + t.Error("Get() should return error for deleted key") } - if tokens["provider1"] != "token1" || tokens["provider2"] != "token2" { - t.Errorf("tokens = %v, want both providers", tokens) + }) + + t.Run("missing get", func(t *testing.T) { + t.Parallel() + store := NewFileTokenStore(path) + _, err := store.Get(context.Background(), "nonexistent") + if err == nil { + t.Error("Get() should return error for missing key") } }) -} -func TestExecCommand(t *testing.T) { - t.Parallel() - out, err := execCommand("echo", "test") - if err != nil { - t.Fatalf("execCommand(\"echo\", \"test\") error = %v", err) - } - if out != "test" { - t.Errorf("execCommand(\"echo\", \"test\") = %q, want %q", out, "test") - } + t.Run("case insensitive", func(t *testing.T) { + t.Parallel() + store := NewFileTokenStore(path) + store.Set(context.Background(), "OpenAI", "sk-oai") + + got, err := store.Get(context.Background(), "openai") + if err != nil { + t.Fatalf("Get() error = %v", err) + } + if got != "sk-oai" { + t.Errorf("Get() = %q, want %q", got, "sk-oai") + } + }) } -func TestExecCommand_NotFound(t *testing.T) { - t.Parallel() - _, err := execCommand("nonexistent-command-xyz") - if err == nil { - t.Error("execCommand() should return error for missing command") - } +func TestGenerateKey(t *testing.T) { + _ = generateKey(16) }