mirror of
https://github.com/sstent/go-garminconnect.git
synced 2026-02-15 03:41:35 +00:00
env working but auth not yet
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user