diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 4b897cc..0000000 --- a/TODO.md +++ /dev/null @@ -1,60 +0,0 @@ -# Go-GarminConnect Porting Project - Remaining Tasks - -## Endpoint Implementation -### Health Data Endpoints -- [ ] Body composition API endpoint -- [ ] Sleep data retrieval and parsing -- [ ] Heart rate/HRV/RHR data endpoint -- [ ] Stress data API implementation -- [ ] Body battery endpoint - -### User Data Endpoints -- [ ] User summary endpoint -- [ ] Daily statistics API -- [ ] Goals/badges endpoint implementation -- [ ] Hydration data endpoint -- [ ] Respiration data API - -### Activity Endpoints -- [ ] Activity type filtering -- [ ] Activity comment functionality -- [ ] Activity like/unlike feature -- [ ] Activity sharing options - -## FIT File Handling -- [ ] Complete weight composition encoding -- [ ] Implement all-day stress FIT encoding -- [ ] Add HRV data to FIT export -- [ ] Validate FIT compatibility with Garmin devices -- [ ] Optimize FIT file parsing performance - -## Testing & Quality Assurance -- [ ] Implement table-driven tests for all endpoints -- [ ] Create mock server for isolated testing -- [ ] Add golden file tests for FIT validation -- [ ] Complete performance benchmarks -- [ ] Integrate static analysis (golangci-lint) -- [ ] Implement code coverage reporting -- [ ] Add stress/load testing scenarios - -## Documentation & Examples -- [ ] Complete GoDoc coverage for all packages -- [ ] Create usage examples for all API endpoints -- [ ] Build CLI demonstration application -- [ ] Port Python examples to Go equivalents -- [ ] Update README with comprehensive documentation -- [ ] Create migration guide from Python library - -## Infrastructure & Optimization -- [ ] Implement connection pooling -- [ ] Complete rate limiting mechanism -- [ ] Optimize session management -- [ ] Add automatic token refresh tests -- [ ] Implement response caching -- [ ] Add circuit breaker pattern for API calls - -## Project Management -- [ ] Prioritize health data endpoints (critical path) -- [ ] Create GitHub project board for tracking -- [ ] Set up milestone tracking -- [ ] Assign priority labels (P0, P1, P2) diff --git a/cmd/garmin-cli/main.go b/cmd/garmin-cli/main.go index 426fddb..807667c 100644 --- a/cmd/garmin-cli/main.go +++ b/cmd/garmin-cli/main.go @@ -10,11 +10,28 @@ import ( "time" "github.com/joho/godotenv" + "github.com/spf13/cobra" "github.com/sstent/go-garminconnect/internal/api" "github.com/sstent/go-garminconnect/internal/auth/garth" ) -func main() { +var rootCmd = &cobra.Command{ + Use: "garmin-cli", + Short: "CLI for interacting with Garmin Connect API", +} + +var authCmd = &cobra.Command{ + Use: "auth", + Short: "Authentication commands", +} + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Authenticate with Garmin Connect", + Run: loginHandler, +} + +func loginHandler(cmd *cobra.Command, args []string) { // Try to load from .env if environment variables not set if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" { if err := godotenv.Load(); err != nil { @@ -91,6 +108,18 @@ func main() { } } +func main() { + // Setup command structure + authCmd.AddCommand(loginCmd) + rootCmd.AddCommand(authCmd) + + // Execute CLI + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + // getCredentials prompts for username and password func getCredentials() (string, string) { reader := bufio.NewReader(os.Stdin) diff --git a/garminconnect.md b/garminconnect.md new file mode 100644 index 0000000..81995ef --- /dev/null +++ b/garminconnect.md @@ -0,0 +1,82 @@ +# Go-GarminConnect Porting Project - Progress and Roadmap + +## Current Progress + +### Authentication System: +- [x] OAuth1/OAuth2 token flow implemented +- [x] Token auto-refresh mechanism +- [x] MFA handling with console prompts +- [x] Session persistence to JSON file +- [x] Comprehensive authentication tests + +### CLI Implementation: +- [x] Command structure with Cobra +- [x] Basic login command skeleton +- [ ] Session save/restore integration +- [ ] Status command implementation +- [ ] Logout functionality + +### API Implementation: +- [ ] Sleep data retrieval +- [ ] Stress tracking API +- [ ] Body composition models + +## Next Steps (Priority Order) + +```mermaid +graph TD + A[Next Phase] --> B[CLI Completion] + A --> C[API Enhancement] + A --> D[FIT File Support] + + B --> B1[Login Command] + B --> B2[Status Command] + B --> B3[Logout Command] + + C --> C1[Retry Logic] + C --> C2[Structured Logging] + C --> C3[Sleep/Stress Endpoints] + + D --> D1[FIT Encoding] + D --> D2[Validation] +``` + +### 1. CLI Completion (2 days) +- Complete login command with session handling +- Implement status command to verify authentication +- Add logout functionality with session cleanup + +### 2. API Enhancement (3 days) +- Implement retry logic with exponential backoff +- Add structured request/response logging +- Develop sleep/stress endpoints +- Create body composition models + +### 3. FIT File Support (2 days) +- Complete weight composition encoding +- Implement all-day stress FIT encoding +- Add HRV data to FIT export +- Validate FIT compatibility + +## Testing & Quality Assurance +- [ ] CLI end-to-end tests +- [ ] API integration tests +- [ ] FIT validation tests +- [ ] Performance benchmarks + +## Documentation & Examples +- [ ] CLI usage guide +- [ ] API reference documentation +- [ ] FIT file specification + +## Quality Gates + +### Before Release: +- [ ] 100% test coverage for core features +- [ ] Security audit of authentication flow +- [ ] Performance benchmarks met + +## Success Metrics +- [ ] Authentication success rate > 99% +- [ ] API response time < 1s +- [ ] FIT file compatibility 100% diff --git a/garth.md b/garth.md index 2633df9..417b02a 100644 --- a/garth.md +++ b/garth.md @@ -1,4 +1,3 @@ - # Garth Go Port Plan - Test-Driven Development Implementation ## Project Overview @@ -12,13 +11,16 @@ Based on the original Garth library, the main components are: - **API Client**: HTTP client for Garmin Connect API requests - **Data Models**: Structured data types for health/fitness metrics - **Session Management**: Token persistence and restoration +- **CLI Interface**: Command-line authentication and session management ## 1. Project Structure ``` garth-go/ ├── cmd/ -│ └── garth/ # CLI tool (like Python's uvx garth login) +│ └── garth/ # CLI tool +│ ├── auth.go # Authentication commands +│ ├── commands.go # Root CLI commands │ └── main.go ├── pkg/ │ ├── auth/ # Authentication module @@ -27,378 +29,91 @@ garth-go/ │ │ ├── session.go │ │ └── session_test.go │ ├── client/ # HTTP client module -│ │ ├── client.go -│ │ ├── client_test.go -│ │ ├── endpoints.go -│ │ └── endpoints_test.go │ ├── models/ # Data structures -│ │ ├── sleep.go -│ │ ├── sleep_test.go -│ │ ├── stress.go -│ │ ├── stress_test.go -│ │ ├── steps.go -│ │ ├── weight.go -│ │ ├── hrv.go -│ │ └── user.go │ └── garth/ # Main package interface -│ ├── garth.go -│ └── garth_test.go ├── internal/ │ ├── testutil/ # Test utilities -│ │ ├── fixtures.go -│ │ └── mock_server.go │ └── config/ # Internal configuration -│ └── constants.go -├── examples/ # Usage examples -│ ├── basic/ -│ ├── sleep_analysis/ -│ └── stress_tracking/ +├── examples/ ├── go.mod ├── go.sum -├── README.md -├── Makefile -└── .github/ - └── workflows/ - └── ci.yml +└── README.md ``` -## 2. Data Flow Architecture +## 2. Current Progress -### Authentication Flow -``` -User Credentials → OAuth1 Token → OAuth2 Token → API Requests - ↓ ↓ - Persisted Auto-refresh -``` +### Authentication System: +- [x] OAuth1/OAuth2 token flow implemented +- [x] Token auto-refresh mechanism +- [x] MFA handling with console prompts +- [x] Session persistence to JSON file +- [x] Comprehensive authentication tests -### API Request Flow -``` -Client Request → Token Validation → HTTP Request → JSON Response → Struct Unmarshaling - ↓ - Auto-refresh if expired -``` +### CLI Implementation: +- [x] Command structure with Cobra +- [x] Basic login command skeleton +- [ ] Session save/restore integration +- [ ] Status command implementation +- [ ] Logout functionality -### Data Processing Flow -``` -Raw API Response → JSON Unmarshaling → Data Validation → Business Logic → Client Response -``` +## 3. Next Steps (Priority Order) -## 3. Recommended Go Modules - -### Core Dependencies -```go -// HTTP client and utilities -"net/http" -"context" -"time" - -// JSON handling -"encoding/json" - -// OAuth implementation -"golang.org/x/oauth2" // For OAuth2 flows - -// HTTP client with advanced features -"github.com/go-resty/resty/v2" // Alternative to net/http with better ergonomics - -// Configuration and environment -"github.com/spf13/viper" // Configuration management -"github.com/spf13/cobra" // CLI framework - -// Validation -"github.com/go-playground/validator/v10" // Struct validation - -// Logging -"go.uber.org/zap" // Structured logging - -// Testing -"github.com/stretchr/testify" // Testing utilities -"github.com/jarcoal/httpmock" // HTTP mocking -``` - -### Development Dependencies -```go -// Code generation -"github.com/golang/mock/gomock" // Mock generation - -// Linting and quality -"github.com/golangci/golangci-lint" -``` - -## 4. TDD Implementation Plan - -### Phase 1: Authentication Module (Week 1-2) - -#### Test Cases to Implement First: - -**OAuth Session Tests:** -```go -func TestSessionSave(t *testing.T) -func TestSessionLoad(t *testing.T) -func TestSessionValidation(t *testing.T) -func TestSessionExpiry(t *testing.T) -``` - -**OAuth Flow Tests:** -```go -func TestOAuth1Login(t *testing.T) -func TestOAuth2TokenRefresh(t *testing.T) -func TestMFAHandling(t *testing.T) -func TestLoginFailure(t *testing.T) -``` - -#### Implementation Order: -1. **Write failing tests** for session management -2. **Implement** basic session struct and methods -3. **Write failing tests** for OAuth1 authentication -4. **Implement** OAuth1 flow -5. **Write failing tests** for OAuth2 token refresh -6. **Implement** OAuth2 auto-refresh mechanism -7. **Write failing tests** for MFA handling -8. **Implement** MFA prompt system - -### Phase 2: HTTP Client Module (Week 3) - -#### Test Cases: -```go -func TestClientCreation(t *testing.T) -func TestAPIRequest(t *testing.T) -func TestAuthenticationHeaders(t *testing.T) -func TestErrorHandling(t *testing.T) -func TestRetryLogic(t *testing.T) -``` - -#### Mock Server Setup: -```go -// Create mock Garmin Connect API responses -func setupMockGarminServer() *httptest.Server -func mockSuccessResponse() string -func mockErrorResponse() string -``` - -### Phase 3: Data Models (Week 4-5) - -#### Core Models Implementation Order: - -**1. User Profile:** -```go -type UserProfile struct { - ID int `json:"id" validate:"required"` - ProfileID int `json:"profileId" validate:"required"` - DisplayName string `json:"displayName"` - FullName string `json:"fullName"` - // ... other fields -} -``` - -**2. Sleep Data:** -```go -type SleepData struct { - CalendarDate time.Time `json:"calendarDate"` - SleepTimeSeconds int `json:"sleepTimeSeconds"` - DeepSleep int `json:"deepSleepSeconds"` - LightSleep int `json:"lightSleepSeconds"` - // ... other fields -} -``` - -**3. Stress Data:** -```go -type DailyStress struct { - CalendarDate time.Time `json:"calendarDate"` - OverallStressLevel int `json:"overallStressLevel"` - RestStressDuration int `json:"restStressDuration"` - // ... other fields -} -``` - -#### Test Implementation Strategy: -1. **JSON Unmarshaling Tests** - Test API response parsing -2. **Validation Tests** - Test struct validation -3. **Business Logic Tests** - Test derived properties and methods - -### Phase 4: Main Interface (Week 6) - -#### High-level API Tests: -```go -func TestGarthLogin(t *testing.T) -func TestGarthConnectAPI(t *testing.T) -func TestGarthSave(t *testing.T) -func TestGarthResume(t *testing.T) -``` - -#### Integration Tests: -```go -func TestEndToEndSleepDataRetrieval(t *testing.T) -func TestEndToEndStressDataRetrieval(t *testing.T) -``` - -## 5. TDD Development Workflow - -### Red-Green-Refactor Cycle: - -#### For Each Feature: -1. **RED**: Write failing test that describes desired behavior -2. **GREEN**: Write minimal code to make test pass -3. **REFACTOR**: Clean up code while keeping tests green -4. **REPEAT**: Add more test cases and iterate - -#### Example TDD Session - Session Management: - -**Step 1 - RED**: Write failing test -```go -func TestSessionSave(t *testing.T) { - session := &Session{ - OAuth1Token: "token1", - OAuth2Token: "token2", - } +```mermaid +graph LR + A[Next Phase] --> B[CLI Implementation] + A --> C[HTTP Client Enhancement] + A --> D[Health Data Endpoints] - err := session.Save("/tmp/test_session") - require.NoError(t, err) + B --> B1[Complete Login Command] + B --> B2[Add Status Command] + B --> B3[Implement Logout] - // Should create file - _, err = os.Stat("/tmp/test_session") - assert.NoError(t, err) -} -``` - -**Step 2 - GREEN**: Make test pass -```go -type Session struct { - OAuth1Token string `json:"oauth1_token"` - OAuth2Token string `json:"oauth2_token"` -} - -func (s *Session) Save(path string) error { - data, err := json.Marshal(s) - if err != nil { - return err - } - return os.WriteFile(path, data, 0644) -} -``` - -**Step 3 - REFACTOR**: Improve implementation -```go -func (s *Session) Save(path string) error { - // Add validation - if s.OAuth1Token == "" { - return errors.New("oauth1 token required") - } + C --> C1[Retry Logic] + C --> C2[Structured Logging] - data, err := json.MarshalIndent(s, "", " ") - if err != nil { - return fmt.Errorf("marshal session: %w", err) - } - - return os.WriteFile(path, data, 0600) // More secure permissions -} + D --> D1[Sleep Data API] + D --> D2[Stress Tracking] ``` -### Testing Strategy: +1. **CLI Completion (1 day)**: + - Finalize login command with session handling + - Implement status command to verify authentication + - Add logout functionality with session cleanup -#### Unit Tests (80% coverage target): -- All public methods tested -- Error conditions covered -- Edge cases handled +2. **HTTP Client Improvements (1 day)**: + - Implement retry logic with exponential backoff + - Add structured request/response logging + - Handle session expiration scenarios -#### Integration Tests: -- Full authentication flow -- API request/response cycles -- File I/O operations +3. **Health Data Endpoints (2 days)**: + - Implement sleep data retrieval + - Build stress tracking API + - Create body composition models -#### Mock Usage: -```go -type MockHTTPClient interface { - Do(req *http.Request) (*http.Response, error) -} +## 4. Implementation Timeline Update -type MockGarminAPI struct { - responses map[string]*http.Response -} - -func (m *MockGarminAPI) Do(req *http.Request) (*http.Response, error) { - response, exists := m.responses[req.URL.Path] - if !exists { - return nil, errors.New("unexpected request") - } - return response, nil -} -``` - -## 6. Implementation Timeline - -### Week 1: Project Setup + Authentication Tests -- [x] Initialize Go module and project structure -- [x] Write authentication test cases -- [ ] Set up CI/CD pipeline -- [x] Implement basic session management - -### Week 2: Complete Authentication Module +### Week 1-2: Authentication & CLI Focus - [x] Implement OAuth1 flow - [x] Implement OAuth2 token refresh -- [x] Add MFA support (core implementation) -- [x] Comprehensive authentication testing +- [x] Add MFA support +- [x] CLI command structure +- [ ] Complete CLI authentication commands -### Week 3: HTTP Client Module -- [ ] Write HTTP client tests -- [ ] Implement client with retry logic -- [ ] Add request/response logging -- [ ] Mock server for testing +### Revised Week 3-4: +- [ ] HTTP client enhancements +- [ ] Health data endpoint implementation +- [ ] Comprehensive integration testing -### Week 4: Data Models - Core Types -- [ ] User profile models -- [ ] Sleep data models -- [ ] JSON marshaling/unmarshaling tests +## 5. Quality Gates -### Week 5: Data Models - Health Metrics -- [x] Stress data models (implemented) -- [x] Steps, HRV, weight models (implemented) -- [x] Validation and business logic +### Before CLI Release: +- [ ] 100% test coverage for auth flows +- [ ] End-to-end CLI test scenarios +- [ ] Security audit of session handling -### Week 6: Main Interface + Integration -- [ ] High-level API implementation -- [ ] Integration tests -- [ ] Documentation and examples -- [ ] Performance optimization +## 6. Success Metrics -### Week 7: CLI Tool + Polish -- [ ] Command-line interface -- [ ] Error handling improvements -- [ ] Final testing and bug fixes - -## 7. Quality Gates - -### Before Each Phase Completion: -- [ ] All tests passing -- [ ] Code coverage > 80% -- [ ] Linting passes -- [ ] Documentation updated - -### Before Release: -- [ ] Integration tests with real Garmin API (optional) -- [ ] Performance benchmarks -- [ ] Security review -- [ ] Cross-platform testing - -## 8. Success Metrics - -### Functional Requirements: -- [x] Authentication flow matches Python library -- [x] All data models supported -- [ ] API requests work identically -- [x] Session persistence compatible - -### Quality Requirements: -- [ ] >90% test coverage -- [ ] Zero critical security issues -- [ ] Memory usage < 50MB for typical operations -- [ ] API response time < 2s for standard requests - -### Developer Experience: -- [ ] Clear documentation with examples -- [ ] Easy installation (`go install`) -- [ ] Intuitive API design -- [ ] Comprehensive error messages - -This TDD approach ensures that the Go port will be robust, well-tested, and maintain feature parity with the original Python library while leveraging Go's strengths in performance and concurrency. +### CLI Completion Metrics: +- [ ] Login success rate > 99% +- [ ] Session restore success > 95% +- [ ] Average auth time < 5 seconds diff --git a/go.mod b/go.mod index 9d58220..0ff4025 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/go-playground/validator/v10 v10.27.0 github.com/go-resty/resty/v2 v2.11.0 github.com/joho/godotenv v1.5.1 + github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.8.4 ) diff --git a/internal/api/bodycomposition_test.go b/internal/api/bodycomposition_test.go index a9b4b89..2342e53 100644 --- a/internal/api/bodycomposition_test.go +++ b/internal/api/bodycomposition_test.go @@ -89,12 +89,7 @@ func TestGetBodyComposition(t *testing.T) { } // Create mock authenticator for tests - mockAuth := &struct { - RefreshToken func(_, _ string) (string, error) - }{} - mockAuth.RefreshToken = func(_, _ string) (string, error) { - return "refreshed-token", nil - } + mockAuth := NewMockAuthenticator() client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(server.URL) diff --git a/internal/api/gear_test.go b/internal/api/gear_test.go index 5af0e39..94b1cb7 100644 --- a/internal/api/gear_test.go +++ b/internal/api/gear_test.go @@ -14,13 +14,6 @@ import ( "github.com/stretchr/testify/assert" ) -// mockAuthImpl implements the Authenticator interface for tests -type mockAuthImpl struct{} - -func (m *mockAuthImpl) RefreshToken(_, _ string) (string, error) { - return "refreshed-token", nil -} - func TestGearService(t *testing.T) { // Create test server srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -83,7 +76,7 @@ func TestGearService(t *testing.T) { // Create client // Create mock authenticator for tests - mockAuth := &mockAuthImpl{} + mockAuth := NewMockAuthenticator() client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(srv.URL) @@ -103,7 +96,7 @@ func TestGearService(t *testing.T) { // Create client // Create mock authenticator for tests - mockAuth := &mockAuthImpl{} + mockAuth := NewMockAuthenticator() client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(srv.URL) @@ -123,7 +116,7 @@ func TestGearService(t *testing.T) { // Create client // Create mock authenticator for tests - mockAuth := &mockAuthImpl{} + mockAuth := NewMockAuthenticator() client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(srv.URL) diff --git a/internal/api/health_test.go b/internal/api/health_test.go index 3295bbb..e6b099f 100644 --- a/internal/api/health_test.go +++ b/internal/api/health_test.go @@ -193,8 +193,9 @@ func TestGetSleepData(t *testing.T) { OAuth2Token: "test-token", ExpiresAt: time.Now().Add(8 * time.Hour), } - // Pass nil authenticator for tests - client, err := NewClient(nil, session, "") + // Use mock authenticator + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(mockServer.URL()) @@ -291,8 +292,9 @@ func TestGetHRVData(t *testing.T) { OAuth2Token: "test-token", ExpiresAt: time.Now().Add(8 * time.Hour), } - // Pass nil authenticator for tests - client, err := NewClient(nil, session, "") + // Use mock authenticator + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(mockServer.URL()) @@ -380,8 +382,9 @@ func TestGetBodyBatteryData(t *testing.T) { OAuth2Token: "test-token", ExpiresAt: time.Now().Add(8 * time.Hour), } - // Pass nil authenticator for tests - client, err := NewClient(nil, session, "") + // Use mock authenticator + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(mockServer.URL()) diff --git a/internal/api/integration_test.go b/internal/api/integration_test.go index d987efa..14a4d4c 100644 --- a/internal/api/integration_test.go +++ b/internal/api/integration_test.go @@ -81,8 +81,9 @@ func TestIntegrationHealthMetrics(t *testing.T) { OAuth2Token: "test-token", ExpiresAt: time.Now().Add(8 * time.Hour), } - // For integration tests, pass nil for authenticator since we don't need token refresh - client, err := NewClient(nil, session, "") + // Use mock authenticator for integration tests + mockAuth := &MockAuthenticator{} + client, err := NewClient(mockAuth, session, "") assert.NoError(t, err) client.HTTPClient.SetBaseURL(mockServer.URL()) diff --git a/internal/api/mock_server_test.go b/internal/api/mock_server_test.go index cc8205e..aa4d0b3 100644 --- a/internal/api/mock_server_test.go +++ b/internal/api/mock_server_test.go @@ -408,13 +408,6 @@ func (m *MockServer) handleGear(w http.ResponseWriter, r *http.Request) { }) } -// MockAuthenticator implements garth.Authenticator for testing -type MockAuthenticator struct{} - -func (m *MockAuthenticator) RefreshToken(_, _ string) (string, error) { - return "refreshed-token", nil -} - // NewClientWithBaseURL creates a test client that uses the mock server's URL func NewClientWithBaseURL(baseURL string) *Client { session := &garth.Session{ @@ -423,7 +416,7 @@ func NewClientWithBaseURL(baseURL string) *Client { } // Create mock authenticator for tests - auth := &MockAuthenticator{} + auth := NewMockAuthenticator() client, err := NewClient(auth, session, "") if err != nil { diff --git a/internal/api/test_helpers.go b/internal/api/test_helpers.go new file mode 100644 index 0000000..01bd882 --- /dev/null +++ b/internal/api/test_helpers.go @@ -0,0 +1,35 @@ +package api + +// MockAuthenticator implements the Authenticator interface for testing +type MockAuthenticator struct { + // RefreshTokenFunc can be set for custom refresh behavior + RefreshTokenFunc func(oauth1Token, oauth1Secret string) (string, error) + + // CallCount tracks how many times RefreshToken was called + CallCount int +} + +// RefreshToken implements the Authenticator interface +func (m *MockAuthenticator) RefreshToken(oauth1Token, oauth1Secret string) (string, error) { + m.CallCount++ + + // If custom function is provided, use it + if m.RefreshTokenFunc != nil { + return m.RefreshTokenFunc(oauth1Token, oauth1Secret) + } + + // Default behavior: return a mock token + return "refreshed-test-token", nil +} + +// NewMockAuthenticator creates a new mock authenticator with default behavior +func NewMockAuthenticator() *MockAuthenticator { + return &MockAuthenticator{} +} + +// NewMockAuthenticatorWithFunc creates a mock authenticator with custom refresh behavior +func NewMockAuthenticatorWithFunc(refreshFunc func(string, string) (string, error)) *MockAuthenticator { + return &MockAuthenticator{ + RefreshTokenFunc: refreshFunc, + } +} diff --git a/internal/api/user_test.go b/internal/api/user_test.go index 8a3b3af..c380bf4 100644 --- a/internal/api/user_test.go +++ b/internal/api/user_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/sstent/go-garminconnect/internal/auth/garth" "github.com/stretchr/testify/assert" ) @@ -75,7 +76,18 @@ func TestGetUserProfile(t *testing.T) { mockServer := NewMockServer() defer mockServer.Close() - client := NewClientWithBaseURL(mockServer.URL()) + // Create client with non-expired session + session := &garth.Session{ + OAuth2Token: "test-token", + ExpiresAt: time.Now().Add(8 * time.Hour), + } + // Use mock authenticator + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + client.HTTPClient.SetBaseURL(mockServer.URL()) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -178,7 +190,18 @@ func TestGetUserStats(t *testing.T) { mockServer := NewMockServer() defer mockServer.Close() - client := NewClientWithBaseURL(mockServer.URL()) + // Create client with non-expired session + session := &garth.Session{ + OAuth2Token: "test-token", + ExpiresAt: time.Now().Add(8 * time.Hour), + } + // Use mock authenticator + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + client.HTTPClient.SetBaseURL(mockServer.URL()) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/tests_TODO.md b/tests_TODO.md new file mode 100644 index 0000000..b446cb4 --- /dev/null +++ b/tests_TODO.md @@ -0,0 +1,87 @@ +# Mock Authenticator Implementation Tasks + +## Overview +Implement a shared MockAuthenticator to fix test failures caused by: +- Improper interface implementation in tests +- Duplicate mock implementations across files +- Inconsistent test client creation patterns + +## Tasks + +### Phase 1: Create Shared Test Helper (test_helpers.go) +```go +package api + +type MockAuthenticator struct { + RefreshTokenFunc func(oauth1Token, oauth1Secret string) (string, error) + CallCount int +} + +func (m *MockAuthenticator) RefreshToken(oauth1Token, oauth1Secret string) (string, error) { + m.CallCount++ + if m.RefreshTokenFunc != nil { + return m.RefreshTokenFunc(oauth1Token, oauth1Secret) + } + return "refreshed-test-token", nil +} + +func NewMockAuthenticator() *MockAuthenticator { + return &MockAuthenticator{} +} + +func NewMockAuthenticatorWithFunc(refreshFunc func(string, string) (string, error)) *MockAuthenticator { + return &MockAuthenticator{ + RefreshTokenFunc: refreshFunc, + } +} +``` + +### Phase 2: Update Test Files +1. **bodycomposition_test.go** + Replace existing mock with: + ```go + mockAuth := NewMockAuthenticator() + ``` + +2. **gear_test.go** + Remove `mockAuthImpl` definition and use: + ```go + mockAuth := NewMockAuthenticator() + ``` + +3. **health_test.go** + Update client creation: + ```go + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") + ``` + +4. **user_test.go** + Update client creation: + ```go + mockAuth := NewMockAuthenticator() + client, err := NewClient(mockAuth, session, "") + ``` + +5. **mock_server_test.go** + Update `NewClientWithBaseURL`: + ```go + auth := NewMockAuthenticator() + ``` + +### Phase 3: Verification +- [ ] Run tests: `go test ./internal/api/...` +- [ ] Fix any remaining compilation errors +- [ ] Verify all tests pass +- [ ] Check for consistent mock usage across all test files + +## Progress Tracking +- [x] test_helpers.go created +- [x] bodycomposition_test.go updated +- [x] gear_test.go updated +- [x] health_test.go updated +- [x] user_test.go updated +- [x] mock_server_test.go updated +- [x] integration_test.go updated +- [x] Run tests: `go test ./internal/api/...` +- [x] Verify all tests pass