mirror of
https://github.com/sstent/go-garminconnect.git
synced 2025-12-06 08:02:02 +00:00
garth more done
This commit is contained in:
60
TODO.md
60
TODO.md
@@ -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)
|
|
||||||
@@ -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
82
garminconnect.md
Normal 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
411
garth.md
@@ -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
1
go.mod
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
35
internal/api/test_helpers.go
Normal file
35
internal/api/test_helpers.go
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
87
tests_TODO.md
Normal 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
|
||||||
Reference in New Issue
Block a user