From eb01e2daa0dc8d5fef3dfc9e1730f8e05221356f Mon Sep 17 00:00:00 2001 From: Justin Murray Date: Fri, 17 Oct 2025 15:25:44 -0400 Subject: [PATCH 1/2] Set user-agent for client requests --- internal/tiger/api/client_util.go | 2 ++ internal/tiger/cmd/graphql.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/internal/tiger/api/client_util.go b/internal/tiger/api/client_util.go index a964d8af..a0fb2d81 100644 --- a/internal/tiger/api/client_util.go +++ b/internal/tiger/api/client_util.go @@ -53,6 +53,8 @@ func NewTigerClient(apiKey string) (*ClientWithResponses, error) { // Add API key to Authorization header encodedKey := base64.StdEncoding.EncodeToString([]byte(apiKey)) req.Header.Set("Authorization", "Basic "+encodedKey) + // Add User-Agent header to identify CLI version + req.Header.Set("User-Agent", fmt.Sprintf("tiger-cli/%s", config.Version)) return nil })) diff --git a/internal/tiger/cmd/graphql.go b/internal/tiger/cmd/graphql.go index 9883845e..6bac0e82 100644 --- a/internal/tiger/cmd/graphql.go +++ b/internal/tiger/cmd/graphql.go @@ -6,6 +6,8 @@ import ( "io" "net/http" "strings" + + "github.com/timescale/tiger-cli/internal/tiger/config" ) // We currently use a few GraphQL endpoints as part of the OAuth login flow, @@ -151,6 +153,7 @@ func makeGraphQLRequest[T any](queryURL, accessToken, query string, variables ma req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + req.Header.Set("User-Agent", fmt.Sprintf("tiger-cli/%s", config.Version)) resp, err := http.DefaultClient.Do(req) if err != nil { From ec2ea3ab5ea3ad58ff683628afc2807b24fb5d96 Mon Sep 17 00:00:00 2001 From: Justin Murray Date: Fri, 17 Oct 2025 15:56:47 -0400 Subject: [PATCH 2/2] add test coverage --- internal/tiger/api/client_util_test.go | 102 ++++++++++++++++++- internal/tiger/cmd/graphql_test.go | 133 +++++++++++++++++++++++++ 2 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 internal/tiger/cmd/graphql_test.go diff --git a/internal/tiger/api/client_util_test.go b/internal/tiger/api/client_util_test.go index 6f61f3c8..a8f3c2d7 100644 --- a/internal/tiger/api/client_util_test.go +++ b/internal/tiger/api/client_util_test.go @@ -3,13 +3,14 @@ package api_test import ( "context" "net/http" + "net/http/httptest" "testing" - "github.com/timescale/tiger-cli/internal/tiger/util" - "go.uber.org/mock/gomock" - "github.com/timescale/tiger-cli/internal/tiger/api" "github.com/timescale/tiger-cli/internal/tiger/api/mocks" + "github.com/timescale/tiger-cli/internal/tiger/config" + "github.com/timescale/tiger-cli/internal/tiger/util" + "go.uber.org/mock/gomock" ) func TestValidateAPIKeyWithClient(t *testing.T) { @@ -121,3 +122,98 @@ func TestValidateAPIKey_Integration(t *testing.T) { // This test would require a real API key and network connectivity t.Skip("Integration test requires real API key - implement when needed") } + +func TestNewTigerClientUserAgent(t *testing.T) { + // Create a test server that captures the User-Agent header + var capturedUserAgent string + var requestReceived bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestReceived = true + capturedUserAgent = r.Header.Get("User-Agent") + // Return a valid JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[]`)) // Empty array for services list + })) + defer server.Close() + + // Setup test config with the test server URL + _, err := config.UseTestConfig(t.TempDir(), map[string]any{ + "api_url": server.URL, + }) + if err != nil { + t.Fatalf("Failed to setup test config: %v", err) + } + + // Create a new Tiger client + client, err := api.NewTigerClient("test-api-key") + if err != nil { + t.Fatalf("Failed to create Tiger client: %v", err) + } + + // Make a request to trigger the User-Agent header + ctx := context.Background() + _, err = client.GetProjectsProjectIdServicesWithResponse(ctx, "test-project-id") + if err != nil { + t.Fatalf("Request failed: %v", err) + } + + if !requestReceived { + t.Fatal("Request was not received by test server") + } + + // Verify the User-Agent header was set correctly + expectedUserAgent := "tiger-cli/" + config.Version + if capturedUserAgent != expectedUserAgent { + t.Errorf("Expected User-Agent %q, got %q", expectedUserAgent, capturedUserAgent) + } +} + +func TestNewTigerClientAuthorizationHeader(t *testing.T) { + // Create a test server that captures the Authorization header + var capturedAuthHeader string + var requestReceived bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestReceived = true + capturedAuthHeader = r.Header.Get("Authorization") + // Return a valid JSON response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[]`)) // Empty array for services list + })) + defer server.Close() + + // Setup test config with the test server URL + _, err := config.UseTestConfig(t.TempDir(), map[string]any{ + "api_url": server.URL, + }) + if err != nil { + t.Fatalf("Failed to setup test config: %v", err) + } + + // Create a new Tiger client with a test API key + apiKey := "test-api-key:test-secret-key" + client, err := api.NewTigerClient(apiKey) + if err != nil { + t.Fatalf("Failed to create Tiger client: %v", err) + } + + // Make a request to trigger the Authorization header + ctx := context.Background() + _, err = client.GetProjectsProjectIdServicesWithResponse(ctx, "test-project-id") + if err != nil { + t.Fatalf("Request failed: %v", err) + } + + if !requestReceived { + t.Fatal("Request was not received by test server") + } + + // Verify the Authorization header was set correctly (should be Base64 encoded) + if capturedAuthHeader == "" { + t.Error("Expected Authorization header to be set, but it was empty") + } + if len(capturedAuthHeader) < 6 || capturedAuthHeader[:6] != "Basic " { + t.Errorf("Expected Authorization header to start with 'Basic ', got: %s", capturedAuthHeader) + } +} diff --git a/internal/tiger/cmd/graphql_test.go b/internal/tiger/cmd/graphql_test.go new file mode 100644 index 00000000..5c9970ff --- /dev/null +++ b/internal/tiger/cmd/graphql_test.go @@ -0,0 +1,133 @@ +package cmd + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/timescale/tiger-cli/internal/tiger/config" +) + +func TestGraphQLUserAgent(t *testing.T) { + // Set up a test server that captures the User-Agent header + var capturedUserAgent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedUserAgent = r.Header.Get("User-Agent") + + // Return a valid GraphQL response + response := GraphQLResponse[GetUserData]{ + Data: &GetUserData{ + GetUser: User{ + ID: "test-user-id", + Name: "Test User", + Email: "test@example.com", + }, + }, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + // Create a GraphQL client pointing to our test server + client := &GraphQLClient{ + URL: server.URL, + } + + // Make a request + _, err := client.getUser("test-access-token") + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + // Verify the User-Agent header was set correctly + expectedUserAgent := "tiger-cli/" + config.Version + if capturedUserAgent != expectedUserAgent { + t.Errorf("Expected User-Agent %q, got %q", expectedUserAgent, capturedUserAgent) + } +} + +func TestGraphQLUserAgentInAllRequests(t *testing.T) { + tests := []struct { + name string + requestFunc func(*GraphQLClient, string) (interface{}, error) + }{ + { + name: "getUserProjects", + requestFunc: func(c *GraphQLClient, token string) (interface{}, error) { + return c.getUserProjects(token) + }, + }, + { + name: "getUser", + requestFunc: func(c *GraphQLClient, token string) (interface{}, error) { + return c.getUser(token) + }, + }, + { + name: "createPATRecord", + requestFunc: func(c *GraphQLClient, token string) (interface{}, error) { + return c.createPATRecord(token, "test-project-id", "test-pat-name") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var capturedUserAgent string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedUserAgent = r.Header.Get("User-Agent") + + // Return appropriate response based on the query + var response interface{} + if tt.name == "getUserProjects" { + response = GraphQLResponse[GetAllProjectsData]{ + Data: &GetAllProjectsData{ + GetAllProjects: []Project{ + {ID: "test-id", Name: "test-project"}, + }, + }, + } + } else if tt.name == "getUser" { + response = GraphQLResponse[GetUserData]{ + Data: &GetUserData{ + GetUser: User{ID: "test-id", Name: "Test User", Email: "test@example.com"}, + }, + } + } else if tt.name == "createPATRecord" { + response = GraphQLResponse[CreatePATRecordData]{ + Data: &CreatePATRecordData{ + CreatePATRecord: PATRecordResponse{ + ClientCredentials: struct { + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` + }{ + AccessKey: "test-access-key", + SecretKey: "test-secret-key", + }, + }, + }, + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + })) + defer server.Close() + + client := &GraphQLClient{URL: server.URL} + _, err := tt.requestFunc(client, "test-token") + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + expectedUserAgent := "tiger-cli/" + config.Version + if capturedUserAgent != expectedUserAgent { + t.Errorf("Expected User-Agent %q, got %q", expectedUserAgent, capturedUserAgent) + } + }) + } +}