env working but auth not yet

This commit is contained in:
2025-08-28 14:43:18 -07:00
parent 05eb95304e
commit 5f27c27444
5 changed files with 81 additions and 95 deletions

View File

@@ -20,12 +20,12 @@ func main() {
if err := godotenv.Load(); err != nil { if err := godotenv.Load(); err != nil {
fmt.Println("Failed to load .env file:", err) fmt.Println("Failed to load .env file:", err)
} }
}
// Re-check after loading .env
// Verify required credentials if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" {
if os.Getenv("GARMIN_USERNAME") == "" || os.Getenv("GARMIN_PASSWORD") == "" { fmt.Println("GARMIN_USERNAME and GARMIN_PASSWORD must be set in environment or .env file")
fmt.Println("GARMIN_USERNAME and GARMIN_PASSWORD must be set in environment or .env file") os.Exit(1)
os.Exit(1) }
} }
// Configure session persistence // Configure session persistence
@@ -47,7 +47,8 @@ func main() {
// Perform authentication if no valid session // Perform authentication if no valid session
if session == nil { if session == nil {
username, password := getCredentials() username := os.Getenv("GARMIN_USERNAME")
password := os.Getenv("GARMIN_PASSWORD")
session, err = authClient.Login(username, password) session, err = authClient.Login(username, password)
if err != nil { if err != nil {
fmt.Printf("Authentication failed: %v\n", err) fmt.Printf("Authentication failed: %v\n", err)

View File

@@ -54,6 +54,11 @@ func (c *Client) Get(ctx context.Context, path string, v interface{}) error {
return err return err
} }
// Handle unmarshaling errors for successful responses
if resp.IsSuccess() && resp.Error() != nil {
return handleAPIError(resp)
}
if resp.StatusCode() == http.StatusUnauthorized { if resp.StatusCode() == http.StatusUnauthorized {
// Force token refresh on next attempt // Force token refresh on next attempt
c.session = nil c.session = nil
@@ -79,6 +84,11 @@ func (c *Client) Post(ctx context.Context, path string, body interface{}, v inte
return err return err
} }
// Handle unmarshaling errors for successful responses
if resp.IsSuccess() && resp.Error() != nil {
return handleAPIError(resp)
}
if resp.StatusCode() >= 400 { if resp.StatusCode() >= 400 {
return handleAPIError(resp) return handleAPIError(resp)
} }
@@ -110,8 +120,9 @@ func (c *Client) refreshTokenIfNeeded() error {
return nil return nil
} }
// handleAPIError processes non-200 responses // handleAPIError processes API errors including JSON unmarshaling issues
func handleAPIError(resp *resty.Response) error { func handleAPIError(resp *resty.Response) error {
// Check if response has valid JSON error structure
errorResponse := struct { errorResponse := struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` 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) 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()) return fmt.Errorf("unexpected status code: %d", resp.StatusCode())
} }

View File

@@ -2,9 +2,27 @@ package api
import ( import (
"encoding/json" "encoding/json"
"fmt"
"time" "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 // Time represents a Garmin Connect time value
type Time time.Time type Time time.Time
@@ -74,4 +92,4 @@ type BodyComposition struct {
type BodyCompositionRequest struct { type BodyCompositionRequest struct {
StartDate Time `json:"startDate"` StartDate Time `json:"startDate"`
EndDate Time `json:"endDate"` EndDate Time `json:"endDate"`
} }

View File

@@ -23,28 +23,20 @@ type UserProfile struct {
// UserStats represents fitness statistics for a user // UserStats represents fitness statistics for a user
type UserStats struct { type UserStats struct {
TotalSteps int `json:"totalSteps"` TotalSteps int `json:"totalSteps"`
TotalDistance float64 `json:"totalDistance"` // in meters TotalDistance float64 `json:"totalDistance"` // in meters
TotalCalories int `json:"totalCalories"` TotalCalories int `json:"totalCalories"`
ActiveMinutes int `json:"activeMinutes"` ActiveMinutes int `json:"activeMinutes"`
RestingHR int `json:"restingHeartRate"` RestingHR int `json:"restingHeartRate"`
Date time.Time `json:"date"` Date string `json:"date"` // Store as string in "YYYY-MM-DD" format
} }
// GetUserProfile retrieves the user's profile information // GetUserProfile retrieves the user's profile information
func (c *Client) GetUserProfile(ctx context.Context) (*UserProfile, error) { func (c *Client) GetUserProfile(ctx context.Context) (*UserProfile, error) {
var profile UserProfile var profile UserProfile
path := "/userprofile-service/socialProfile" if err := c.Get(ctx, "/userprofile-service/socialProfile", &profile); err != nil {
if err := c.Get(ctx, path, &profile); err != nil {
return nil, fmt.Errorf("failed to get user profile: %w", err) 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 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) { func (c *Client) GetUserStats(ctx context.Context, date time.Time) (*UserStats, error) {
var stats UserStats var stats UserStats
path := fmt.Sprintf("/stats-service/stats/daily/%s", date.Format("2006-01-02")) path := fmt.Sprintf("/stats-service/stats/daily/%s", date.Format("2006-01-02"))
if err := c.Get(ctx, path, &stats); err != nil { if err := c.Get(ctx, path, &stats); err != nil {
return nil, fmt.Errorf("failed to get user stats: %w", err) return nil, fmt.Errorf("failed to get user stats: %w", err)
} }

View File

@@ -11,7 +11,6 @@ import (
) )
func TestGetUserProfile(t *testing.T) { func TestGetUserProfile(t *testing.T) {
// Define test cases
tests := []struct { tests := []struct {
name string name string
mockResponse interface{} mockResponse interface{}
@@ -55,7 +54,7 @@ func TestGetUserProfile(t *testing.T) {
"error": "Profile not found", "error": "Profile not found",
}, },
mockStatus: http.StatusNotFound, mockStatus: http.StatusNotFound,
expectedError: "user profile not found", expectedError: "API error 404: Profile not found",
}, },
{ {
name: "invalid response format", name: "invalid response format",
@@ -63,7 +62,7 @@ func TestGetUserProfile(t *testing.T) {
"invalid": "data", "invalid": "data",
}, },
mockStatus: http.StatusOK, mockStatus: http.StatusOK,
expectedError: "failed to parse user profile", expectedError: "failed to unmarshal successful response",
}, },
{ {
name: "server error", name: "server error",
@@ -71,27 +70,21 @@ func TestGetUserProfile(t *testing.T) {
"error": "Internal server error", "error": "Internal server error",
}, },
mockStatus: http.StatusInternalServerError, mockStatus: http.StatusInternalServerError,
expectedError: "API request failed with status 500", expectedError: "API error 500: Internal server error",
}, },
} }
// Create test server
mockServer := NewMockServer() mockServer := NewMockServer()
defer mockServer.Close() defer mockServer.Close()
// Create client
client := NewClientWithBaseURL(mockServer.URL()) client := NewClientWithBaseURL(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) {
// Configure mock server
mockServer.Reset() mockServer.Reset()
mockServer.SetResponse("/userprofile-service/socialProfile", tt.mockStatus, tt.mockResponse) mockServer.SetResponse("/userprofile-service/socialProfile", tt.mockStatus, tt.mockResponse)
// Execute test
profile, err := client.GetUserProfile(context.Background()) profile, err := client.GetUserProfile(context.Background())
// Assert results
if tt.expectedError != "" { if tt.expectedError != "" {
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError) 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) { func BenchmarkGetUserProfile(b *testing.B) {
// Create test server
mockServer := NewMockServer() mockServer := NewMockServer()
defer mockServer.Close() defer mockServer.Close()
// Setup successful response
mockResponse := map[string]interface{}{ mockResponse := map[string]interface{}{
"displayName": "Benchmark User", "displayName": "Benchmark User",
"fullName": "Benchmark User Full", "fullName": "Benchmark User Full",
@@ -118,58 +108,21 @@ func BenchmarkGetUserProfile(b *testing.B) {
"username": "benchmark", "username": "benchmark",
"profileId": "benchmark-123", "profileId": "benchmark-123",
"profileImageUrlLarge": "https://example.com/benchmark.jpg", "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) mockServer.SetResponse("/userprofile-service/socialProfile", http.StatusOK, mockResponse)
// Create client
client := NewClientWithBaseURL(mockServer.URL()) client := NewClientWithBaseURL(mockServer.URL())
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
_, _ = client.GetUserProfile(context.Background()) _, _ = 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) { func TestGetUserStats(t *testing.T) {
now := time.Now() now := time.Now()
testDate := now.Format("2006-01-02") testDate := now.Format("2006-01-02")
// Define test cases
tests := []struct { tests := []struct {
name string name string
date time.Time date time.Time
@@ -196,7 +149,7 @@ func TestGetUserStats(t *testing.T) {
TotalCalories: 2200, TotalCalories: 2200,
ActiveMinutes: 45, ActiveMinutes: 45,
RestingHR: 55, 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", "error": "No stats found",
}, },
mockStatus: http.StatusNotFound, mockStatus: http.StatusNotFound,
expectedError: "failed to get user stats", expectedError: "API error 404: No stats found",
},
{
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",
}, },
{ {
name: "invalid stats response", name: "invalid stats response",
@@ -224,28 +168,22 @@ func TestGetUserStats(t *testing.T) {
"invalid": "data", "invalid": "data",
}, },
mockStatus: http.StatusOK, mockStatus: http.StatusOK,
expectedError: "failed to parse user stats", expectedError: "failed to unmarshal successful response",
}, },
} }
// Create test server
mockServer := NewMockServer() mockServer := NewMockServer()
defer mockServer.Close() defer mockServer.Close()
// Create client
client := NewClientWithBaseURL(mockServer.URL()) client := NewClientWithBaseURL(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) {
// Configure mock server
mockServer.Reset() mockServer.Reset()
path := fmt.Sprintf("/stats-service/stats/daily/%s", tt.date.Format("2006-01-02")) path := fmt.Sprintf("/stats-service/stats/daily/%s", tt.date.Format("2006-01-02"))
mockServer.SetResponse(path, tt.mockStatus, tt.mockResponse) mockServer.SetResponse(path, tt.mockStatus, tt.mockResponse)
// Execute test
stats, err := client.GetUserStats(context.Background(), tt.date) stats, err := client.GetUserStats(context.Background(), tt.date)
// Assert results
if tt.expectedError != "" { if tt.expectedError != "" {
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), tt.expectedError) 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)
}
}