diff --git a/CHANGELOG.md b/CHANGELOG.md index 2153fbe..7af524a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased](https://github.com/fivetran/go-fivetran/compare/v1.3.5...HEAD) +## [Unreleased](https://github.com/fivetran/go-fivetran/compare/v1.3.6...HEAD) + +## [1.3.6](https://github.com/fivetran/go-fivetran/compare/v1.3.5...v1.3.6) + +## Added +- Request-level User-Agent suffix helpers for attributing calls without mutating shared client state: + `HttpService.DoWithUserAgentSuffix`, connection create/details/update/delete suffix variants, + and `MetadataDetailsService.DoWithUserAgentSuffix`. ## [1.3.5](https://github.com/fivetran/go-fivetran/compare/v1.3.4...v1.3.5) diff --git a/connections/connection_create.go b/connections/connection_create.go index 8b4a9c0..778da9e 100644 --- a/connections/connection_create.go +++ b/connections/connection_create.go @@ -1,245 +1,256 @@ package connections import ( - "context" + "context" - "github.com/fivetran/go-fivetran/utils" - httputils "github.com/fivetran/go-fivetran/http_utils" + httputils "github.com/fivetran/go-fivetran/http_utils" + "github.com/fivetran/go-fivetran/utils" ) type ConnectionCreateService struct { - httputils.HttpService - service *string - groupID *string - trustCertificates *bool - trustFingerprints *bool - runSetupTests *bool - paused *bool - syncFrequency *int - dailySyncTime *string - pauseAfterTrial *bool - hybridDeploymentAgentId *string - networkingMethod *string - privateLinkId *string - proxyAgentId *string - dataDelaySensitivity *string - dataDelayThreshold *int - destinationSchemaNames *string - config *ConnectionConfig - auth *ConnectionAuth - configCustom *map[string]interface{} - authCustom *map[string]interface{} + httputils.HttpService + service *string + groupID *string + trustCertificates *bool + trustFingerprints *bool + runSetupTests *bool + paused *bool + syncFrequency *int + dailySyncTime *string + pauseAfterTrial *bool + hybridDeploymentAgentId *string + networkingMethod *string + privateLinkId *string + proxyAgentId *string + dataDelaySensitivity *string + dataDelayThreshold *int + destinationSchemaNames *string + config *ConnectionConfig + auth *ConnectionAuth + configCustom *map[string]interface{} + authCustom *map[string]interface{} } func (s *ConnectionCreateService) requestBase() connectionCreateRequestBase { - return connectionCreateRequestBase{ - Service: s.service, - GroupID: s.groupID, - TrustCertificates: s.trustCertificates, - TrustFingerprints: s.trustFingerprints, - RunSetupTests: s.runSetupTests, - Paused: s.paused, - SyncFrequency: s.syncFrequency, - DailySyncTime: s.dailySyncTime, - PauseAfterTrial: s.pauseAfterTrial, - PrivateLinkId: s.privateLinkId, - HybridDeploymentAgentId: s.hybridDeploymentAgentId, - NetworkingMethod: s.networkingMethod, - ProxyAgentId: s.proxyAgentId, - DataDelaySensitivity: s.dataDelaySensitivity, - DataDelayThreshold: s.dataDelayThreshold, - DestinationSchemaNames: s.destinationSchemaNames, - } + return connectionCreateRequestBase{ + Service: s.service, + GroupID: s.groupID, + TrustCertificates: s.trustCertificates, + TrustFingerprints: s.trustFingerprints, + RunSetupTests: s.runSetupTests, + Paused: s.paused, + SyncFrequency: s.syncFrequency, + DailySyncTime: s.dailySyncTime, + PauseAfterTrial: s.pauseAfterTrial, + PrivateLinkId: s.privateLinkId, + HybridDeploymentAgentId: s.hybridDeploymentAgentId, + NetworkingMethod: s.networkingMethod, + ProxyAgentId: s.proxyAgentId, + DataDelaySensitivity: s.dataDelaySensitivity, + DataDelayThreshold: s.dataDelayThreshold, + DestinationSchemaNames: s.destinationSchemaNames, + } } func (s *ConnectionCreateService) request() *connectionCreateRequest { - var config interface{} - if s.config != nil { - config = s.config.Request() - } - - var auth interface{} - if s.auth != nil { - auth = s.auth.Request() - } - - r := &connectionCreateRequest{ - connectionCreateRequestBase: s.requestBase(), - Config: config, - Auth: auth, - } - - return r + var config interface{} + if s.config != nil { + config = s.config.Request() + } + + var auth interface{} + if s.auth != nil { + auth = s.auth.Request() + } + + r := &connectionCreateRequest{ + connectionCreateRequestBase: s.requestBase(), + Config: config, + Auth: auth, + } + + return r } func (s *ConnectionCreateService) requestCustom() *connectionCustomCreateRequest { - return &connectionCustomCreateRequest{ - connectionCreateRequestBase: s.requestBase(), - Config: s.configCustom, - Auth: s.authCustom, - } + return &connectionCustomCreateRequest{ + connectionCreateRequestBase: s.requestBase(), + Config: s.configCustom, + Auth: s.authCustom, + } } func (s *ConnectionCreateService) requestCustomMerged() (*connectionCustomCreateRequest, error) { - currentConfig := s.configCustom - - if s.config != nil { - var err error - currentConfig, err = s.config.Merge(currentConfig) - if err != nil { - return nil, err - } - } - - currentAuth := s.authCustom - if s.auth != nil { - var err error - currentAuth, err = s.auth.Merge(currentAuth) - if err != nil { - return nil, err - } - } - - return &connectionCustomCreateRequest{ - connectionCreateRequestBase: s.requestBase(), - Config: currentConfig, - Auth: currentAuth, - }, nil + currentConfig := s.configCustom + + if s.config != nil { + var err error + currentConfig, err = s.config.Merge(currentConfig) + if err != nil { + return nil, err + } + } + + currentAuth := s.authCustom + if s.auth != nil { + var err error + currentAuth, err = s.auth.Merge(currentAuth) + if err != nil { + return nil, err + } + } + + return &connectionCustomCreateRequest{ + connectionCreateRequestBase: s.requestBase(), + Config: currentConfig, + Auth: currentAuth, + }, nil } func (s *ConnectionCreateService) Service(value string) *ConnectionCreateService { - s.service = &value - return s + s.service = &value + return s } func (s *ConnectionCreateService) GroupID(value string) *ConnectionCreateService { - s.groupID = &value - return s + s.groupID = &value + return s } func (s *ConnectionCreateService) HybridDeploymentAgentId(value string) *ConnectionCreateService { - s.hybridDeploymentAgentId = &value - return s + s.hybridDeploymentAgentId = &value + return s } func (s *ConnectionCreateService) ProxyAgentId(value string) *ConnectionCreateService { - s.proxyAgentId = &value - return s + s.proxyAgentId = &value + return s } func (s *ConnectionCreateService) PrivateLinkId(value string) *ConnectionCreateService { - s.privateLinkId = &value - return s + s.privateLinkId = &value + return s } func (s *ConnectionCreateService) NetworkingMethod(value string) *ConnectionCreateService { - s.networkingMethod = &value - return s + s.networkingMethod = &value + return s } func (s *ConnectionCreateService) TrustCertificates(value bool) *ConnectionCreateService { - s.trustCertificates = &value - return s + s.trustCertificates = &value + return s } func (s *ConnectionCreateService) TrustFingerprints(value bool) *ConnectionCreateService { - s.trustFingerprints = &value - return s + s.trustFingerprints = &value + return s } func (s *ConnectionCreateService) RunSetupTests(value bool) *ConnectionCreateService { - s.runSetupTests = &value - return s + s.runSetupTests = &value + return s } func (s *ConnectionCreateService) Paused(value bool) *ConnectionCreateService { - s.paused = &value - return s + s.paused = &value + return s } func (s *ConnectionCreateService) SyncFrequency(value *int) *ConnectionCreateService { - s.syncFrequency = value - return s + s.syncFrequency = value + return s } func (s *ConnectionCreateService) DailySyncTime(value string) *ConnectionCreateService { - s.dailySyncTime = &value - return s + s.dailySyncTime = &value + return s } func (s *ConnectionCreateService) PauseAfterTrial(value bool) *ConnectionCreateService { - s.pauseAfterTrial = &value - return s + s.pauseAfterTrial = &value + return s } func (s *ConnectionCreateService) Config(value *ConnectionConfig) *ConnectionCreateService { - s.config = value - return s + s.config = value + return s } func (s *ConnectionCreateService) ConfigCustom(value *map[string]interface{}) *ConnectionCreateService { - s.configCustom = value - return s + s.configCustom = value + return s } func (s *ConnectionCreateService) Auth(value *ConnectionAuth) *ConnectionCreateService { - s.auth = value - return s + s.auth = value + return s } func (s *ConnectionCreateService) AuthCustom(value *map[string]interface{}) *ConnectionCreateService { - s.authCustom = value - return s + s.authCustom = value + return s } func (s *ConnectionCreateService) DataDelayThreshold(value *int) *ConnectionCreateService { - s.dataDelayThreshold = value - return s + s.dataDelayThreshold = value + return s } func (s *ConnectionCreateService) DataDelaySensitivity(value string) *ConnectionCreateService { - s.dataDelaySensitivity = &value - return s + s.dataDelaySensitivity = &value + return s } func (s *ConnectionCreateService) DestinationSchemaNames(value string) *ConnectionCreateService { - s.destinationSchemaNames = &value - return s + s.destinationSchemaNames = &value + return s } func (s *ConnectionCreateService) do(ctx context.Context, req, response any) error { - err := s.HttpService.Do(ctx, "POST", "/connections", req, nil, 201, &response) - return err + return s.HttpService.Do(ctx, "POST", "/connections", req, nil, 201, &response) +} + +func (s *ConnectionCreateService) doWithUserAgentSuffix(ctx context.Context, userAgentSuffix string, req, response any) error { + return s.HttpService.DoWithUserAgentSuffix(ctx, userAgentSuffix, "POST", "/connections", req, nil, 201, &response) } func (s *ConnectionCreateService) Do(ctx context.Context) (DetailsWithConfigResponse, error) { - var response DetailsWithConfigResponse + var response DetailsWithConfigResponse - err := s.do(ctx, s.request(), &response) + err := s.do(ctx, s.request(), &response) - return response, err + return response, err } func (s *ConnectionCreateService) DoCustom(ctx context.Context) (DetailsWithCustomConfigResponse, error) { - var response DetailsWithCustomConfigResponse + var response DetailsWithCustomConfigResponse + + err := s.do(ctx, s.requestCustom(), &response) + + return response, err +} + +func (s *ConnectionCreateService) DoCustomWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (DetailsWithCustomConfigResponse, error) { + var response DetailsWithCustomConfigResponse - err := s.do(ctx, s.requestCustom(), &response) + err := s.doWithUserAgentSuffix(ctx, userAgentSuffix, s.requestCustom(), &response) - return response, err + return response, err } func (s *ConnectionCreateService) DoCustomMerged(ctx context.Context) (DetailsWithCustomMergedConfigResponse, error) { - var response DetailsWithCustomMergedConfigResponse + var response DetailsWithCustomMergedConfigResponse - req, err := s.requestCustomMerged() + req, err := s.requestCustomMerged() - if err != nil { - return response, err - } + if err != nil { + return response, err + } - err = s.do(ctx, req, &response) + err = s.do(ctx, req, &response) - if err == nil { - err = utils.FetchFromMap(&response.Data.CustomConfig, &response.Data.Config) - } + if err == nil { + err = utils.FetchFromMap(&response.Data.CustomConfig, &response.Data.Config) + } - return response, err + return response, err } diff --git a/connections/connection_delete.go b/connections/connection_delete.go index 835b9fd..4911da5 100644 --- a/connections/connection_delete.go +++ b/connections/connection_delete.go @@ -9,7 +9,7 @@ import ( ) type ConnectionDeleteService struct { - httputils.HttpService + httputils.HttpService connectionID *string } @@ -19,8 +19,16 @@ func (s *ConnectionDeleteService) ConnectionID(connectionID string) *ConnectionD } func (s *ConnectionDeleteService) Do(ctx context.Context) (common.CommonResponse, error) { + return s.do(ctx) +} + +func (s *ConnectionDeleteService) DoWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (common.CommonResponse, error) { + return s.doWithUserAgentSuffix(ctx, userAgentSuffix) +} + +func (s *ConnectionDeleteService) do(ctx context.Context) (common.CommonResponse, error) { var response common.CommonResponse - + if s.connectionID == nil { return response, fmt.Errorf("missing required connectionID") } @@ -28,4 +36,16 @@ func (s *ConnectionDeleteService) Do(ctx context.Context) (common.CommonResponse url := fmt.Sprintf("/connections/%v", *s.connectionID) err := s.HttpService.Do(ctx, "DELETE", url, nil, nil, 200, &response) return response, err -} \ No newline at end of file +} + +func (s *ConnectionDeleteService) doWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (common.CommonResponse, error) { + var response common.CommonResponse + + if s.connectionID == nil { + return response, fmt.Errorf("missing required connectionID") + } + + url := fmt.Sprintf("/connections/%v", *s.connectionID) + err := s.HttpService.DoWithUserAgentSuffix(ctx, userAgentSuffix, "DELETE", url, nil, nil, 200, &response) + return response, err +} diff --git a/connections/connection_details.go b/connections/connection_details.go index 97ba36d..5f2f4b1 100644 --- a/connections/connection_details.go +++ b/connections/connection_details.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - "github.com/fivetran/go-fivetran/utils" httputils "github.com/fivetran/go-fivetran/http_utils" + "github.com/fivetran/go-fivetran/utils" ) type ConnectionDetailsService struct { - httputils.HttpService + httputils.HttpService connectionID *string } @@ -22,9 +22,16 @@ func (s *ConnectionDetailsService) do(ctx context.Context, response any) error { if s.connectionID == nil { return fmt.Errorf("missing required ConnectionID") } - url := fmt.Sprintf("/connections/%v", *s.connectionID) - err := s.HttpService.Do(ctx, "GET", url, nil, nil, 200, &response) - return err + url := fmt.Sprintf("/connections/%v", *s.connectionID) + return s.HttpService.Do(ctx, "GET", url, nil, nil, 200, &response) +} + +func (s *ConnectionDetailsService) doWithUserAgentSuffix(ctx context.Context, userAgentSuffix string, response any) error { + if s.connectionID == nil { + return fmt.Errorf("missing required ConnectionID") + } + url := fmt.Sprintf("/connections/%v", *s.connectionID) + return s.HttpService.DoWithUserAgentSuffix(ctx, userAgentSuffix, "GET", url, nil, nil, 200, &response) } func (s *ConnectionDetailsService) Do(ctx context.Context) (DetailsWithConfigNoTestsResponse, error) { @@ -43,6 +50,14 @@ func (s *ConnectionDetailsService) DoCustom(ctx context.Context) (DetailsWithCus return response, err } +func (s *ConnectionDetailsService) DoCustomWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (DetailsWithCustomConfigNoTestsResponse, error) { + var response DetailsWithCustomConfigNoTestsResponse + + err := s.doWithUserAgentSuffix(ctx, userAgentSuffix, &response) + + return response, err +} + func (s *ConnectionDetailsService) DoCustomMerged(ctx context.Context) (DetailsWithCustomMergedConfigNoTestsResponse, error) { var response DetailsWithCustomMergedConfigNoTestsResponse diff --git a/connections/connection_details_test.go b/connections/connection_details_test.go index 9b4558c..88539a0 100644 --- a/connections/connection_details_test.go +++ b/connections/connection_details_test.go @@ -1,100 +1,128 @@ package connections_test import ( - "context" - "net/http" - "testing" + "context" + "net/http" + "testing" - "github.com/fivetran/go-fivetran/connections" - - "github.com/fivetran/go-fivetran/tests/mock" + "github.com/fivetran/go-fivetran/connections" - testutils "github.com/fivetran/go-fivetran/test_utils" + "github.com/fivetran/go-fivetran/tests/mock" + + testutils "github.com/fivetran/go-fivetran/test_utils" ) func TestConnectionDetailsMock(t *testing.T) { - // arrange - ftClient, mockClient := testutils.CreateTestClient() - handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( - - func(req *http.Request) (*http.Response, error) { - response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) - return response, nil - }) - - // act - response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").Do(context.Background()) - - if err != nil { - t.Logf("%+v\n", response) - t.Error(err) - } - - // assert - interactions := mockClient.Interactions() - testutils.AssertEqual(t, len(interactions), 1) - testutils.AssertEqual(t, interactions[0].Handler, handler) - testutils.AssertEqual(t, handler.Interactions, 1) - - assertConnectionDetailsResponse(t, response) + // arrange + ftClient, mockClient := testutils.CreateTestClient() + handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( + + func(req *http.Request) (*http.Response, error) { + response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) + return response, nil + }) + + // act + response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").Do(context.Background()) + + if err != nil { + t.Logf("%+v\n", response) + t.Error(err) + } + + // assert + interactions := mockClient.Interactions() + testutils.AssertEqual(t, len(interactions), 1) + testutils.AssertEqual(t, interactions[0].Handler, handler) + testutils.AssertEqual(t, handler.Interactions, 1) + + assertConnectionDetailsResponse(t, response) } func TestCustomConnectionDetailsMock(t *testing.T) { - // arrange - ftClient, mockClient := testutils.CreateTestClient() - handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( - - func(req *http.Request) (*http.Response, error) { - response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) - return response, nil - }) - - // act - response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").DoCustom(context.Background()) - - if err != nil { - t.Logf("%+v\n", response) - t.Error(err) - } - - // assert - interactions := mockClient.Interactions() - testutils.AssertEqual(t, len(interactions), 1) - testutils.AssertEqual(t, interactions[0].Handler, handler) - testutils.AssertEqual(t, handler.Interactions, 1) - - assertCustomConnectionDetailsResponse(t, response) + // arrange + ftClient, mockClient := testutils.CreateTestClient() + handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( + + func(req *http.Request) (*http.Response, error) { + response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) + return response, nil + }) + + // act + response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").DoCustom(context.Background()) + + if err != nil { + t.Logf("%+v\n", response) + t.Error(err) + } + + // assert + interactions := mockClient.Interactions() + testutils.AssertEqual(t, len(interactions), 1) + testutils.AssertEqual(t, interactions[0].Handler, handler) + testutils.AssertEqual(t, handler.Interactions, 1) + + assertCustomConnectionDetailsResponse(t, response) +} + +func TestCustomConnectionDetailsWithUserAgentSuffixMock(t *testing.T) { + // arrange + ftClient, mockClient := testutils.CreateTestClient() + ftClient.CustomUserAgent("terraform-provider-fivetran/1.9.37") + handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( + func(req *http.Request) (*http.Response, error) { + testutils.AssertEqual(t, req.Header.Get("User-Agent"), "Go-Fivetran/1.3.3 terraform-provider-fivetran/1.9.37 fivetran_connection_v2") + response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) + return response, nil + }) + + // act + response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").DoCustomWithUserAgentSuffix(context.Background(), "fivetran_connection_v2") + + if err != nil { + t.Logf("%+v\n", response) + t.Error(err) + } + + // assert + interactions := mockClient.Interactions() + testutils.AssertEqual(t, len(interactions), 1) + testutils.AssertEqual(t, interactions[0].Handler, handler) + testutils.AssertEqual(t, handler.Interactions, 1) + + assertCustomConnectionDetailsResponse(t, response) } func TestCustomMergedConnectionDetailsMock(t *testing.T) { - // arrange - ftClient, mockClient := testutils.CreateTestClient() - handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( - - func(req *http.Request) (*http.Response, error) { - response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) - return response, nil - }) - - // act - response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").DoCustomMerged(context.Background()) - - if err != nil { - t.Logf("%+v\n", response) - t.Error(err) - } - - // assert - interactions := mockClient.Interactions() - testutils.AssertEqual(t, len(interactions), 1) - testutils.AssertEqual(t, interactions[0].Handler, handler) - testutils.AssertEqual(t, handler.Interactions, 1) - - assertCustomMergedConnectionDetailsResponse(t, response) + // arrange + ftClient, mockClient := testutils.CreateTestClient() + handler := mockClient.When(http.MethodGet, "/v1/connections/connection_id").ThenCall( + + func(req *http.Request) (*http.Response, error) { + response := mock.NewResponse(req, http.StatusOK, prepareConnectionDetailsResponse()) + return response, nil + }) + + // act + response, err := ftClient.NewConnectionDetails().ConnectionID("connection_id").DoCustomMerged(context.Background()) + + if err != nil { + t.Logf("%+v\n", response) + t.Error(err) + } + + // assert + interactions := mockClient.Interactions() + testutils.AssertEqual(t, len(interactions), 1) + testutils.AssertEqual(t, interactions[0].Handler, handler) + testutils.AssertEqual(t, handler.Interactions, 1) + + assertCustomMergedConnectionDetailsResponse(t, response) } func prepareConnectionDetailsResponse() string { - return `{ + return `{ "code": "Success", "data": { "id": "speak_inexpensive", @@ -144,45 +172,45 @@ func prepareConnectionDetailsResponse() string { func assertConnectionDetailsResponse(t *testing.T, response connections.DetailsWithConfigNoTestsResponse) { - testutils.AssertEqual(t, response.Code, "Success") + testutils.AssertEqual(t, response.Code, "Success") - testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Key, "key") - testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Value, "value") - testutils.AssertEqual(t, response.Data.Config.ShareURL, "share_url") - testutils.AssertEqual(t, *response.Data.Config.IsKeypair, true) + testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Key, "key") + testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Value, "value") + testutils.AssertEqual(t, response.Data.Config.ShareURL, "share_url") + testutils.AssertEqual(t, *response.Data.Config.IsKeypair, true) } func assertCustomConnectionDetailsResponse(t *testing.T, response connections.DetailsWithCustomConfigNoTestsResponse) { - testutils.AssertEqual(t, response.Code, "Success") + testutils.AssertEqual(t, response.Code, "Success") - testutils.AssertKey(t, "share_url", response.Data.Config, "share_url") - testutils.AssertKey(t, "is_keypair", response.Data.Config, true) + testutils.AssertKey(t, "share_url", response.Data.Config, "share_url") + testutils.AssertKey(t, "is_keypair", response.Data.Config, true) - secretsList, ok := response.Data.Config["secrets_list"].([]interface{}) + secretsList, ok := response.Data.Config["secrets_list"].([]interface{}) - testutils.AssertEqual(t, ok, true) - testutils.AssertEqual(t, len(secretsList), 1) + testutils.AssertEqual(t, ok, true) + testutils.AssertEqual(t, len(secretsList), 1) - secret := secretsList[0].(map[string]interface{}) + secret := secretsList[0].(map[string]interface{}) - testutils.AssertKey(t, "key", secret, "key") - testutils.AssertKey(t, "value", secret, "value") + testutils.AssertKey(t, "key", secret, "key") + testutils.AssertKey(t, "value", secret, "value") } func assertCustomMergedConnectionDetailsResponse(t *testing.T, response connections.DetailsWithCustomMergedConfigNoTestsResponse) { - testutils.AssertEqual(t, response.Code, "Success") + testutils.AssertEqual(t, response.Code, "Success") - testutils.AssertHasNoKey(t, response.Data.CustomConfig, "share_url") - testutils.AssertHasNoKey(t, response.Data.CustomConfig, "is_keypair") - testutils.AssertHasNoKey(t, response.Data.CustomConfig, "secrets_list") + testutils.AssertHasNoKey(t, response.Data.CustomConfig, "share_url") + testutils.AssertHasNoKey(t, response.Data.CustomConfig, "is_keypair") + testutils.AssertHasNoKey(t, response.Data.CustomConfig, "secrets_list") - testutils.AssertKeyValue(t, response.Data.CustomConfig, "fake_field", "unmapped-value") + testutils.AssertKeyValue(t, response.Data.CustomConfig, "fake_field", "unmapped-value") - testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Key, "key") - testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Value, "value") - testutils.AssertEqual(t, response.Data.Config.ShareURL, "share_url") - testutils.AssertEqual(t, *response.Data.Config.IsKeypair, true) + testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Key, "key") + testutils.AssertEqual(t, response.Data.Config.SecretsList[0].Value, "value") + testutils.AssertEqual(t, response.Data.Config.ShareURL, "share_url") + testutils.AssertEqual(t, *response.Data.Config.IsKeypair, true) } diff --git a/connections/connection_update.go b/connections/connection_update.go index e313dd8..0eed1af 100644 --- a/connections/connection_update.go +++ b/connections/connection_update.go @@ -1,253 +1,268 @@ package connections import ( - "context" - "fmt" + "context" + "fmt" - "github.com/fivetran/go-fivetran/utils" - httputils "github.com/fivetran/go-fivetran/http_utils" + httputils "github.com/fivetran/go-fivetran/http_utils" + "github.com/fivetran/go-fivetran/utils" ) type ConnectionUpdateService struct { - httputils.HttpService - connectionID *string - paused *bool - syncFrequency *int - dailySyncTime *string - trustCertificates *bool - trustFingerprints *bool - isHistoricalSync *bool - scheduleType *string - runSetupTests *bool - pauseAfterTrial *bool - hybridDeploymentAgentId *string - networkingMethod *string - privateLinkId *string - proxyAgentId *string - dataDelaySensitivity *string - dataDelayThreshold *int - schedule *ConnectorSchedule - config *ConnectionConfig - auth *ConnectionAuth - configCustom *map[string]interface{} - authCustom *map[string]interface{} + httputils.HttpService + connectionID *string + paused *bool + syncFrequency *int + dailySyncTime *string + trustCertificates *bool + trustFingerprints *bool + isHistoricalSync *bool + scheduleType *string + runSetupTests *bool + pauseAfterTrial *bool + hybridDeploymentAgentId *string + networkingMethod *string + privateLinkId *string + proxyAgentId *string + dataDelaySensitivity *string + dataDelayThreshold *int + schedule *ConnectorSchedule + config *ConnectionConfig + auth *ConnectionAuth + configCustom *map[string]interface{} + authCustom *map[string]interface{} } func (s *ConnectionUpdateService) requestBase() connectionUpdateRequestBase { - return connectionUpdateRequestBase{ - Paused: s.paused, - SyncFrequency: s.syncFrequency, - DailySyncTime: s.dailySyncTime, - TrustCertificates: s.trustCertificates, - TrustFingerprints: s.trustFingerprints, - IsHistoricalSync: s.isHistoricalSync, - ScheduleType: s.scheduleType, - RunSetupTests: s.runSetupTests, - PauseAfterTrial: s.pauseAfterTrial, - PrivateLinkId: s.privateLinkId, - HybridDeploymentAgentId: s.hybridDeploymentAgentId, - NetworkingMethod: s.networkingMethod, - ProxyAgentId: s.proxyAgentId, - DataDelaySensitivity: s.dataDelaySensitivity, - DataDelayThreshold: s.dataDelayThreshold, - Schedule: s.schedule, - } + return connectionUpdateRequestBase{ + Paused: s.paused, + SyncFrequency: s.syncFrequency, + DailySyncTime: s.dailySyncTime, + TrustCertificates: s.trustCertificates, + TrustFingerprints: s.trustFingerprints, + IsHistoricalSync: s.isHistoricalSync, + ScheduleType: s.scheduleType, + RunSetupTests: s.runSetupTests, + PauseAfterTrial: s.pauseAfterTrial, + PrivateLinkId: s.privateLinkId, + HybridDeploymentAgentId: s.hybridDeploymentAgentId, + NetworkingMethod: s.networkingMethod, + ProxyAgentId: s.proxyAgentId, + DataDelaySensitivity: s.dataDelaySensitivity, + DataDelayThreshold: s.dataDelayThreshold, + Schedule: s.schedule, + } } func (s *ConnectionUpdateService) request() *connectionUpdateRequest { - var config interface{} - if s.config != nil { - config = s.config.Request() - } - - var auth interface{} - if s.auth != nil { - auth = s.auth.Request() - } - - return &connectionUpdateRequest{ - connectionUpdateRequestBase: s.requestBase(), - Config: config, - Auth: auth, - } + var config interface{} + if s.config != nil { + config = s.config.Request() + } + + var auth interface{} + if s.auth != nil { + auth = s.auth.Request() + } + + return &connectionUpdateRequest{ + connectionUpdateRequestBase: s.requestBase(), + Config: config, + Auth: auth, + } } func (s *ConnectionUpdateService) requestCustom() *connectionCustomUpdateRequest { - return &connectionCustomUpdateRequest{ - connectionUpdateRequestBase: s.requestBase(), - Config: s.configCustom, - Auth: s.authCustom, - } + return &connectionCustomUpdateRequest{ + connectionUpdateRequestBase: s.requestBase(), + Config: s.configCustom, + Auth: s.authCustom, + } } func (s *ConnectionUpdateService) requestCustomMerged() (*connectionCustomUpdateRequest, error) { - currentConfig := s.configCustom - - if s.config != nil { - var err error - currentConfig, err = s.config.Merge(currentConfig) - if err != nil { - return nil, err - } - } - - currentAuth := s.authCustom - if s.auth != nil { - var err error - currentAuth, err = s.auth.Merge(currentAuth) - if err != nil { - return nil, err - } - } - - return &connectionCustomUpdateRequest{ - connectionUpdateRequestBase: s.requestBase(), - Config: currentConfig, - Auth: currentAuth, - }, nil + currentConfig := s.configCustom + + if s.config != nil { + var err error + currentConfig, err = s.config.Merge(currentConfig) + if err != nil { + return nil, err + } + } + + currentAuth := s.authCustom + if s.auth != nil { + var err error + currentAuth, err = s.auth.Merge(currentAuth) + if err != nil { + return nil, err + } + } + + return &connectionCustomUpdateRequest{ + connectionUpdateRequestBase: s.requestBase(), + Config: currentConfig, + Auth: currentAuth, + }, nil } func (s *ConnectionUpdateService) ConnectionID(value string) *ConnectionUpdateService { - s.connectionID = &value - return s + s.connectionID = &value + return s } func (s *ConnectionUpdateService) HybridDeploymentAgentId(value string) *ConnectionUpdateService { - s.hybridDeploymentAgentId = &value - return s + s.hybridDeploymentAgentId = &value + return s } func (s *ConnectionUpdateService) ProxyAgentId(value string) *ConnectionUpdateService { - s.proxyAgentId = &value - return s + s.proxyAgentId = &value + return s } func (s *ConnectionUpdateService) PrivateLinkId(value string) *ConnectionUpdateService { - s.privateLinkId = &value - return s + s.privateLinkId = &value + return s } func (s *ConnectionUpdateService) NetworkingMethod(value string) *ConnectionUpdateService { - s.networkingMethod = &value - return s + s.networkingMethod = &value + return s } func (s *ConnectionUpdateService) Paused(value bool) *ConnectionUpdateService { - s.paused = &value - return s + s.paused = &value + return s } func (s *ConnectionUpdateService) SyncFrequency(value *int) *ConnectionUpdateService { - s.syncFrequency = value - return s + s.syncFrequency = value + return s } func (s *ConnectionUpdateService) DailySyncTime(value string) *ConnectionUpdateService { - s.dailySyncTime = &value - return s + s.dailySyncTime = &value + return s } func (s *ConnectionUpdateService) Config(value *ConnectionConfig) *ConnectionUpdateService { - s.config = value - return s + s.config = value + return s } func (s *ConnectionUpdateService) Auth(value *ConnectionAuth) *ConnectionUpdateService { - s.auth = value - return s + s.auth = value + return s } func (s *ConnectionUpdateService) ConfigCustom(value *map[string]interface{}) *ConnectionUpdateService { - s.configCustom = value - return s + s.configCustom = value + return s } func (s *ConnectionUpdateService) AuthCustom(value *map[string]interface{}) *ConnectionUpdateService { - s.authCustom = value - return s + s.authCustom = value + return s } func (s *ConnectionUpdateService) TrustCertificates(value bool) *ConnectionUpdateService { - s.trustCertificates = &value - return s + s.trustCertificates = &value + return s } func (s *ConnectionUpdateService) TrustFingerprints(value bool) *ConnectionUpdateService { - s.trustFingerprints = &value - return s + s.trustFingerprints = &value + return s } func (s *ConnectionUpdateService) IsHistoricalSync(value bool) *ConnectionUpdateService { - s.isHistoricalSync = &value - return s + s.isHistoricalSync = &value + return s } func (s *ConnectionUpdateService) ScheduleType(value string) *ConnectionUpdateService { - s.scheduleType = &value - return s + s.scheduleType = &value + return s } func (s *ConnectionUpdateService) RunSetupTests(value bool) *ConnectionUpdateService { - s.runSetupTests = &value - return s + s.runSetupTests = &value + return s } func (s *ConnectionUpdateService) PauseAfterTrial(value bool) *ConnectionUpdateService { - s.pauseAfterTrial = &value - return s + s.pauseAfterTrial = &value + return s } func (s *ConnectionUpdateService) DataDelayThreshold(value *int) *ConnectionUpdateService { - s.dataDelayThreshold = value - return s + s.dataDelayThreshold = value + return s } func (s *ConnectionUpdateService) DataDelaySensitivity(value string) *ConnectionUpdateService { - s.dataDelaySensitivity = &value - return s + s.dataDelaySensitivity = &value + return s } func (s *ConnectionUpdateService) Schedule(value *ConnectorSchedule) *ConnectionUpdateService { - s.schedule = value - return s + s.schedule = value + return s } func (s *ConnectionUpdateService) do(ctx context.Context, req, response any) error { - if s.connectionID == nil { - return fmt.Errorf("missing required connectionID") - } - url := fmt.Sprintf("/connections/%v", *s.connectionID) - err := s.HttpService.Do(ctx, "PATCH", url, req, nil, 200, &response) - return err + if s.connectionID == nil { + return fmt.Errorf("missing required connectionID") + } + url := fmt.Sprintf("/connections/%v", *s.connectionID) + return s.HttpService.Do(ctx, "PATCH", url, req, nil, 200, &response) +} + +func (s *ConnectionUpdateService) doWithUserAgentSuffix(ctx context.Context, userAgentSuffix string, req, response any) error { + if s.connectionID == nil { + return fmt.Errorf("missing required connectionID") + } + url := fmt.Sprintf("/connections/%v", *s.connectionID) + return s.HttpService.DoWithUserAgentSuffix(ctx, userAgentSuffix, "PATCH", url, req, nil, 200, &response) } func (s *ConnectionUpdateService) Do(ctx context.Context) (DetailsWithConfigResponse, error) { - var response DetailsWithConfigResponse + var response DetailsWithConfigResponse - err := s.do(ctx, s.request(), &response) + err := s.do(ctx, s.request(), &response) - return response, err + return response, err } func (s *ConnectionUpdateService) DoCustom(ctx context.Context) (DetailsWithCustomConfigResponse, error) { - var response DetailsWithCustomConfigResponse + var response DetailsWithCustomConfigResponse + + err := s.do(ctx, s.requestCustom(), &response) + + return response, err +} + +func (s *ConnectionUpdateService) DoCustomWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (DetailsWithCustomConfigResponse, error) { + var response DetailsWithCustomConfigResponse - err := s.do(ctx, s.requestCustom(), &response) + err := s.doWithUserAgentSuffix(ctx, userAgentSuffix, s.requestCustom(), &response) - return response, err + return response, err } func (s *ConnectionUpdateService) DoCustomMerged(ctx context.Context) (DetailsWithCustomMergedConfigResponse, error) { - var response DetailsWithCustomMergedConfigResponse + var response DetailsWithCustomMergedConfigResponse - req, err := s.requestCustomMerged() + req, err := s.requestCustomMerged() - if err != nil { - return response, err - } + if err != nil { + return response, err + } - err = s.do(ctx, req, &response) + err = s.do(ctx, req, &response) - if err == nil { - err = utils.FetchFromMap(&response.Data.CustomConfig, &response.Data.Config) - } + if err == nil { + err = utils.FetchFromMap(&response.Data.CustomConfig, &response.Data.Config) + } - return response, err + return response, err } diff --git a/http_utils/http_service.go b/http_utils/http_service.go index 4186d41..2d9211e 100644 --- a/http_utils/http_service.go +++ b/http_utils/http_service.go @@ -22,7 +22,53 @@ func (s HttpService) Do( queries map[string]string, expectedStatus int, response any) error { + return s.do(ctx, method, url, requestBody, queries, expectedStatus, response) +} +func (s HttpService) DoWithUserAgentSuffix( + ctx context.Context, + userAgentSuffix, + method, + url string, + requestBody any, + queries map[string]string, + expectedStatus int, + response any) error { + return s.doWithUserAgentSuffix(ctx, userAgentSuffix, method, url, requestBody, queries, expectedStatus, response) +} + +func (s HttpService) do( + ctx context.Context, + method, + url string, + requestBody any, + queries map[string]string, + expectedStatus int, + response any) error { + return s.send(ctx, nil, method, url, requestBody, queries, expectedStatus, response) +} + +func (s HttpService) doWithUserAgentSuffix( + ctx context.Context, + userAgentSuffix, + method, + url string, + requestBody any, + queries map[string]string, + expectedStatus int, + response any) error { + return s.send(ctx, &userAgentSuffix, method, url, requestBody, queries, expectedStatus, response) +} + +func (s HttpService) send( + ctx context.Context, + userAgentSuffix *string, + method, + url string, + requestBody any, + queries map[string]string, + expectedStatus int, + response any) error { var body []byte = nil var err error = nil @@ -33,8 +79,17 @@ func (s HttpService) Do( } } + headers := copyHeaders(s.CommonHeaders) + if userAgentSuffix != nil && *userAgentSuffix != "" { + if headers["User-Agent"] == "" { + headers["User-Agent"] = *userAgentSuffix + } else { + headers["User-Agent"] += " " + *userAgentSuffix + } + } + if method == "POST" || method == "PATCH" { - s.CommonHeaders["Content-Type"] = "application/json" + headers["Content-Type"] = "application/json" } r := Request{ @@ -42,7 +97,7 @@ func (s HttpService) Do( Url: s.BaseUrl + url, Body: body, Queries: queries, - Headers: s.CommonHeaders, + Headers: headers, Client: s.Client, HandleRateLimits: s.HandleRateLimits, MaxRetryAttempts: s.MaxRetryAttempts, @@ -63,3 +118,11 @@ func (s HttpService) Do( } return nil } + +func copyHeaders(headers map[string]string) map[string]string { + result := make(map[string]string, len(headers)) + for k, v := range headers { + result[k] = v + } + return result +} diff --git a/http_utils/http_service_test.go b/http_utils/http_service_test.go new file mode 100644 index 0000000..4c7075b --- /dev/null +++ b/http_utils/http_service_test.go @@ -0,0 +1,55 @@ +package httputils + +import ( + "context" + "io" + "net/http" + "strings" + "testing" +) + +type capturingClient struct { + request *http.Request +} + +func (c *capturingClient) Do(req *http.Request) (*http.Response, error) { + c.request = req + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"code":"Success"}`)), + }, nil +} + +func TestDoWithUserAgentSuffixAppendsSuffixWithoutMutatingCommonHeaders(t *testing.T) { + client := &capturingClient{} + service := HttpService{ + BaseUrl: "https://api.example.com/v1", + CommonHeaders: map[string]string{ + "Authorization": "Basic token", + "User-Agent": "Go-Fivetran/1.3.3 terraform-provider-fivetran/1.9.37", + }, + Client: client, + } + + var response map[string]string + err := service.DoWithUserAgentSuffix( + context.Background(), + "fivetran_connection_v2", + http.MethodGet, + "/connections/connection_id", + nil, + nil, + http.StatusOK, + &response, + ) + if err != nil { + t.Fatalf("DoWithUserAgentSuffix returned error: %v", err) + } + + if got, want := client.request.Header.Get("User-Agent"), "Go-Fivetran/1.3.3 terraform-provider-fivetran/1.9.37 fivetran_connection_v2"; got != want { + t.Fatalf("request User-Agent = %q, want %q", got, want) + } + if got, want := service.CommonHeaders["User-Agent"], "Go-Fivetran/1.3.3 terraform-provider-fivetran/1.9.37"; got != want { + t.Fatalf("common User-Agent = %q, want %q", got, want) + } +} diff --git a/metadata/metadata_details.go b/metadata/metadata_details.go index 46a57ce..c14c6fd 100644 --- a/metadata/metadata_details.go +++ b/metadata/metadata_details.go @@ -9,7 +9,7 @@ import ( type MetadataDetailsService struct { httputils.HttpService - service *string + service *string } func (s *MetadataDetailsService) Service(value string) *MetadataDetailsService { @@ -18,12 +18,31 @@ func (s *MetadataDetailsService) Service(value string) *MetadataDetailsService { } func (s *MetadataDetailsService) Do(ctx context.Context) (ConnectorMetadataResponse, error) { + return s.do(ctx) +} + +func (s *MetadataDetailsService) DoWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (ConnectorMetadataResponse, error) { + return s.doWithUserAgentSuffix(ctx, userAgentSuffix) +} + +func (s *MetadataDetailsService) do(ctx context.Context) (ConnectorMetadataResponse, error) { var response ConnectorMetadataResponse - if s.service == nil { - return response, fmt.Errorf("missing required service") - } + if s.service == nil { + return response, fmt.Errorf("missing required service") + } url := fmt.Sprintf("/metadata/connector-types/%v", *s.service) err := s.HttpService.Do(ctx, "GET", url, nil, nil, 200, &response) return response, err -} \ No newline at end of file +} + +func (s *MetadataDetailsService) doWithUserAgentSuffix(ctx context.Context, userAgentSuffix string) (ConnectorMetadataResponse, error) { + var response ConnectorMetadataResponse + if s.service == nil { + return response, fmt.Errorf("missing required service") + } + + url := fmt.Sprintf("/metadata/connector-types/%v", *s.service) + err := s.HttpService.DoWithUserAgentSuffix(ctx, userAgentSuffix, "GET", url, nil, nil, 200, &response) + return response, err +} diff --git a/metadata/metadata_details_test.go b/metadata/metadata_details_test.go index 50236b8..e05f016 100644 --- a/metadata/metadata_details_test.go +++ b/metadata/metadata_details_test.go @@ -39,6 +39,35 @@ func TestMetadataDetailsServiceDo(t *testing.T) { assertMetadataDetailsResponse(t, response) } +func TestMetadataDetailsServiceDoWithUserAgentSuffix(t *testing.T) { + // arrange + + ftClient, mockClient := testutils.CreateTestClient() + ftClient.CustomUserAgent("terraform-provider-fivetran/1.9.37") + handler := mockClient.When(http.MethodGet, "/v1/metadata/connector-types/google_ads"). + ThenCall(func(req *http.Request) (*http.Response, error) { + testutils.AssertEqual(t, req.Header.Get("User-Agent"), "Go-Fivetran/1.3.3 terraform-provider-fivetran/1.9.37 fivetran_connection_v2") + response := mock.NewResponse(req, http.StatusOK, prepareMetadataDetailsResponse()) + return response, nil + }) + + // act + response, err := ftClient.NewMetadataDetails(). + Service("google_ads"). + DoWithUserAgentSuffix(context.Background(), "fivetran_connection_v2") + + // assert + if err != nil { + t.Error(err) + } + + interactions := mockClient.Interactions() + testutils.AssertEqual(t, len(interactions), 1) + testutils.AssertEqual(t, interactions[0].Handler, handler) + testutils.AssertEqual(t, handler.Interactions, 1) + assertMetadataDetailsResponse(t, response) +} + func prepareMetadataDetailsResponse() string { return `{ "code": "Success",