Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions internal/keyring/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package keyring

// Provider defines the interface for token storage
type Provider interface {
// Set stores a token for the given service and user
Set(service, user, token string) error
// Get retrieves a token for the given service and user
Get(service, user string) (string, error)
}

// provider is the platform-specific implementation
var provider Provider

// Set stores a token using the platform-specific provider
func Set(service, user, token string) error {
return provider.Set(service, user, token)
}

// Get retrieves a token using the platform-specific provider
func Get(service, user string) (string, error) {
return provider.Get(service, user)
}
24 changes: 24 additions & 0 deletions internal/keyring/keyring_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build darwin

package keyring

import (
"github.com/zalando/go-keyring"
)

func init() {
provider = &systemKeyringProvider{}
}

// systemKeyringProvider implements token storage using the system keyring
type systemKeyringProvider struct{}

// Set stores a token in the system keyring
func (s *systemKeyringProvider) Set(service, user, token string) error {
return keyring.Set(service, user, token)
}

// Get retrieves a token from the system keyring
func (s *systemKeyringProvider) Get(service, user string) (string, error) {
return keyring.Get(service, user)
}
96 changes: 96 additions & 0 deletions internal/keyring/keyring_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//go:build linux

package keyring

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)

const tokenFile = "token"

func init() {
provider = &fileProvider{}
}

// fileProvider implements token storage using files
type fileProvider struct{}

// Set stores a token in a file
func (f *fileProvider) Set(service, user, token string) error {
tokenPath, err := getTokenFilePath()
if err != nil {
return err
}

// Create config directory if it doesn't exist
configDirPath := filepath.Dir(tokenPath)
if err := os.MkdirAll(configDirPath, 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}

// Store as a simple key-value map
tokens := make(map[string]string)

// Try to load existing tokens
if data, err := os.ReadFile(tokenPath); err == nil {
_ = json.Unmarshal(data, &tokens)
}

// Add or update token for this user
tokens[user] = token

// Save to file
data, err := json.MarshalIndent(tokens, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal tokens: %w", err)
}

// Write with 0600 permissions (only owner can read/write)
if err := os.WriteFile(tokenPath, data, 0600); err != nil {
return fmt.Errorf("failed to write token file: %w", err)
}

return nil
}

// Get retrieves a token from a file
func (f *fileProvider) Get(service, user string) (string, error) {
tokenPath, err := getTokenFilePath()
if err != nil {
return "", err
}

data, err := os.ReadFile(tokenPath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("token not found")
}
return "", fmt.Errorf("failed to read token file: %w", err)
}

tokens := make(map[string]string)
if err := json.Unmarshal(data, &tokens); err != nil {
return "", fmt.Errorf("failed to parse token file: %w", err)
}

token, ok := tokens[user]
if !ok {
return "", fmt.Errorf("token not found for user: %s", user)
}

return token, nil
}

// getTokenFilePath returns the path to the token file
func getTokenFilePath() (string, error) {
configDirPath, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("failed to get config directory: %w", err)
}

tokenPath := filepath.Join(configDirPath, "slack-cli", tokenFile)
return tokenPath, nil
}
153 changes: 153 additions & 0 deletions internal/keyring/keyring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package keyring

import (
"os"
"path/filepath"
"testing"
)

// TestSetGet tests basic set and get operations
func TestSetGet(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()

// Override the config directory
origConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() {
if origConfigDir != "" {
os.Setenv("XDG_CONFIG_HOME", origConfigDir)
} else {
os.Unsetenv("XDG_CONFIG_HOME")
}
}()

testService := "test-service"
testUser := "test-user"
testToken := "test-token-12345"

// Test Set
err := Set(testService, testUser, testToken)
if err != nil {
t.Fatalf("Failed to set token: %v", err)
}

// Test Get
retrievedToken, err := Get(testService, testUser)
if err != nil {
t.Fatalf("Failed to get token: %v", err)
}

if retrievedToken != testToken {
t.Errorf("Expected token %q, got %q", testToken, retrievedToken)
}
}

// TestMultipleUsers tests storing tokens for multiple users
func TestMultipleUsers(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()

// Override the config directory
origConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() {
if origConfigDir != "" {
os.Setenv("XDG_CONFIG_HOME", origConfigDir)
} else {
os.Unsetenv("XDG_CONFIG_HOME")
}
}()

testService := "test-service"
users := map[string]string{
"user1": "token1",
"user2": "token2",
"user3": "token3",
}

// Set all tokens
for user, token := range users {
err := Set(testService, user, token)
if err != nil {
t.Fatalf("Failed to set token for %s: %v", user, err)
}
}

// Get and verify all tokens
for user, expectedToken := range users {
retrievedToken, err := Get(testService, user)
if err != nil {
t.Fatalf("Failed to get token for %s: %v", user, err)
}
if retrievedToken != expectedToken {
t.Errorf("For user %s, expected token %q, got %q", user, expectedToken, retrievedToken)
}
}
}

// TestGetNotFound tests error handling when token doesn't exist
func TestGetNotFound(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()

// Override the config directory
origConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() {
if origConfigDir != "" {
os.Setenv("XDG_CONFIG_HOME", origConfigDir)
} else {
os.Unsetenv("XDG_CONFIG_HOME")
}
}()

// Try to get from non-existent file
_, err := Get("test-service", "nonexistent-user")
if err == nil {
t.Error("Expected error when getting non-existent token, got nil")
}
}

// TestFilePermissions tests that token file has correct permissions (Linux only)
func TestFilePermissions(t *testing.T) {
// Skip on non-Linux platforms since file implementation is Linux-specific
if provider == nil {
t.Skip("Skipping file permissions test on non-Linux platform")
}

// Check if provider is fileProvider (Linux)
if _, ok := provider.(*fileProvider); !ok {
t.Skip("Skipping file permissions test on non-Linux platform")
}

tmpDir := t.TempDir()

origConfigDir := os.Getenv("XDG_CONFIG_HOME")
os.Setenv("XDG_CONFIG_HOME", tmpDir)
defer func() {
if origConfigDir != "" {
os.Setenv("XDG_CONFIG_HOME", origConfigDir)
} else {
os.Unsetenv("XDG_CONFIG_HOME")
}
}()

// Set a token
err := Set("test-service", "test-user", "test-token")
if err != nil {
t.Fatalf("Failed to set token: %v", err)
}

// Check file permissions
tokenPath := filepath.Join(tmpDir, "slack-cli", "token")
info, err := os.Stat(tokenPath)
if err != nil {
t.Fatalf("Failed to stat token file: %v", err)
}

expectedPerm := os.FileMode(0600)
if info.Mode().Perm() != expectedPerm {
t.Errorf("Expected file permissions %v, got %v", expectedPerm, info.Mode().Perm())
}
}
24 changes: 24 additions & 0 deletions internal/keyring/keyring_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//go:build windows

package keyring

import (
"github.com/zalando/go-keyring"
)

func init() {
provider = &systemKeyringProvider{}
}

// systemKeyringProvider implements token storage using the system keyring
type systemKeyringProvider struct{}

// Set stores a token in the system keyring
func (s *systemKeyringProvider) Set(service, user, token string) error {
return keyring.Set(service, user, token)
}

// Get retrieves a token from the system keyring
func (s *systemKeyringProvider) Get(service, user string) (string, error) {
return keyring.Get(service, user)
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
"strings"
"syscall"

"github.com/kitproj/slack-cli/internal/keyring"
"github.com/slack-go/slack"
"github.com/zalando/go-keyring"
"golang.org/x/term"
)

Expand Down