mirror of
https://github.com/sstent/go-garminconnect.git
synced 2026-01-25 16:42:32 +00:00
sync
This commit is contained in:
@@ -45,9 +45,34 @@ docker compose up -d --build
|
||||
|
||||
3. Access the application at: http://localhost:8080
|
||||
|
||||
## Activity Endpoints Implementation Details
|
||||
- [x] Implemented `GetActivities` with pagination support
|
||||
- [x] Created `GetActivityDetails` endpoint
|
||||
- [x] Added custom JSON unmarshalling for activity data
|
||||
- [x] Implemented robust error handling for 404 responses
|
||||
- [x] Added GPS track point timestamp parsing
|
||||
- [x] Created comprehensive table-driven tests
|
||||
- Custom time parsing with garminTime structure
|
||||
- Mock server implementation
|
||||
- Test coverage for 200/404 responses
|
||||
|
||||
## Gear Management Implementation Details
|
||||
- [x] Implemented `GetGearStats` endpoint
|
||||
- Retrieves detailed statistics for a gear item
|
||||
- Handles 404 responses for invalid UUIDs
|
||||
- [x] Implemented `GetGearActivities` endpoint
|
||||
- Supports pagination (start, limit parameters)
|
||||
- Returns activity details with proper time formatting
|
||||
- [x] Added comprehensive table-driven tests
|
||||
- Mock server implementations
|
||||
- Test coverage for success and error cases
|
||||
- Pagination verification
|
||||
|
||||
## Next Steps
|
||||
- Implement activity upload/download functionality
|
||||
- Add FIT file encoder implementation
|
||||
- Implement additional API endpoints
|
||||
- Complete FIT encoder implementation
|
||||
- Add comprehensive test coverage
|
||||
- Add comprehensive test coverage for all endpoints
|
||||
- Improve error handling and logging
|
||||
- Add session management for MFA flow
|
||||
|
||||
@@ -46,10 +46,17 @@
|
||||
- [ ] Body battery
|
||||
|
||||
#### Activity Endpoints
|
||||
- [ ] Activity list/search
|
||||
- [ ] Activity details
|
||||
- [x] Activity list/search
|
||||
- Implemented with pagination support
|
||||
- [x] Activity details
|
||||
- Added GPS track point timestamp parsing
|
||||
- Custom time handling with garminTime structure
|
||||
- Comprehensive table-driven tests
|
||||
- [ ] Activity upload/download
|
||||
- [ ] Gear management
|
||||
- [x] Gear management
|
||||
- Implemented GetGearStats
|
||||
- Implemented GetGearActivities with pagination
|
||||
- Comprehensive tests
|
||||
|
||||
#### User Data Endpoints
|
||||
- [ ] User summary
|
||||
|
||||
@@ -18,6 +18,7 @@ type Client struct {
|
||||
httpClient *http.Client
|
||||
limiter *rate.Limiter
|
||||
logger Logger
|
||||
Gear *GearService
|
||||
}
|
||||
|
||||
// NewClient creates a new API client
|
||||
@@ -36,6 +37,7 @@ func NewClient(baseURL string, httpClient *http.Client) (*Client, error) {
|
||||
httpClient: httpClient,
|
||||
limiter: rate.NewLimiter(rate.Every(time.Second/10), 10), // 10 requests per second
|
||||
logger: &stdLogger{},
|
||||
Gear: &GearService{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
83
internal/api/gear.go
Normal file
83
internal/api/gear.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GearStats represents detailed statistics for a gear item
|
||||
type GearStats struct {
|
||||
UUID string `json:"uuid"` // Unique identifier for the gear item
|
||||
Name string `json:"name"` // Display name of the gear item
|
||||
Distance float64 `json:"distance"` // in meters
|
||||
TotalActivities int `json:"totalActivities"` // number of activities
|
||||
TotalTime int `json:"totalTime"` // in seconds
|
||||
Calories int `json:"calories"` // total calories
|
||||
ElevationGain float64 `json:"elevationGain"` // in meters
|
||||
ElevationLoss float64 `json:"elevationLoss"` // in meters
|
||||
}
|
||||
|
||||
// GearActivity represents a simplified activity linked to a gear item
|
||||
type GearActivity struct {
|
||||
ActivityID int64 `json:"activityId"` // Activity identifier
|
||||
ActivityName string `json:"activityName"` // Name of the activity
|
||||
StartTime time.Time `json:"startTimeLocal"` // Local start time of the activity
|
||||
Duration int `json:"duration"` // Duration in seconds
|
||||
Distance float64 `json:"distance"` // Distance in meters
|
||||
}
|
||||
|
||||
// GetGearStats retrieves statistics for a specific gear item by its UUID.
|
||||
// Returns a GearStats struct containing gear usage metrics or an error.
|
||||
func (c *Client) GetGearStats(gearUUID string) (GearStats, error) {
|
||||
endpoint := "gear-service/stats/" + gearUUID
|
||||
req, err := c.newRequest(http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return GearStats{}, err
|
||||
}
|
||||
|
||||
var stats GearStats
|
||||
_, err = c.do(req, &stats)
|
||||
if err != nil {
|
||||
return GearStats{}, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetGearActivities retrieves paginated activities associated with a gear item.
|
||||
// start: pagination start index
|
||||
// limit: maximum number of results to return
|
||||
// Returns a slice of GearActivity structs or an error.
|
||||
func (c *Client) GetGearActivities(gearUUID string, start, limit int) ([]GearActivity, error) {
|
||||
endpoint := "gear-service/activities/" + gearUUID
|
||||
params := url.Values{}
|
||||
params.Add("start", strconv.Itoa(start))
|
||||
params.Add("limit", strconv.Itoa(limit))
|
||||
|
||||
req, err := c.newRequest(http.MethodGet, endpoint+"?"+params.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var activities []GearActivity
|
||||
_, err = c.do(req, &activities)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return activities, nil
|
||||
}
|
||||
|
||||
// formatDuration converts total seconds to HH:MM:SS time format.
|
||||
// Primarily used for displaying activity durations in a human-readable format.
|
||||
func formatDuration(seconds int) string {
|
||||
d := time.Duration(seconds) * time.Second
|
||||
hours := int(d.Hours())
|
||||
minutes := int(d.Minutes()) % 60
|
||||
seconds = int(d.Seconds()) % 60
|
||||
return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds)
|
||||
}
|
||||
99
internal/api/gear_test.go
Normal file
99
internal/api/gear_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGearService(t *testing.T) {
|
||||
// Create test server
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/gear-service/stats/valid-uuid":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(GearStats{
|
||||
UUID: "valid-uuid",
|
||||
Name: "Test Gear",
|
||||
Distance: 1500.5,
|
||||
TotalActivities: 10,
|
||||
TotalTime: 3600,
|
||||
})
|
||||
case "/gear-service/stats/invalid-uuid":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintln(w, `{"message": "gear not found"}`)
|
||||
case "/gear-service/activities/valid-uuid":
|
||||
startStr := r.URL.Query().Get("start")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
start, _ := strconv.Atoi(startStr)
|
||||
limit, _ := strconv.Atoi(limitStr)
|
||||
|
||||
activities := []GearActivity{
|
||||
{ActivityID: 1, ActivityName: "Run 1", StartTime: time.Now(), Duration: 1800, Distance: 5000},
|
||||
{ActivityID: 2, ActivityName: "Run 2", StartTime: time.Now().Add(-24*time.Hour), Duration: 3600, Distance: 10000},
|
||||
}
|
||||
|
||||
// Simulate pagination
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := start + limit
|
||||
if end > len(activities) {
|
||||
end = len(activities)
|
||||
}
|
||||
if start > len(activities) {
|
||||
start = len(activities)
|
||||
end = len(activities)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(activities[start:end])
|
||||
case "/gear-service/activities/invalid-uuid":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprintln(w, `{"message": "gear activities not found"}`)
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Create client
|
||||
client, _ := NewClient(srv.URL, http.DefaultClient)
|
||||
client.SetLogger(NewTestLogger(t))
|
||||
|
||||
t.Run("GetGearStats success", func(t *testing.T) {
|
||||
stats, err := client.GetGearStats("valid-uuid")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Test Gear", stats.Name)
|
||||
assert.Equal(t, 1500.5, stats.Distance)
|
||||
})
|
||||
|
||||
t.Run("GetGearStats not found", func(t *testing.T) {
|
||||
_, err := client.GetGearStats("invalid-uuid")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "status code: 404")
|
||||
})
|
||||
|
||||
t.Run("GetGearActivities pagination", func(t *testing.T) {
|
||||
activities, err := client.GetGearActivities("valid-uuid", 0, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, activities, 1)
|
||||
assert.Equal(t, "Run 1", activities[0].ActivityName)
|
||||
|
||||
activities, err = client.GetGearActivities("valid-uuid", 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, activities, 1)
|
||||
assert.Equal(t, "Run 2", activities[0].ActivityName)
|
||||
|
||||
_, err = client.GetGearActivities("invalid-uuid", 0, 10)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "status code: 404")
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user