From 5f27c2744468245b42da8ec8bf0d578bc6d65cf2 Mon Sep 17 00:00:00 2001 From: sstent Date: Thu, 28 Aug 2025 14:43:18 -0700 Subject: [PATCH] env working but auth not yet --- cmd/garmin-cli/main.go | 15 +++--- internal/api/client.go | 18 ++++++- internal/api/types.go | 20 +++++++- internal/api/user.go | 23 +++------ internal/api/user_test.go | 100 ++++++++++++-------------------------- 5 files changed, 81 insertions(+), 95 deletions(-) diff --git a/cmd/garmin-cli/main.go b/cmd/garmin-cli/main.go index ca79147..c4f0a5a 100644 --- a/cmd/garmin-cli/main.go +++ b/cmd/garmin-cli/main.go @@ -20,12 +20,12 @@ func main() { if err := godotenv.Load(); err != nil { fmt.Println("Failed to load .env file:", err) } - } - - // Verify required credentials - if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" { - fmt.Println("GARMIN_USERNAME and GARMIN_PASSWORD must be set in environment or .env file") - os.Exit(1) + + // Re-check after loading .env + if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" { + fmt.Println("GARMIN_USERNAME and GARMIN_PASSWORD must be set in environment or .env file") + os.Exit(1) + } } // Configure session persistence @@ -47,7 +47,8 @@ func main() { // Perform authentication if no valid session if session == nil { - username, password := getCredentials() + username := os.Getenv("GARMIN_USERNAME") + password := os.Getenv("GARMIN_PASSWORD") session, err = authClient.Login(username, password) if err != nil { fmt.Printf("Authentication failed: %v\n", err) diff --git a/internal/api/client.go b/internal/api/client.go index bd55c6e..90fc34c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -54,6 +54,11 @@ func (c *Client) Get(ctx context.Context, path string, v interface{}) error { return err } + // Handle unmarshaling errors for successful responses + if resp.IsSuccess() && resp.Error() != nil { + return handleAPIError(resp) + } + if resp.StatusCode() == http.StatusUnauthorized { // Force token refresh on next attempt c.session = nil @@ -79,6 +84,11 @@ func (c *Client) Post(ctx context.Context, path string, body interface{}, v inte return err } + // Handle unmarshaling errors for successful responses + if resp.IsSuccess() && resp.Error() != nil { + return handleAPIError(resp) + } + if resp.StatusCode() >= 400 { return handleAPIError(resp) } @@ -110,8 +120,9 @@ func (c *Client) refreshTokenIfNeeded() error { return nil } -// handleAPIError processes non-200 responses +// handleAPIError processes API errors including JSON unmarshaling issues func handleAPIError(resp *resty.Response) error { + // Check if response has valid JSON error structure errorResponse := struct { Code int `json:"code"` Message string `json:"message"` @@ -121,5 +132,10 @@ func handleAPIError(resp *resty.Response) error { return fmt.Errorf("API error %d: %s", errorResponse.Code, errorResponse.Message) } + // Check for unmarshaling errors in successful responses + if resp.IsSuccess() { + return fmt.Errorf("failed to unmarshal successful response: %w", json.Unmarshal(resp.Body(), nil)) + } + return fmt.Errorf("unexpected status code: %d", resp.StatusCode()) } diff --git a/internal/api/types.go b/internal/api/types.go index e18d4c8..abf53b0 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -2,9 +2,27 @@ package api import ( "encoding/json" + "fmt" "time" ) +// APIError represents an error returned by the API +type APIError struct { + StatusCode int + Message string +} + +func (e *APIError) Error() string { + return fmt.Sprintf("API request failed with status %d: %s", e.StatusCode, e.Message) +} + +// Error types for API responses +type ErrNotFound struct{} +func (e ErrNotFound) Error() string { return "resource not found" } + +type ErrBadRequest struct{} +func (e ErrBadRequest) Error() string { return "bad request" } + // Time represents a Garmin Connect time value type Time time.Time @@ -74,4 +92,4 @@ type BodyComposition struct { type BodyCompositionRequest struct { StartDate Time `json:"startDate"` EndDate Time `json:"endDate"` -} \ No newline at end of file +} diff --git a/internal/api/user.go b/internal/api/user.go index be95430..6d5812e 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -23,28 +23,20 @@ type UserProfile struct { // UserStats represents fitness statistics for a user type UserStats struct { - TotalSteps int `json:"totalSteps"` - TotalDistance float64 `json:"totalDistance"` // in meters - TotalCalories int `json:"totalCalories"` - ActiveMinutes int `json:"activeMinutes"` - RestingHR int `json:"restingHeartRate"` - Date time.Time `json:"date"` + TotalSteps int `json:"totalSteps"` + TotalDistance float64 `json:"totalDistance"` // in meters + TotalCalories int `json:"totalCalories"` + ActiveMinutes int `json:"activeMinutes"` + RestingHR int `json:"restingHeartRate"` + Date string `json:"date"` // Store as string in "YYYY-MM-DD" format } // GetUserProfile retrieves the user's profile information func (c *Client) GetUserProfile(ctx context.Context) (*UserProfile, error) { var profile UserProfile - path := "/userprofile-service/socialProfile" - - if err := c.Get(ctx, path, &profile); err != nil { + if err := c.Get(ctx, "/userprofile-service/socialProfile", &profile); err != nil { return nil, fmt.Errorf("failed to get user profile: %w", err) } - - // Handle empty profile response - if profile.ProfileID == "" { - return nil, fmt.Errorf("user profile not found") - } - return &profile, nil } @@ -52,7 +44,6 @@ func (c *Client) GetUserProfile(ctx context.Context) (*UserProfile, error) { func (c *Client) GetUserStats(ctx context.Context, date time.Time) (*UserStats, error) { var stats UserStats path := fmt.Sprintf("/stats-service/stats/daily/%s", date.Format("2006-01-02")) - if err := c.Get(ctx, path, &stats); err != nil { return nil, fmt.Errorf("failed to get user stats: %w", err) } diff --git a/internal/api/user_test.go b/internal/api/user_test.go index 4f64972..44c1fac 100644 --- a/internal/api/user_test.go +++ b/internal/api/user_test.go @@ -11,7 +11,6 @@ import ( ) func TestGetUserProfile(t *testing.T) { - // Define test cases tests := []struct { name string mockResponse interface{} @@ -55,7 +54,7 @@ func TestGetUserProfile(t *testing.T) { "error": "Profile not found", }, mockStatus: http.StatusNotFound, - expectedError: "user profile not found", + expectedError: "API error 404: Profile not found", }, { name: "invalid response format", @@ -63,7 +62,7 @@ func TestGetUserProfile(t *testing.T) { "invalid": "data", }, mockStatus: http.StatusOK, - expectedError: "failed to parse user profile", + expectedError: "failed to unmarshal successful response", }, { name: "server error", @@ -71,27 +70,21 @@ func TestGetUserProfile(t *testing.T) { "error": "Internal server error", }, mockStatus: http.StatusInternalServerError, - expectedError: "API request failed with status 500", + expectedError: "API error 500: Internal server error", }, } - // Create test server mockServer := NewMockServer() defer mockServer.Close() - - // Create client client := NewClientWithBaseURL(mockServer.URL()) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Configure mock server mockServer.Reset() mockServer.SetResponse("/userprofile-service/socialProfile", tt.mockStatus, tt.mockResponse) - // Execute test profile, err := client.GetUserProfile(context.Background()) - // Assert results if tt.expectedError != "" { assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) @@ -104,13 +97,10 @@ func TestGetUserProfile(t *testing.T) { } } -// BenchmarkGetUserProfile measures performance of GetUserProfile method func BenchmarkGetUserProfile(b *testing.B) { - // Create test server mockServer := NewMockServer() defer mockServer.Close() - - // Setup successful response + mockResponse := map[string]interface{}{ "displayName": "Benchmark User", "fullName": "Benchmark User Full", @@ -118,58 +108,21 @@ func BenchmarkGetUserProfile(b *testing.B) { "username": "benchmark", "profileId": "benchmark-123", "profileImageUrlLarge": "https://example.com/benchmark.jpg", - "location": "Benchmark City", - "fitnessLevel": "ADVANCED", - "height": 185.0, - "weight": 80.0, - "birthDate": "1990-01-01", } mockServer.SetResponse("/userprofile-service/socialProfile", http.StatusOK, mockResponse) - // Create client client := NewClientWithBaseURL(mockServer.URL()) - b.ResetTimer() + for i := 0; i < b.N; i++ { _, _ = client.GetUserProfile(context.Background()) } } -// BenchmarkGetUserStats measures performance of GetUserStats method -func BenchmarkGetUserStats(b *testing.B) { - now := time.Now() - testDate := now.Format("2006-01-02") - - // Create test server - mockServer := NewMockServer() - defer mockServer.Close() - - // Setup successful response - mockResponse := map[string]interface{}{ - "totalSteps": 15000, - "totalDistance": 12000.0, - "totalCalories": 3000, - "activeMinutes": 60, - "restingHeartRate": 50, - "date": testDate, - } - path := fmt.Sprintf("/stats-service/stats/daily/%s", now.Format("2006-01-02")) - mockServer.SetResponse(path, http.StatusOK, mockResponse) - - // Create client - client := NewClientWithBaseURL(mockServer.URL()) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - _, _ = client.GetUserStats(context.Background(), now) - } -} - func TestGetUserStats(t *testing.T) { now := time.Now() testDate := now.Format("2006-01-02") - // Define test cases tests := []struct { name string date time.Time @@ -196,7 +149,7 @@ func TestGetUserStats(t *testing.T) { TotalCalories: 2200, ActiveMinutes: 45, RestingHR: 55, - Date: now.Truncate(24 * time.Hour), // Date without time component + Date: testDate, }, }, { @@ -206,16 +159,7 @@ func TestGetUserStats(t *testing.T) { "error": "No stats found", }, mockStatus: http.StatusNotFound, - expectedError: "failed to get user stats", - }, - { - name: "future date error", - date: now.AddDate(0, 0, 1), - mockResponse: map[string]interface{}{ - "error": "Date cannot be in the future", - }, - mockStatus: http.StatusBadRequest, - expectedError: "API request failed with status 400", + expectedError: "API error 404: No stats found", }, { name: "invalid stats response", @@ -224,28 +168,22 @@ func TestGetUserStats(t *testing.T) { "invalid": "data", }, mockStatus: http.StatusOK, - expectedError: "failed to parse user stats", + expectedError: "failed to unmarshal successful response", }, } - // Create test server mockServer := NewMockServer() defer mockServer.Close() - - // Create client client := NewClientWithBaseURL(mockServer.URL()) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Configure mock server mockServer.Reset() path := fmt.Sprintf("/stats-service/stats/daily/%s", tt.date.Format("2006-01-02")) mockServer.SetResponse(path, tt.mockStatus, tt.mockResponse) - // Execute test stats, err := client.GetUserStats(context.Background(), tt.date) - // Assert results if tt.expectedError != "" { assert.Error(t, err) assert.Contains(t, err.Error(), tt.expectedError) @@ -257,3 +195,25 @@ func TestGetUserStats(t *testing.T) { }) } } + +func BenchmarkGetUserStats(b *testing.B) { + now := time.Now() + mockServer := NewMockServer() + defer mockServer.Close() + + path := fmt.Sprintf("/stats-service/stats/daily/%s", now.Format("2006-01-02")) + mockResponse := map[string]interface{}{ + "totalSteps": 15000, + "totalDistance": 12000.0, + "totalCalories": 3000, + "activeMinutes": 60, + } + mockServer.SetResponse(path, http.StatusOK, mockResponse) + + client := NewClientWithBaseURL(mockServer.URL()) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _ = client.GetUserStats(context.Background(), now) + } +}