diff --git a/shared/management/client/rest/client.go b/shared/management/client/rest/client.go index 2a5de5bbc..4d1de2631 100644 --- a/shared/management/client/rest/client.go +++ b/shared/management/client/rest/client.go @@ -16,6 +16,7 @@ type Client struct { managementURL string authHeader string httpClient HttpClient + userAgent string // Accounts NetBird account APIs // see more: https://docs.netbird.io/api/resources/accounts @@ -128,6 +129,9 @@ func (c *Client) NewRequest(ctx context.Context, method, path string, body io.Re if body != nil { req.Header.Add("Content-Type", "application/json") } + if c.userAgent != "" { + req.Header.Set("User-Agent", c.userAgent) + } if len(query) != 0 { q := req.URL.Query() diff --git a/shared/management/client/rest/client_test.go b/shared/management/client/rest/client_test.go index 54a0290d0..17df8dd8b 100644 --- a/shared/management/client/rest/client_test.go +++ b/shared/management/client/rest/client_test.go @@ -4,10 +4,14 @@ package rest_test import ( + "context" "net/http" "net/http/httptest" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/netbirdio/netbird/management/server/http/testing/testing_tools/channel" "github.com/netbirdio/netbird/shared/management/client/rest" ) @@ -32,3 +36,50 @@ func withBlackBoxServer(t *testing.T, callback func(*rest.Client)) { c := rest.New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") callback(c) } + +func TestClient_UserAgent_Set(t *testing.T) { + expectedUserAgent := "TestApp/1.2.3" + mux := &http.ServeMux{} + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, expectedUserAgent, r.Header.Get("User-Agent")) + w.WriteHeader(200) + _, err := w.Write([]byte("[]")) + require.NoError(t, err) + }) + + c := rest.NewWithOptions( + rest.WithManagementURL(server.URL), + rest.WithPAT("test-token"), + rest.WithUserAgent(expectedUserAgent), + ) + + _, err := c.Accounts.List(context.Background()) + require.NoError(t, err) +} + +func TestClient_UserAgent_NotSet(t *testing.T) { + mux := &http.ServeMux{} + server := httptest.NewServer(mux) + defer server.Close() + + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + // When no custom user agent is set, Go's default HTTP client will set one + // We just verify that the header exists (it will be Go's default) + userAgent := r.Header.Get("User-Agent") + assert.NotEmpty(t, userAgent) + w.WriteHeader(200) + _, err := w.Write([]byte("[]")) + require.NoError(t, err) + }) + + c := rest.NewWithOptions( + rest.WithManagementURL(server.URL), + rest.WithPAT("test-token"), + ) + + _, err := c.Accounts.List(context.Background()) + require.NoError(t, err) +} diff --git a/shared/management/client/rest/options.go b/shared/management/client/rest/options.go index 21f2394e9..17c7e15cd 100644 --- a/shared/management/client/rest/options.go +++ b/shared/management/client/rest/options.go @@ -42,3 +42,10 @@ func WithAuthHeader(value string) option { c.authHeader = value } } + +// WithUserAgent sets a custom User-Agent header for HTTP requests +func WithUserAgent(userAgent string) option { + return func(c *Client) { + c.userAgent = userAgent + } +}