package api import ( "encoding/json" "net/http" "net/http/httptest" "strconv" "strings" "sync" "time" "github.com/sstent/go-garminconnect/internal/auth/garth" ) // MockServer simulates the Garmin Connect API type MockServer struct { server *httptest.Server mu sync.Mutex // Endpoint handlers activitiesHandler http.HandlerFunc activityDetailsHandler http.HandlerFunc uploadHandler http.HandlerFunc userHandler http.HandlerFunc healthHandler http.HandlerFunc authHandler http.HandlerFunc statsHandler http.HandlerFunc // Added for stats endpoints // Request counters requestCounters map[string]int } // NewMockServer creates a new mock Garmin Connect server func NewMockServer() *MockServer { m := &MockServer{ requestCounters: make(map[string]int), } m.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { m.mu.Lock() defer m.mu.Unlock() // Track request count if m.requestCounters == nil { m.requestCounters = make(map[string]int) } endpointType := "unknown" path := r.URL.Path // Route requests to appropriate handlers based on path patterns switch { case strings.Contains(path, "/activitylist-service/activities"): endpointType = "activities" m.handleActivities(w, r) case strings.Contains(path, "/activity-service/activity/"): endpointType = "activityDetails" m.handleActivityDetails(w, r) case strings.Contains(path, "/upload-service/upload"): endpointType = "upload" m.handleUpload(w, r) case strings.Contains(path, "/userprofile-service") || strings.Contains(path, "/user-service"): endpointType = "user" if m.userHandler != nil { m.userHandler(w, r) return } m.handleUserData(w, r) case strings.Contains(path, "/wellness-service") || strings.Contains(path, "/hrv-service") || strings.Contains(path, "/bodybattery-service"): endpointType = "health" m.handleHealthData(w, r) case strings.Contains(path, "/auth") || strings.Contains(path, "/oauth"): endpointType = "auth" m.handleAuth(w, r) case strings.Contains(path, "/body-composition"): endpointType = "bodycomposition" m.handleBodyComposition(w, r) case strings.Contains(path, "/gear-service"): endpointType = "gear" m.handleGear(w, r) case strings.Contains(path, "/stats-service"): // Added stats routing endpointType = "stats" if m.statsHandler != nil { m.statsHandler(w, r) return } m.handleStats(w, r) default: endpointType = "unknown" http.Error(w, "Not found", http.StatusNotFound) } m.requestCounters[endpointType]++ })) return m } // URL returns the base URL of the mock server func (m *MockServer) URL() string { return m.server.URL } // Close shuts down the mock server func (m *MockServer) Close() { m.server.Close() } // SetActivitiesHandler sets a custom handler for activities endpoint func (m *MockServer) SetActivitiesHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.activitiesHandler = handler } // SetUploadHandler sets a custom handler for upload endpoint func (m *MockServer) SetUploadHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.uploadHandler = handler } // SetActivityDetailsHandler sets a custom handler for activity details endpoint func (m *MockServer) SetActivityDetailsHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.activityDetailsHandler = handler } // SetUserHandler sets a custom handler for user endpoint func (m *MockServer) SetUserHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.userHandler = handler } // SetHealthHandler sets a custom handler for health endpoint func (m *MockServer) SetHealthHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.healthHandler = handler } // SetAuthHandler sets a custom handler for auth endpoint func (m *MockServer) SetAuthHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.authHandler = handler } // SetStatsHandler sets a custom handler for stats endpoint func (m *MockServer) SetStatsHandler(handler http.HandlerFunc) { m.mu.Lock() defer m.mu.Unlock() m.statsHandler = handler } // Reset resets all handlers and counters to default state func (m *MockServer) Reset() { m.mu.Lock() defer m.mu.Unlock() m.activitiesHandler = nil m.activityDetailsHandler = nil m.uploadHandler = nil m.userHandler = nil m.healthHandler = nil m.authHandler = nil m.statsHandler = nil m.requestCounters = make(map[string]int) } // RequestCount returns the number of requests made to a specific endpoint func (m *MockServer) RequestCount(endpoint string) int { m.mu.Lock() defer m.mu.Unlock() return m.requestCounters[endpoint] } // SetResponse sets a standardized response for a specific endpoint func (m *MockServer) SetResponse(endpoint string, status int, body interface{}) { handler := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(body) } switch endpoint { case "activities": m.SetActivitiesHandler(handler) case "activityDetails": m.SetActivityDetailsHandler(handler) case "upload": m.SetUploadHandler(handler) case "user": m.SetUserHandler(handler) case "health": m.SetHealthHandler(handler) case "auth": m.SetAuthHandler(handler) case "stats": m.SetStatsHandler(handler) } } // SetErrorResponse configures an error response for a specific endpoint func (m *MockServer) SetErrorResponse(endpoint string, status int, message string) { m.SetResponse(endpoint, status, map[string]string{"error": message}) } // handleActivities is the default activities endpoint handler func (m *MockServer) handleActivities(w http.ResponseWriter, r *http.Request) { if m.activitiesHandler != nil { m.activitiesHandler(w, r) return } // Default implementation activities := []ActivityResponse{ { ActivityID: 1, Name: "Morning Run", Type: "RUNNING", StartTime: garminTime{time.Now().Add(-24 * time.Hour)}, Duration: 3600, Distance: 10.0, }, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ActivitiesResponse{ Activities: activities, Pagination: Pagination{ Page: 1, PageSize: 10, TotalCount: 1, }, }) } // handleActivityDetails is the default activity details endpoint handler func (m *MockServer) handleActivityDetails(w http.ResponseWriter, r *http.Request) { if m.activityDetailsHandler != nil { m.activityDetailsHandler(w, r) return } // Extract activity ID from path pathParts := strings.Split(r.URL.Path, "/") activityID, err := strconv.ParseInt(pathParts[len(pathParts)-1], 10, 64) if err != nil { http.Error(w, "Invalid activity ID", http.StatusBadRequest) return } activity := ActivityDetailResponse{ ActivityResponse: ActivityResponse{ ActivityID: activityID, Name: "Mock Activity", Type: "RUNNING", StartTime: garminTime{time.Now().Add(-24 * time.Hour)}, Duration: 3600, Distance: 10.0, }, Calories: 500, AverageHR: 150, MaxHR: 170, ElevationGain: 100, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(activity) } // handleUpload is the default activity upload handler func (m *MockServer) handleUpload(w http.ResponseWriter, r *http.Request) { if m.uploadHandler != nil { m.uploadHandler(w, r) return } // Simulate successful upload response := map[string]interface{}{ "activityId": 12345, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(response) } // handleUserData is the default user data handler func (m *MockServer) handleUserData(w http.ResponseWriter, r *http.Request) { if m.userHandler != nil { m.userHandler(w, r) return } // Default to successful response user := map[string]interface{}{ "displayName": "Mock User", "fullName": "Mock User Full", "emailAddress": "mock@example.com", "username": "mockuser", "profileId": "mock-123", "profileImageUrlLarge": "https://example.com/mock.jpg", "location": "Mock Location", "fitnessLevel": "INTERMEDIATE", "height": 175.0, "weight": 70.0, "birthDate": "1990-01-01", } // If a custom handler is set, it will handle the response // Otherwise, we return the default success response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(user) } // handleHealthData is the default health data handler func (m *MockServer) handleHealthData(w http.ResponseWriter, r *http.Request) { if m.healthHandler != nil { m.healthHandler(w, r) return } // Return mock health data data := map[string]interface{}{ "bodyBattery": 90, "stress": 35, "sleep": map[string]interface{}{ "duration": 480, "quality": 85, }, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(data) } // handleAuth is the default authentication handler func (m *MockServer) handleAuth(w http.ResponseWriter, r *http.Request) { if m.authHandler != nil { m.authHandler(w, r) return } // Handle session refresh requests if strings.Contains(r.URL.Path, "/refresh") { // Validate refresh token and return new access token response := map[string]interface{}{ "oauth2_token": "new-mock-token", "expires_in": 3600, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) return } // Simulate successful authentication response := map[string]interface{}{ "oauth2_token": "mock-access-token", "refresh_token": "mock-refresh-token", "expires_in": 3600, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } // handleStats is the default stats handler func (m *MockServer) handleStats(w http.ResponseWriter, r *http.Request) { if m.statsHandler != nil { m.statsHandler(w, r) return } // Extract date from URL path pathParts := strings.Split(r.URL.Path, "/") date := "" if len(pathParts) > 0 { date = pathParts[len(pathParts)-1] } // Default stats response with consistent units (meters) stats := map[string]interface{}{ "totalSteps": 10000, "totalDistance": 8500.5, // Converted to meters "totalCalories": 2200, "activeMinutes": 45, "restingHeartRate": 55, "date": date, // Include date field } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(stats) } // handleBodyComposition handles body composition requests func (m *MockServer) handleBodyComposition(w http.ResponseWriter, r *http.Request) { BodyCompositionHandler(w, r) } // handleGear handles gear service requests func (m *MockServer) handleGear(w http.ResponseWriter, r *http.Request) { // Basic gear handler - can be expanded as needed w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{ "uuid": "test-gear-uuid", "name": "Test Gear", }) } // 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 func NewClientWithBaseURL(baseURL string) *Client { session := &garth.Session{ OAuth2Token: "mock-token", ExpiresAt: time.Now().Add(8 * time.Hour), } // Create mock authenticator for tests auth := &MockAuthenticator{} client, err := NewClient(auth, session, "") if err != nil { panic("failed to create test client: " + err.Error()) } client.HTTPClient.SetBaseURL(baseURL) return client }