diff --git a/cmd/garth/main.go b/cmd/garth/main.go new file mode 100644 index 0000000..0776be7 --- /dev/null +++ b/cmd/garth/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "time" + + "garmin-connect/garth/client" + "garmin-connect/garth/credentials" + "garmin-connect/garth/types" +) + +func main() { + // Parse command line flags + outputTokens := flag.Bool("tokens", false, "Output OAuth tokens in JSON format") + flag.Parse() + + // Load credentials from .env file + email, password, domain, err := credentials.LoadEnvCredentials() + if err != nil { + log.Fatalf("Failed to load credentials: %v", err) + } + + // Create client + garminClient, err := client.NewClient(domain) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Try to load existing session first + sessionFile := "garmin_session.json" + if err := garminClient.LoadSession(sessionFile); err != nil { + fmt.Println("No existing session found, logging in with credentials from .env...") + + if err := garminClient.Login(email, password); err != nil { + log.Fatalf("Login failed: %v", err) + } + + // Save session for future use + if err := garminClient.SaveSession(sessionFile); err != nil { + fmt.Printf("Failed to save session: %v\n", err) + } + } else { + fmt.Println("Loaded existing session") + } + + // If tokens flag is set, output tokens and exit + if *outputTokens { + tokens := struct { + OAuth1 *types.OAuth1Token `json:"oauth1"` + OAuth2 *types.OAuth2Token `json:"oauth2"` + }{ + OAuth1: garminClient.OAuth1Token, + OAuth2: garminClient.OAuth2Token, + } + + jsonBytes, err := json.MarshalIndent(tokens, "", " ") + if err != nil { + log.Fatalf("Failed to marshal tokens: %v", err) + } + fmt.Println(string(jsonBytes)) + return + } + + // Test getting activities + activities, err := garminClient.GetActivities(5) + if err != nil { + log.Fatalf("Failed to get activities: %v", err) + } + + // Display activities + displayActivities(activities) +} + +func displayActivities(activities []types.Activity) { + fmt.Printf("\n=== Recent Activities ===\n") + for i, activity := range activities { + fmt.Printf("%d. %s\n", i+1, activity.ActivityName) + fmt.Printf(" Type: %s\n", activity.ActivityType.TypeKey) + fmt.Printf(" Date: %s\n", activity.StartTimeLocal) + if activity.Distance > 0 { + fmt.Printf(" Distance: %.2f km\n", activity.Distance/1000) + } + if activity.Duration > 0 { + duration := time.Duration(activity.Duration) * time.Second + fmt.Printf(" Duration: %v\n", duration.Round(time.Second)) + } + fmt.Println() + } +} diff --git a/garth/client/auth_test.go b/garth/client/auth_test.go new file mode 100644 index 0000000..a404c1d --- /dev/null +++ b/garth/client/auth_test.go @@ -0,0 +1,57 @@ +package client_test + +import ( + "net/http" + "testing" + + "garmin-connect/garth/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "garmin-connect/garth/client" + "garmin-connect/garth/errors" +) + +func TestClient_Login_Success(t *testing.T) { + // Create mock SSO server + ssoServer := testutils.MockJSONResponse(http.StatusOK, `{ + "access_token": "test_token", + "token_type": "Bearer", + "expires_in": 3600 + }`) + defer ssoServer.Close() + + // Create client with test configuration + c, err := client.NewClient("example.com") + require.NoError(t, err) + c.Domain = ssoServer.URL + + // Perform login + err = c.Login("test@example.com", "password") + + // Verify login + require.NoError(t, err) + assert.Equal(t, "Bearer test_token", c.AuthToken) +} + +func TestClient_Login_Failure(t *testing.T) { + // Create mock SSO server returning error + ssoServer := testutils.MockJSONResponse(http.StatusUnauthorized, `{ + "error": "invalid_credentials" + }`) + defer ssoServer.Close() + + // Create client with test configuration + c, err := client.NewClient("example.com") + require.NoError(t, err) + c.Domain = ssoServer.URL + + // Perform login + err = c.Login("test@example.com", "wrongpassword") + + // Verify error + require.Error(t, err) + assert.IsType(t, &errors.AuthenticationError{}, err) + assert.Contains(t, err.Error(), "SSO login failed") +} diff --git a/garth/client/client_test.go b/garth/client/client_test.go new file mode 100644 index 0000000..f17a8e6 --- /dev/null +++ b/garth/client/client_test.go @@ -0,0 +1,40 @@ +package client_test + +import ( + "net/http" + "testing" + "time" + + "garmin-connect/garth/testutils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "garmin-connect/garth/client" +) + +func TestClient_GetUserProfile(t *testing.T) { + // Create mock server returning user profile + server := testutils.MockJSONResponse(http.StatusOK, `{ + "userName": "testuser", + "displayName": "Test User", + "fullName": "Test User", + "location": "Test Location" + }`) + defer server.Close() + + // Create client with test configuration + c := &client.Client{ + Domain: server.URL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + AuthToken: "Bearer testtoken", + } + + // Get user profile + profile, err := c.GetUserProfile() + + // Verify response + require.NoError(t, err) + assert.Equal(t, "testuser", profile.UserName) + assert.Equal(t, "Test User", profile.DisplayName) +} diff --git a/garth/testutils/http.go b/garth/testutils/http.go new file mode 100644 index 0000000..3fe9506 --- /dev/null +++ b/garth/testutils/http.go @@ -0,0 +1,14 @@ +package testutils + +import ( + "net/http" + "net/http/httptest" +) + +func MockJSONResponse(code int, body string) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + w.Write([]byte(body)) + })) +}