garth more done

This commit is contained in:
2025-08-29 07:08:35 -07:00
parent fd0924e85e
commit 6b17d41a7a
13 changed files with 340 additions and 443 deletions

60
TODO.md
View File

@@ -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)

View File

@@ -10,11 +10,28 @@ import (
"time" "time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/spf13/cobra"
"github.com/sstent/go-garminconnect/internal/api" "github.com/sstent/go-garminconnect/internal/api"
"github.com/sstent/go-garminconnect/internal/auth/garth" "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 // Try to load from .env if environment variables not set
if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" { if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" {
if err := godotenv.Load(); err != nil { 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 // getCredentials prompts for username and password
func getCredentials() (string, string) { func getCredentials() (string, string) {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)

82
garminconnect.md Normal file
View File

@@ -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%

411
garth.md
View File

@@ -1,4 +1,3 @@
# Garth Go Port Plan - Test-Driven Development Implementation # Garth Go Port Plan - Test-Driven Development Implementation
## Project Overview ## 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 - **API Client**: HTTP client for Garmin Connect API requests
- **Data Models**: Structured data types for health/fitness metrics - **Data Models**: Structured data types for health/fitness metrics
- **Session Management**: Token persistence and restoration - **Session Management**: Token persistence and restoration
- **CLI Interface**: Command-line authentication and session management
## 1. Project Structure ## 1. Project Structure
``` ```
garth-go/ garth-go/
├── cmd/ ├── cmd/
│ └── garth/ # CLI tool (like Python's uvx garth login) │ └── garth/ # CLI tool
│ ├── auth.go # Authentication commands
│ ├── commands.go # Root CLI commands
│ └── main.go │ └── main.go
├── pkg/ ├── pkg/
│ ├── auth/ # Authentication module │ ├── auth/ # Authentication module
@@ -27,378 +29,91 @@ garth-go/
│ │ ├── session.go │ │ ├── session.go
│ │ └── session_test.go │ │ └── session_test.go
│ ├── client/ # HTTP client module │ ├── client/ # HTTP client module
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── endpoints.go
│ │ └── endpoints_test.go
│ ├── models/ # Data structures │ ├── 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/ # Main package interface
│ ├── garth.go
│ └── garth_test.go
├── internal/ ├── internal/
│ ├── testutil/ # Test utilities │ ├── testutil/ # Test utilities
│ │ ├── fixtures.go
│ │ └── mock_server.go
│ └── config/ # Internal configuration │ └── config/ # Internal configuration
│ └── constants.go ├── examples/
├── examples/ # Usage examples
│ ├── basic/
│ ├── sleep_analysis/
│ └── stress_tracking/
├── go.mod ├── go.mod
├── go.sum ├── go.sum
── README.md ── README.md
├── Makefile
└── .github/
└── workflows/
└── ci.yml
``` ```
## 2. Data Flow Architecture ## 2. Current Progress
### Authentication Flow ### Authentication System:
``` - [x] OAuth1/OAuth2 token flow implemented
User Credentials → OAuth1 Token → OAuth2 Token → API Requests - [x] Token auto-refresh mechanism
↓ ↓ - [x] MFA handling with console prompts
Persisted Auto-refresh - [x] Session persistence to JSON file
``` - [x] Comprehensive authentication tests
### API Request Flow ### CLI Implementation:
``` - [x] Command structure with Cobra
Client Request → Token Validation → HTTP Request → JSON Response → Struct Unmarshaling - [x] Basic login command skeleton
- [ ] Session save/restore integration
Auto-refresh if expired - [ ] Status command implementation
``` - [ ] Logout functionality
### Data Processing Flow ## 3. Next Steps (Priority Order)
```
Raw API Response → JSON Unmarshaling → Data Validation → Business Logic → Client Response
```
## 3. Recommended Go Modules ```mermaid
graph LR
### Core Dependencies A[Next Phase] --> B[CLI Implementation]
```go A --> C[HTTP Client Enhancement]
// HTTP client and utilities A --> D[Health Data Endpoints]
"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",
}
err := session.Save("/tmp/test_session") B --> B1[Complete Login Command]
require.NoError(t, err) B --> B2[Add Status Command]
B --> B3[Implement Logout]
// Should create file C --> C1[Retry Logic]
_, err = os.Stat("/tmp/test_session") C --> C2[Structured Logging]
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")
}
data, err := json.MarshalIndent(s, "", " ") D --> D1[Sleep Data API]
if err != nil { D --> D2[Stress Tracking]
return fmt.Errorf("marshal session: %w", err)
}
return os.WriteFile(path, data, 0600) // More secure permissions
}
``` ```
### 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): 2. **HTTP Client Improvements (1 day)**:
- All public methods tested - Implement retry logic with exponential backoff
- Error conditions covered - Add structured request/response logging
- Edge cases handled - Handle session expiration scenarios
#### Integration Tests: 3. **Health Data Endpoints (2 days)**:
- Full authentication flow - Implement sleep data retrieval
- API request/response cycles - Build stress tracking API
- File I/O operations - Create body composition models
#### Mock Usage: ## 4. Implementation Timeline Update
```go
type MockHTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type MockGarminAPI struct { ### Week 1-2: Authentication & CLI Focus
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
- [x] Implement OAuth1 flow - [x] Implement OAuth1 flow
- [x] Implement OAuth2 token refresh - [x] Implement OAuth2 token refresh
- [x] Add MFA support (core implementation) - [x] Add MFA support
- [x] Comprehensive authentication testing - [x] CLI command structure
- [ ] Complete CLI authentication commands
### Week 3: HTTP Client Module ### Revised Week 3-4:
- [ ] Write HTTP client tests - [ ] HTTP client enhancements
- [ ] Implement client with retry logic - [ ] Health data endpoint implementation
- [ ] Add request/response logging - [ ] Comprehensive integration testing
- [ ] Mock server for testing
### Week 4: Data Models - Core Types ## 5. Quality Gates
- [ ] User profile models
- [ ] Sleep data models
- [ ] JSON marshaling/unmarshaling tests
### Week 5: Data Models - Health Metrics ### Before CLI Release:
- [x] Stress data models (implemented) - [ ] 100% test coverage for auth flows
- [x] Steps, HRV, weight models (implemented) - [ ] End-to-end CLI test scenarios
- [x] Validation and business logic - [ ] Security audit of session handling
### Week 6: Main Interface + Integration ## 6. Success Metrics
- [ ] High-level API implementation
- [ ] Integration tests
- [ ] Documentation and examples
- [ ] Performance optimization
### Week 7: CLI Tool + Polish ### CLI Completion Metrics:
- [ ] Command-line interface - [ ] Login success rate > 99%
- [ ] Error handling improvements - [ ] Session restore success > 95%
- [ ] Final testing and bug fixes - [ ] Average auth time < 5 seconds
## 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.

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/go-playground/validator/v10 v10.27.0 github.com/go-playground/validator/v10 v10.27.0
github.com/go-resty/resty/v2 v2.11.0 github.com/go-resty/resty/v2 v2.11.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
) )

View File

@@ -89,12 +89,7 @@ func TestGetBodyComposition(t *testing.T) {
} }
// Create mock authenticator for tests // Create mock authenticator for tests
mockAuth := &struct { mockAuth := NewMockAuthenticator()
RefreshToken func(_, _ string) (string, error)
}{}
mockAuth.RefreshToken = func(_, _ string) (string, error) {
return "refreshed-token", nil
}
client, err := NewClient(mockAuth, session, "") client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(server.URL) client.HTTPClient.SetBaseURL(server.URL)

View File

@@ -14,13 +14,6 @@ import (
"github.com/stretchr/testify/assert" "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) { func TestGearService(t *testing.T) {
// Create test server // Create test server
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -83,7 +76,7 @@ func TestGearService(t *testing.T) {
// Create client // Create client
// Create mock authenticator for tests // Create mock authenticator for tests
mockAuth := &mockAuthImpl{} mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "") client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(srv.URL) client.HTTPClient.SetBaseURL(srv.URL)
@@ -103,7 +96,7 @@ func TestGearService(t *testing.T) {
// Create client // Create client
// Create mock authenticator for tests // Create mock authenticator for tests
mockAuth := &mockAuthImpl{} mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "") client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(srv.URL) client.HTTPClient.SetBaseURL(srv.URL)
@@ -123,7 +116,7 @@ func TestGearService(t *testing.T) {
// Create client // Create client
// Create mock authenticator for tests // Create mock authenticator for tests
mockAuth := &mockAuthImpl{} mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "") client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(srv.URL) client.HTTPClient.SetBaseURL(srv.URL)

View File

@@ -193,8 +193,9 @@ func TestGetSleepData(t *testing.T) {
OAuth2Token: "test-token", OAuth2Token: "test-token",
ExpiresAt: time.Now().Add(8 * time.Hour), ExpiresAt: time.Now().Add(8 * time.Hour),
} }
// Pass nil authenticator for tests // Use mock authenticator
client, err := NewClient(nil, session, "") mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(mockServer.URL()) client.HTTPClient.SetBaseURL(mockServer.URL())
@@ -291,8 +292,9 @@ func TestGetHRVData(t *testing.T) {
OAuth2Token: "test-token", OAuth2Token: "test-token",
ExpiresAt: time.Now().Add(8 * time.Hour), ExpiresAt: time.Now().Add(8 * time.Hour),
} }
// Pass nil authenticator for tests // Use mock authenticator
client, err := NewClient(nil, session, "") mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(mockServer.URL()) client.HTTPClient.SetBaseURL(mockServer.URL())
@@ -380,8 +382,9 @@ func TestGetBodyBatteryData(t *testing.T) {
OAuth2Token: "test-token", OAuth2Token: "test-token",
ExpiresAt: time.Now().Add(8 * time.Hour), ExpiresAt: time.Now().Add(8 * time.Hour),
} }
// Pass nil authenticator for tests // Use mock authenticator
client, err := NewClient(nil, session, "") mockAuth := NewMockAuthenticator()
client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(mockServer.URL()) client.HTTPClient.SetBaseURL(mockServer.URL())

View File

@@ -81,8 +81,9 @@ func TestIntegrationHealthMetrics(t *testing.T) {
OAuth2Token: "test-token", OAuth2Token: "test-token",
ExpiresAt: time.Now().Add(8 * time.Hour), ExpiresAt: time.Now().Add(8 * time.Hour),
} }
// For integration tests, pass nil for authenticator since we don't need token refresh // Use mock authenticator for integration tests
client, err := NewClient(nil, session, "") mockAuth := &MockAuthenticator{}
client, err := NewClient(mockAuth, session, "")
assert.NoError(t, err) assert.NoError(t, err)
client.HTTPClient.SetBaseURL(mockServer.URL()) client.HTTPClient.SetBaseURL(mockServer.URL())

View File

@@ -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 // NewClientWithBaseURL creates a test client that uses the mock server's URL
func NewClientWithBaseURL(baseURL string) *Client { func NewClientWithBaseURL(baseURL string) *Client {
session := &garth.Session{ session := &garth.Session{
@@ -423,7 +416,7 @@ func NewClientWithBaseURL(baseURL string) *Client {
} }
// Create mock authenticator for tests // Create mock authenticator for tests
auth := &MockAuthenticator{} auth := NewMockAuthenticator()
client, err := NewClient(auth, session, "") client, err := NewClient(auth, session, "")
if err != nil { if err != nil {

View File

@@ -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,
}
}

View File

@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/sstent/go-garminconnect/internal/auth/garth"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -75,7 +76,18 @@ func TestGetUserProfile(t *testing.T) {
mockServer := NewMockServer() mockServer := NewMockServer()
defer mockServer.Close() 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -178,7 +190,18 @@ func TestGetUserStats(t *testing.T) {
mockServer := NewMockServer() mockServer := NewMockServer()
defer mockServer.Close() 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

87
tests_TODO.md Normal file
View File

@@ -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