This commit is contained in:
2025-08-27 08:32:14 -07:00
parent 970c41a4cb
commit f24d21033a
6 changed files with 373 additions and 91 deletions

View File

@@ -4,82 +4,53 @@ import (
"os"
"path/filepath"
"testing"
"time"
"github.com/dghubble/oauth1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFileStorage(t *testing.T) {
// Create temp directory for tests
tempDir, err := os.MkdirTemp("", "garmin-test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
// Setup
tempDir := t.TempDir()
storage := NewFileStorage()
storage.Path = filepath.Join(tempDir, "token.json")
storage := &FileStorage{
Path: filepath.Join(tempDir, "token.json"),
}
// Test saving and loading token
t.Run("SaveAndLoadToken", func(t *testing.T) {
testToken := &oauth1.Token{
Token: "access-token",
TokenSecret: "access-secret",
t.Run("SaveToken and GetToken", func(t *testing.T) {
token := &oauth1.Token{
Token: "test_token",
TokenSecret: "test_secret",
}
// Save token
err := storage.SaveToken(testToken)
assert.NoError(t, err)
err := storage.SaveToken(token)
require.NoError(t, err)
// Load token
loadedToken, err := storage.GetToken()
assert.NoError(t, err)
assert.Equal(t, testToken.Token, loadedToken.Token)
assert.Equal(t, testToken.TokenSecret, loadedToken.TokenSecret)
// Get token
retrievedToken, err := storage.GetToken()
require.NoError(t, err)
// Verify
assert.Equal(t, token.Token, retrievedToken.Token)
assert.Equal(t, token.TokenSecret, retrievedToken.TokenSecret)
})
// Test missing token file
t.Run("TokenMissing", func(t *testing.T) {
t.Run("EmptyToken", func(t *testing.T) {
token := &oauth1.Token{
Token: "",
TokenSecret: "",
}
err := storage.SaveToken(token)
require.NoError(t, err)
_, err = storage.GetToken()
require.ErrorIs(t, err, os.ErrNotExist)
})
t.Run("NonExistentFile", func(t *testing.T) {
storage.Path = filepath.Join(tempDir, "nonexistent.json")
_, err := storage.GetToken()
assert.ErrorIs(t, err, os.ErrNotExist)
})
// Test token expiration
t.Run("TokenExpiration", func(t *testing.T) {
testCases := []struct {
name string
token *oauth1.Token
expected bool
}{
{
name: "EmptyToken",
token: &oauth1.Token{},
expected: true,
},
{
name: "ValidToken",
token: &oauth1.Token{
Token: "valid",
TokenSecret: "valid",
},
expected: false,
},
{
name: "ExpiredToken",
token: &oauth1.Token{
Token: "expired",
TokenSecret: "expired",
CreatedAt: time.Now().Add(-200 * 24 * time.Hour), // 200 days ago
},
expected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expired := storage.TokenExpired(tc.token)
assert.Equal(t, tc.expected, expired)
})
}
require.ErrorIs(t, err, os.ErrNotExist)
})
}

81
internal/auth/mfastate.go Normal file
View File

@@ -0,0 +1,81 @@
package auth
import (
"encoding/json"
"os"
"path/filepath"
"time"
"sync"
)
// MFAState represents the state of an MFA verification session
type MFAState struct {
VerificationURL string `json:"verification_url"`
SessionToken string `json:"session_token"`
MFACode string `json:"mfa_code"`
ExpiresAt time.Time `json:"expires_at"`
}
// MFAStorage handles persistence of MFA state
type MFAStorage interface {
Store(state MFAState) error
Get() (MFAState, error)
Clear() error
}
// FileMFAStorage implements MFAStorage using a JSON file
type FileMFAStorage struct {
filePath string
mutex sync.RWMutex
}
// NewFileMFAStorage creates a new file-based MFA storage
func NewFileMFAStorage() *FileMFAStorage {
home, _ := os.UserHomeDir()
return &FileMFAStorage{
filePath: filepath.Join(home, ".garminconnect", "mfa_state.json"),
}
}
// Store saves MFA state to file
func (s *FileMFAStorage) Store(state MFAState) error {
s.mutex.Lock()
defer s.mutex.Unlock()
// Create directory if needed
dir := filepath.Dir(s.filePath)
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return err
}
return os.WriteFile(s.filePath, data, 0600)
}
// Get retrieves MFA state from file
func (s *FileMFAStorage) Get() (MFAState, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
data, err := os.ReadFile(s.filePath)
if err != nil {
if os.IsNotExist(err) {
return MFAState{}, nil
}
return MFAState{}, err
}
var state MFAState
err = json.Unmarshal(data, &state)
return state, err
}
// Clear removes the MFA state file
func (s *FileMFAStorage) Clear() error {
s.mutex.Lock()
defer s.mutex.Unlock()
return os.Remove(s.filePath)
}