diff --git a/docs/api/README.md b/docs/api/README.md index aae14f2df..4e7fcd02d 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -6879,7 +6879,7 @@ xor "baseUrl": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Atlar", "secret": "string" } @@ -6912,7 +6912,7 @@ xor "name": "string", "pageSize": 25, "password": "string", - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Bankingcircle", "userCertificate": "string", "userCertificateKey": "string", @@ -6949,7 +6949,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Column" } @@ -7043,7 +7043,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Generic" } @@ -7073,7 +7073,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Increase", "webhookSharedSecret": "string" } @@ -7106,7 +7106,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Mangopay" } @@ -7138,7 +7138,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Modulr" } @@ -7170,7 +7170,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Moneycorp" } @@ -7272,7 +7272,7 @@ xor "endpoint": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Qonto", "stagingToken": "string" } @@ -7304,7 +7304,7 @@ xor "apiKey": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Stripe" } @@ -7364,7 +7364,7 @@ xor "apiKey": "string", "name": "string", "pageSize": 25, - "pollingPeriod": "2m", + "pollingPeriod": "string", "provider": "Wise", "webhookPublicKey": "string" } diff --git a/internal/connectors/config.go b/internal/connectors/config.go index 9bcc6f12a..d152305e1 100644 --- a/internal/connectors/config.go +++ b/internal/connectors/config.go @@ -31,9 +31,13 @@ func combineConfigs(baseConfig models.Config, pluginConfig models.PluginInternal pluginMap = make(map[string]interface{}, len(baseMap)) } - // Merge maps (fields from baseConfig take precedence) + //Merge maps giving precedence to pluginConfig values for key, value := range baseMap { - pluginMap[key] = value + _, exists := pluginMap[key] + if !exists { + pluginMap[key] = value + continue + } } return json.Marshal(pluginMap) diff --git a/internal/connectors/config_test.go b/internal/connectors/config_test.go new file mode 100644 index 000000000..ab50737a8 --- /dev/null +++ b/internal/connectors/config_test.go @@ -0,0 +1,43 @@ +package connectors + +import ( + "encoding/json" + "testing" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCombineConfigs_PluginPrecedence(t *testing.T) { + t.Parallel() + + base := models.Config{ + Name: "conn-name", + PollingPeriod: 10 * time.Minute, // lower than plugin + PageSize: 50, + } + + pluginCfg := map[string]any{ + "apiKey": "sk_test", + "pollingPeriod": "20m0s", // plugin-normalized value should win + } + + b, err := combineConfigs(base, pluginCfg) + require.NoError(t, err) + + var out map[string]any + require.NoError(t, json.Unmarshal(b, &out)) + + // Plugin value should take precedence + assert.Equal(t, "20m0s", out["pollingPeriod"]) + + // Base-only fields should be present + assert.Equal(t, "conn-name", out["name"]) + // pageSize from base since plugin didn't set it + assert.Equal(t, float64(50), out["pageSize"]) // numbers become float64 via json + + // Plugin specific field preserved + assert.Equal(t, "sk_test", out["apiKey"]) +} diff --git a/internal/connectors/engine/engine_test.go b/internal/connectors/engine/engine_test.go index 29314a5de..de5619e8a 100644 --- a/internal/connectors/engine/engine_test.go +++ b/internal/connectors/engine/engine_test.go @@ -134,7 +134,7 @@ var _ = Describe("Engine Tests", func() { config json.RawMessage ) BeforeEach(func() { - config = json.RawMessage(`{"name":"somename","pollingPeriod":"30s"}`) + config = json.RawMessage(`{"name":"somename","pollingPeriod":"30m"}`) }) It("should return validation error when config is not a valid json", func(ctx SpecContext) { @@ -399,7 +399,7 @@ var _ = Describe("Engine Tests", func() { connectorID models.ConnectorID ) BeforeEach(func() { - config = json.RawMessage(`{"name":"somename","pollingPeriod":"30s"}`) + config = json.RawMessage(`{"name":"somename","pollingPeriod":"30m"}`) connectorID = models.ConnectorID{Provider: "dummypay", Reference: uuid.New()} }) @@ -464,7 +464,7 @@ var _ = Describe("Engine Tests", func() { It("should store the updated config", func(ctx SpecContext) { newName := "new-name" - inputJson := json.RawMessage(fmt.Sprintf(`{"name":"%s","pollingPeriod":"2m","pageSize":25}`, newName)) + inputJson := json.RawMessage(fmt.Sprintf(`{"name":"%s","pollingPeriod":"20m","pageSize":25}`, newName)) connector := &models.Connector{ ConnectorBase: models.ConnectorBase{ ID: connectorID, diff --git a/internal/connectors/plugins/public/atlar/config.go b/internal/connectors/plugins/public/atlar/config.go index 3a7323ecc..2e447a971 100644 --- a/internal/connectors/plugins/public/atlar/config.go +++ b/internal/connectors/plugins/public/atlar/config.go @@ -3,22 +3,45 @@ package atlar import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - BaseURL string `json:"baseUrl" validate:"required"` - AccessKey string `json:"accessKey" validate:"required"` - Secret string `json:"secret" validate:"required"` + BaseURL string `json:"baseUrl" validate:"required"` + AccessKey string `json:"accessKey" validate:"required"` + Secret string `json:"secret" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + BaseURL string `json:"baseUrl"` + AccessKey string `json:"accessKey"` + Secret string `json:"secret"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + BaseURL: raw.BaseURL, + AccessKey: raw.AccessKey, + Secret: raw.Secret, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/atlar/config_test.go b/internal/connectors/plugins/public/atlar/config_test.go new file mode 100644 index 000000000..14db8471f --- /dev/null +++ b/internal/connectors/plugins/public/atlar/config_test.go @@ -0,0 +1,70 @@ +package atlar + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test"}`), + expected: Config{ + BaseURL: "https://api.atlar.com", + AccessKey: "ak_test", + Secret: "sk_test", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"baseUrl":"https://api.atlar.com"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test","pollingPeriod":"45m"}`), + expected: Config{ + BaseURL: "https://api.atlar.com", + AccessKey: "ak_test", + Secret: "sk_test", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"baseUrl":"https://api.atlar.com","accessKey":"ak_test","secret":"sk_test","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/bankingcircle/config.go b/internal/connectors/plugins/public/bankingcircle/config.go index 0688a43af..800bec0f2 100644 --- a/internal/connectors/plugins/public/bankingcircle/config.go +++ b/internal/connectors/plugins/public/bankingcircle/config.go @@ -3,25 +3,54 @@ package bankingcircle import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - Username string `json:"username" yaml:"username" validate:"required"` - Password string `json:"password" yaml:"password" validate:"required"` - Endpoint string `json:"endpoint" yaml:"endpoint" validate:"required"` - AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" validate:"required"` - UserCertificate string `json:"userCertificate" yaml:"userCertificate" validate:"required"` - UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey" validate:"required"` + Username string `json:"username" yaml:"username" validate:"required"` + Password string `json:"password" yaml:"password" validate:"required"` + Endpoint string `json:"endpoint" yaml:"endpoint" validate:"required"` + AuthorizationEndpoint string `json:"authorizationEndpoint" yaml:"authorizationEndpoint" validate:"required"` + UserCertificate string `json:"userCertificate" yaml:"userCertificate" validate:"required"` + UserCertificateKey string `json:"userCertificateKey" yaml:"userCertificateKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + Username string `json:"username"` + Password string `json:"password"` + Endpoint string `json:"endpoint"` + AuthorizationEndpoint string `json:"authorizationEndpoint"` + UserCertificate string `json:"userCertificate"` + UserCertificateKey string `json:"userCertificateKey"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + Username: raw.Username, + Password: raw.Password, + Endpoint: raw.Endpoint, + AuthorizationEndpoint: raw.AuthorizationEndpoint, + UserCertificate: raw.UserCertificate, + UserCertificateKey: raw.UserCertificateKey, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/bankingcircle/config_test.go b/internal/connectors/plugins/public/bankingcircle/config_test.go new file mode 100644 index 000000000..0cbbe6cc5 --- /dev/null +++ b/internal/connectors/plugins/public/bankingcircle/config_test.go @@ -0,0 +1,76 @@ +package bankingcircle + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key"}`), + expected: Config{ + Username: "user", + Password: "pass", + Endpoint: "https://api.bankingcircle.com", + AuthorizationEndpoint: "https://auth.bankingcircle.com", + UserCertificate: "cert", + UserCertificateKey: "key", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"username":"user"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key","pollingPeriod":"45m"}`), + expected: Config{ + Username: "user", + Password: "pass", + Endpoint: "https://api.bankingcircle.com", + AuthorizationEndpoint: "https://auth.bankingcircle.com", + UserCertificate: "cert", + UserCertificateKey: "key", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"username":"user","password":"pass","endpoint":"https://api.bankingcircle.com","authorizationEndpoint":"https://auth.bankingcircle.com","userCertificate":"cert","userCertificateKey":"key","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/column/config.go b/internal/connectors/plugins/public/column/config.go index 83736d063..4f37c602d 100644 --- a/internal/connectors/plugins/public/column/config.go +++ b/internal/connectors/plugins/public/column/config.go @@ -2,23 +2,43 @@ package column import ( "encoding/json" - "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required,url"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required,url"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/column/config_test.go b/internal/connectors/plugins/public/column/config_test.go new file mode 100644 index 000000000..51554f7e0 --- /dev/null +++ b/internal/connectors/plugins/public/column/config_test.go @@ -0,0 +1,68 @@ +package column + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.column.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.column.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.column.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/generic/config.go b/internal/connectors/plugins/public/generic/config.go index 7acf12dfa..f949fccb7 100644 --- a/internal/connectors/plugins/public/generic/config.go +++ b/internal/connectors/plugins/public/generic/config.go @@ -3,21 +3,42 @@ package generic import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/generic/config_test.go b/internal/connectors/plugins/public/generic/config_test.go new file mode 100644 index 000000000..7bf44c911 --- /dev/null +++ b/internal/connectors/plugins/public/generic/config_test.go @@ -0,0 +1,68 @@ +package generic + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.generic.example", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.generic.example", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.generic.example","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/increase/config.go b/internal/connectors/plugins/public/increase/config.go index 4723a49de..3d68a3f56 100644 --- a/internal/connectors/plugins/public/increase/config.go +++ b/internal/connectors/plugins/public/increase/config.go @@ -2,22 +2,45 @@ package increase import ( "encoding/json" - "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` - WebhookSharedSecret string `json:"webhookSharedSecret" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + WebhookSharedSecret string `json:"webhookSharedSecret" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + WebhookSharedSecret string `json:"webhookSharedSecret"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + config := Config{ + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + WebhookSharedSecret: raw.WebhookSharedSecret, + PollingPeriod: pp, } validate := validator.New(validator.WithRequiredStructEnabled()) diff --git a/internal/connectors/plugins/public/increase/config_test.go b/internal/connectors/plugins/public/increase/config_test.go new file mode 100644 index 000000000..2d8eb201e --- /dev/null +++ b/internal/connectors/plugins/public/increase/config_test.go @@ -0,0 +1,70 @@ +package increase + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.increase.com", + WebhookSharedSecret: "whsec_123", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"sk_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "sk_test", + Endpoint: "https://api.increase.com", + WebhookSharedSecret: "whsec_123", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"sk_test","endpoint":"https://api.increase.com","webhookSharedSecret":"whsec_123","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/mangopay/config.go b/internal/connectors/plugins/public/mangopay/config.go index 49b12cbaf..48eb1fafb 100644 --- a/internal/connectors/plugins/public/mangopay/config.go +++ b/internal/connectors/plugins/public/mangopay/config.go @@ -3,22 +3,45 @@ package mangopay import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) -type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + type Config struct { + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/mangopay/config_test.go b/internal/connectors/plugins/public/mangopay/config_test.go new file mode 100644 index 000000000..5f8193897 --- /dev/null +++ b/internal/connectors/plugins/public/mangopay/config_test.go @@ -0,0 +1,70 @@ +package mangopay + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.mangopay.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"clientID":"client_123"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com","pollingPeriod":"45m"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.mangopay.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.mangopay.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/modulr/config.go b/internal/connectors/plugins/public/modulr/config.go index a835d4732..a1367916f 100644 --- a/internal/connectors/plugins/public/modulr/config.go +++ b/internal/connectors/plugins/public/modulr/config.go @@ -3,22 +3,45 @@ package modulr import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - APISecret string `json:"apiSecret" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + APISecret string `json:"apiSecret" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload []byte) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + APISecret string `json:"apiSecret"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + APISecret: raw.APISecret, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/modulr/config_test.go b/internal/connectors/plugins/public/modulr/config_test.go new file mode 100644 index 000000000..b65489909 --- /dev/null +++ b/internal/connectors/plugins/public/modulr/config_test.go @@ -0,0 +1,70 @@ +package modulr + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com"}`), + expected: Config{ + APIKey: "ak_test", + APISecret: "as_test", + Endpoint: "https://api.modulr.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"apiKey":"ak_test"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com","pollingPeriod":"45m"}`), + expected: Config{ + APIKey: "ak_test", + APISecret: "as_test", + Endpoint: "https://api.modulr.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"apiKey":"ak_test","apiSecret":"as_test","endpoint":"https://api.modulr.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/moneycorp/config.go b/internal/connectors/plugins/public/moneycorp/config.go index ecb2d6214..b4049134e 100644 --- a/internal/connectors/plugins/public/moneycorp/config.go +++ b/internal/connectors/plugins/public/moneycorp/config.go @@ -3,22 +3,45 @@ package moneycorp import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + PollingPeriod: pp, + } validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/moneycorp/config_test.go b/internal/connectors/plugins/public/moneycorp/config_test.go new file mode 100644 index 000000000..8be0bc4b0 --- /dev/null +++ b/internal/connectors/plugins/public/moneycorp/config_test.go @@ -0,0 +1,70 @@ +package moneycorp + +import ( + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.moneycorp.com", + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: []byte(`{"clientID":"client_123"}`), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com","pollingPeriod":"45m"}`), + expected: Config{ + ClientID: "client_123", + APIKey: "sk_test", + Endpoint: "https://api.moneycorp.com", + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: []byte(`{"clientID":"client_123","apiKey":"sk_test","endpoint":"https://api.moneycorp.com","pollingPeriod":"not-a-duration"}`), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, config) + } + }) + } +} diff --git a/internal/connectors/plugins/public/qonto/config.go b/internal/connectors/plugins/public/qonto/config.go index ca1f3e509..ba758bcce 100644 --- a/internal/connectors/plugins/public/qonto/config.go +++ b/internal/connectors/plugins/public/qonto/config.go @@ -2,22 +2,48 @@ package qonto import ( "encoding/json" - "fmt" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" + "github.com/pkg/errors" ) type Config struct { - ClientID string `json:"clientID" validate:"required"` - APIKey string `json:"apiKey" validate:"required"` - Endpoint string `json:"endpoint" validate:"required"` - StagingToken string `json:"stagingToken" validate:"omitempty"` + ClientID string `json:"clientID" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + Endpoint string `json:"endpoint" validate:"required"` + StagingToken string `json:"stagingToken" validate:"omitempty"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { - return Config{}, fmt.Errorf("%w: %w", err, models.ErrInvalidConfig) + var raw struct { + ClientID string `json:"clientID"` + APIKey string `json:"apiKey"` + Endpoint string `json:"endpoint"` + StagingToken string `json:"stagingToken"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + config := Config{ + ClientID: raw.ClientID, + APIKey: raw.APIKey, + Endpoint: raw.Endpoint, + StagingToken: raw.StagingToken, + PollingPeriod: pp, } validate := validator.New(validator.WithRequiredStructEnabled()) diff --git a/internal/connectors/plugins/public/qonto/config_test.go b/internal/connectors/plugins/public/qonto/config_test.go index 2a173d6ea..200f96d43 100644 --- a/internal/connectors/plugins/public/qonto/config_test.go +++ b/internal/connectors/plugins/public/qonto/config_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/go-playground/validator/v10" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -15,8 +16,19 @@ var _ = Describe("unmarshalAndValidateConfig", func() { expectedError error config Config err error + + defaultPollingPeriod sharedconfig.PollingPeriod + longPollingPeriod sharedconfig.PollingPeriod ) + BeforeEach(func() { + var err error + defaultPollingPeriod, err = sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + Expect(err).To(BeNil()) + longPollingPeriod, err = sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + Expect(err).To(BeNil()) + }) + JustBeforeEach(func() { config, err = unmarshalAndValidateConfig(payload) }) @@ -33,6 +45,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(config.APIKey).To(Equal("validApiKey")) Expect(config.Endpoint).To(Equal("https://example.com")) Expect(config.StagingToken).To(Equal("token123")) + Expect(config.PollingPeriod).To(Equal(defaultPollingPeriod)) }) }) @@ -83,6 +96,7 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(config.APIKey).To(Equal("validApiKey")) Expect(config.Endpoint).To(Equal("https://example.com")) Expect(config.StagingToken).To(BeEmpty()) + Expect(config.PollingPeriod).To(Equal(defaultPollingPeriod)) }) }) @@ -109,4 +123,27 @@ var _ = Describe("unmarshalAndValidateConfig", func() { Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) }) }) + + Context("with custom polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"clientID":"validClient","apiKey":"validApiKey","endpoint":"https://example.com","pollingPeriod":"45m"}`) + }) + + It("should parse and set the custom polling period", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod).To(Equal(longPollingPeriod)) + }) + }) + + Context("with invalid polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"clientID":"validClient","apiKey":"validApiKey","endpoint":"https://example.com","pollingPeriod":"not-a-duration"}`) + expectedError = models.ErrInvalidConfig + }) + + It("should return an error about invalid config", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) + }) + }) }) diff --git a/internal/connectors/plugins/public/stripe/config.go b/internal/connectors/plugins/public/stripe/config.go index 568318bda..f31862cde 100644 --- a/internal/connectors/plugins/public/stripe/config.go +++ b/internal/connectors/plugins/public/stripe/config.go @@ -3,20 +3,41 @@ package stripe import ( "encoding/json" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" "github.com/go-playground/validator/v10" "github.com/pkg/errors" ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + PollingPeriod string `json:"pollingPeriod"` + } + + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + PollingPeriod: pp, + } + validate := validator.New(validator.WithRequiredStructEnabled()) return config, validate.Struct(config) } diff --git a/internal/connectors/plugins/public/stripe/config_test.go b/internal/connectors/plugins/public/stripe/config_test.go new file mode 100644 index 000000000..9b28fa166 --- /dev/null +++ b/internal/connectors/plugins/public/stripe/config_test.go @@ -0,0 +1,99 @@ +package stripe + +import ( + "encoding/json" + "time" + + "github.com/formancehq/payments/internal/models" + "github.com/go-playground/validator/v10" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("unmarshalAndValidateConfig", func() { + var ( + payload json.RawMessage + expectedError error + config Config + err error + ) + + JustBeforeEach(func() { + config, err = unmarshalAndValidateConfig(payload) + }) + + Context("with valid configuration and explicit polling period", func() { + BeforeEach(func() { + // 45 minutes in nanoseconds + payload = json.RawMessage(`{"apiKey":"sk_test_123","pollingPeriod":"45m"}`) + expectedError = nil + }) + + It("should successfully unmarshal and validate with given polling period", func() { + Expect(err).To(BeNil()) + Expect(config.APIKey).To(Equal("sk_test_123")) + Expect(config.PollingPeriod.Duration()).To(Equal(45 * time.Minute)) + }) + }) + + Context("with missing polling period", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"apiKey":"sk_test_123"}`) + expectedError = nil + }) + + It("should default to 30 minutes", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod.Duration()).To(Equal(30 * time.Minute)) + }) + }) + + Context("with polling period lower than minimum", func() { + BeforeEach(func() { + // 5 minutes in nanoseconds + payload = json.RawMessage(`{"apiKey":"sk_test_123","pollingPeriod":"5m"}`) + expectedError = nil + }) + + It("should coerce to minimum 20 minutes", func() { + Expect(err).To(BeNil()) + Expect(config.PollingPeriod.Duration()).To(Equal(20 * time.Minute)) + }) + }) + + Context("with missing apiKey", func() { + BeforeEach(func() { + // 30 minutes in nanoseconds + payload = json.RawMessage(`{"pollingPeriod":"30m"}`) + }) + + It("should return a validation error", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(BeAssignableToTypeOf(validator.ValidationErrors{})) + }) + }) + + Context("with invalid JSON payload", func() { + BeforeEach(func() { + payload = json.RawMessage(`{"apiKey":sk_test_123}`) + expectedError = models.ErrInvalidConfig + }) + + It("should return an unmarshalling error wrapped as invalid config", func() { + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(expectedError.Error()))) + }) + }) +}) + +var _ = Describe("marshall", func() { + It("keeps duration in string format", func() { + raw := json.RawMessage(`{"apiKey":"sk_test_123", "pollingPeriod":"30m"}`) + config, err := unmarshalAndValidateConfig(raw) + Expect(err).To(BeNil()) + marshaledConfig, err := json.Marshal(config) + Expect(err).To(BeNil()) + Expect(string(marshaledConfig)).To(ContainSubstring(`"pollingPeriod":"30m0s"`)) + Expect(string(marshaledConfig)).To(ContainSubstring(`"apiKey":"sk_test_123"`)) + }) +}) diff --git a/internal/connectors/plugins/public/wise/config.go b/internal/connectors/plugins/public/wise/config.go index 681baf8cc..1d861f49f 100644 --- a/internal/connectors/plugins/public/wise/config.go +++ b/internal/connectors/plugins/public/wise/config.go @@ -7,6 +7,7 @@ import ( "encoding/pem" "fmt" + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" "github.com/formancehq/payments/internal/models" errorsutils "github.com/formancehq/payments/internal/utils/errors" "github.com/go-playground/validator/v10" @@ -14,8 +15,9 @@ import ( ) type Config struct { - APIKey string `json:"apiKey" validate:"required"` - WebhookPublicKey string `json:"webhookPublicKey" validate:"required"` + APIKey string `json:"apiKey" validate:"required"` + WebhookPublicKey string `json:"webhookPublicKey" validate:"required"` + PollingPeriod sharedconfig.PollingPeriod `json:"pollingPeriod"` webhookPublicKey *rsa.PublicKey `json:"-"` } @@ -51,10 +53,30 @@ func (c *Config) validate() error { } func unmarshalAndValidateConfig(payload json.RawMessage) (Config, error) { - var config Config - if err := json.Unmarshal(payload, &config); err != nil { + var raw struct { + APIKey string `json:"apiKey"` + WebhookPublicKey string `json:"webhookPublicKey"` + PollingPeriod string `json:"pollingPeriod"` + } + if err := json.Unmarshal(payload, &raw); err != nil { + return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) + } + + pp, err := sharedconfig.NewPollingPeriod( + raw.PollingPeriod, + sharedconfig.DefaultPollingPeriod, + sharedconfig.MinimumPollingPeriod, + ) + if err != nil { return Config{}, errors.Wrap(models.ErrInvalidConfig, err.Error()) } + + config := Config{ + APIKey: raw.APIKey, + WebhookPublicKey: raw.WebhookPublicKey, + PollingPeriod: pp, + } + validate := validator.New(validator.WithRequiredStructEnabled()) if err := validate.Struct(config); err != nil { return config, err diff --git a/internal/connectors/plugins/public/wise/config_test.go b/internal/connectors/plugins/public/wise/config_test.go new file mode 100644 index 000000000..feebae1c3 --- /dev/null +++ b/internal/connectors/plugins/public/wise/config_test.go @@ -0,0 +1,93 @@ +package wise + +import ( + "encoding/json" + "testing" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" + "github.com/formancehq/payments/internal/models" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const validRSAPublicKeyPEM = `-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALiJyoSgGJE0E7E5Wdl66iRS0LlwM651 +01qmPvvrLpzjAU6YewsGmmKzBSSMSmc5QwDFi1Cdm42Hcps225y7sKsCAwEAAQ== +-----END PUBLIC KEY-----` + +func makePayload(t *testing.T, v any) []byte { + b, err := json.Marshal(v) + require.NoError(t, err) + return b +} + +func TestUnmarshalAndValidateConfig(t *testing.T) { + t.Parallel() + + defaultPollingPeriod, _ := sharedconfig.NewPollingPeriod("", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + longPollingPeriod, _ := sharedconfig.NewPollingPeriod("45m", sharedconfig.DefaultPollingPeriod, sharedconfig.MinimumPollingPeriod) + + tests := []struct { + name string + payload []byte + expected Config + expectError bool + }{ + { + name: "Valid Config", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM}), + expected: Config{ + APIKey: "sk_test", + WebhookPublicKey: validRSAPublicKeyPEM, + PollingPeriod: defaultPollingPeriod, + }, + expectError: false, + }, + { + name: "Missing Required Fields", + payload: makePayload(t, map[string]string{"apiKey": "sk_test"}), + expected: Config{}, + expectError: true, + }, + { + name: "Non default polling period", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM, "pollingPeriod": "45m"}), + expected: Config{ + APIKey: "sk_test", + WebhookPublicKey: validRSAPublicKeyPEM, + PollingPeriod: longPollingPeriod, + }, + expectError: false, + }, + { + name: "Invalid polling period", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": validRSAPublicKeyPEM, "pollingPeriod": "not-a-duration"}), + expected: Config{}, + expectError: true, + }, + { + name: "Invalid public key", + payload: makePayload(t, map[string]string{"apiKey": "sk_test", "webhookPublicKey": "not-a-valid-pem"}), + expected: Config{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + config, err := unmarshalAndValidateConfig(tt.payload) + if tt.expectError { + require.Error(t, err) + // For invalid polling period and invalid key, ensure error is marked as invalid config + if tt.name == "Invalid polling period" || tt.name == "Invalid public key" { + assert.ErrorContains(t, err, models.ErrInvalidConfig.Error()) + } + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected.APIKey, config.APIKey) + assert.Equal(t, tt.expected.WebhookPublicKey, config.WebhookPublicKey) + assert.Equal(t, tt.expected.PollingPeriod, config.PollingPeriod) + } + }) + } +} diff --git a/internal/connectors/plugins/sharedconfig/polling_period.go b/internal/connectors/plugins/sharedconfig/polling_period.go new file mode 100644 index 000000000..b4e2625d4 --- /dev/null +++ b/internal/connectors/plugins/sharedconfig/polling_period.go @@ -0,0 +1,37 @@ +package sharedconfig + +import ( + "encoding/json" + "time" +) + +const ( + MinimumPollingPeriod = 20 * time.Minute + DefaultPollingPeriod = 30 * time.Minute +) + +type PollingPeriod time.Duration + +func (p PollingPeriod) Duration() time.Duration { return time.Duration(p) } + +func (p PollingPeriod) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(p).String()) +} + +// Helper to construct the value while applying min/default. +func NewPollingPeriod(raw string, def, min time.Duration) (PollingPeriod, error) { + if raw == "" { + if def < min { + return PollingPeriod(min), nil + } + return PollingPeriod(def), nil + } + v, err := time.ParseDuration(raw) + if err != nil { + return 0, err + } + if v < min { + v = min + } + return PollingPeriod(v), nil +} diff --git a/internal/connectors/plugins/sharedconfig/polling_period_test.go b/internal/connectors/plugins/sharedconfig/polling_period_test.go new file mode 100644 index 000000000..71bf2364f --- /dev/null +++ b/internal/connectors/plugins/sharedconfig/polling_period_test.go @@ -0,0 +1,33 @@ +package sharedconfig_test + +import ( + "testing" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/sharedconfig" +) + +func TestParsePollingPeriod(t *testing.T) { + cases := []struct { + raw string + def, min, want time.Duration + wantErr bool + }{ + {"", 30 * time.Minute, 20 * time.Minute, 30 * time.Minute, false}, + {"15m", 30 * time.Minute, 20 * time.Minute, 20 * time.Minute, false}, + {"45m", 30 * time.Minute, 20 * time.Minute, 45 * time.Minute, false}, + {"not-a-duration", 30 * time.Minute, 20 * time.Minute, 0, true}, + } + for _, c := range cases { + got, err := sharedconfig.NewPollingPeriod(c.raw, c.def, c.min) + if c.wantErr && err == nil { + t.Fatalf("expected error for value %s", c.raw) + } + if !c.wantErr && err != nil { + t.Fatalf("unexpected error for value %s: %v", c.raw, err) + } + if !c.wantErr && got.Duration() != c.want { + t.Fatalf("unexpected result for value %s, got %v, want %v", c.raw, got, c.want) + } + } +} diff --git a/internal/models/config.go b/internal/models/config.go index 116cabfd4..5d6991eca 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -8,7 +8,7 @@ import ( ) const ( - defaultPollingPeriod = 2 * time.Minute + defaultPollingPeriod = 30 * time.Minute defaultPageSize = 25 ) @@ -17,9 +17,10 @@ const ( type PluginInternalConfig interface{} // Config is the generic configuration that all connectors share +// Note that the PollingPeriod defined here is often overwritten by the connector-specific configuration type Config struct { Name string `json:"name" validate:"required,gte=3,lte=500"` - PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=30000000000,lte=86400000000000"` // gte=30s lte=1d in ns + PollingPeriod time.Duration `json:"pollingPeriod" validate:"required,gte=1200000000000,lte=86400000000000"` // gte=20mn lte=1d in ns PageSize int `json:"pageSize" validate:"lte=150"` } diff --git a/internal/models/config_test.go b/internal/models/config_test.go index 51c9027cb..730a081b1 100644 --- a/internal/models/config_test.go +++ b/internal/models/config_test.go @@ -16,7 +16,7 @@ func TestValidate(t *testing.T) { t.Run("missing name", func(t *testing.T) { config := models.Config{ - PollingPeriod: 40 * time.Second, + PollingPeriod: 40 * time.Minute, PageSize: 30, } err := config.Validate() @@ -76,7 +76,7 @@ func TestValidate(t *testing.T) { t.Run(name, func(t *testing.T) { config := models.Config{ Name: "test", - PollingPeriod: time.Minute, + PollingPeriod: 20 * time.Minute, PageSize: c.val, } err := config.Validate() @@ -93,7 +93,7 @@ func TestValidate(t *testing.T) { t.Run("valid config", func(t *testing.T) { config := models.Config{ Name: "test", - PollingPeriod: 30 * time.Second, + PollingPeriod: 30 * time.Minute, } err := config.Validate() // Then @@ -164,7 +164,7 @@ func TestConfigUnmarshalJSON(t *testing.T) { require.NoError(t, err) assert.Equal(t, "test-config", config.Name) - assert.Equal(t, 2*time.Minute, config.PollingPeriod) // Default value + assert.Equal(t, 30*time.Minute, config.PollingPeriod) // Default value assert.Equal(t, 50, config.PageSize) }) @@ -227,7 +227,7 @@ func TestDefaultConfig(t *testing.T) { config := models.DefaultConfig() - assert.Equal(t, 2*time.Minute, config.PollingPeriod) + assert.Equal(t, 30*time.Minute, config.PollingPeriod) assert.Equal(t, 25, config.PageSize) assert.Empty(t, config.Name) } diff --git a/openapi.yaml b/openapi.yaml index 7dfec73b7..8a46d156c 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5746,7 +5746,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Atlar @@ -5776,7 +5775,6 @@ components: type: string pollingPeriod: type: string - default: 2m provider: type: string default: Bankingcircle @@ -5804,7 +5802,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Column @@ -5874,7 +5871,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Generic @@ -5897,7 +5893,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Increase @@ -5924,7 +5919,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Mangopay @@ -5949,7 +5943,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Modulr @@ -5974,7 +5967,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Moneycorp @@ -6057,7 +6049,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Qonto @@ -6078,7 +6069,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Stripe @@ -6123,7 +6113,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Wise diff --git a/openapi/v3/v3-connectors-config.yaml b/openapi/v3/v3-connectors-config.yaml index 346eca818..a8e32cef8 100644 --- a/openapi/v3/v3-connectors-config.yaml +++ b/openapi/v3/v3-connectors-config.yaml @@ -86,7 +86,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Atlar @@ -116,7 +115,6 @@ components: type: string pollingPeriod: type: string - default: 2m provider: type: string default: Bankingcircle @@ -144,7 +142,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Column @@ -214,7 +211,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Generic @@ -237,7 +233,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Increase @@ -264,7 +259,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Mangopay @@ -289,7 +283,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Modulr @@ -314,7 +307,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Moneycorp @@ -397,7 +389,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Qonto @@ -418,7 +409,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Stripe @@ -463,7 +453,6 @@ components: default: 25 pollingPeriod: type: string - default: 2m provider: type: string default: Wise diff --git a/pkg/client/.speakeasy/gen.lock b/pkg/client/.speakeasy/gen.lock index acbe5986e..bd80eebd0 100644 --- a/pkg/client/.speakeasy/gen.lock +++ b/pkg/client/.speakeasy/gen.lock @@ -1,7 +1,7 @@ lockVersion: 2.0.0 id: 1fa8a26f-45d9-44b7-8b97-fbeebcdcd8b1 management: - docChecksum: 2d54f05b4486002d50d64c242f33daa2 + docChecksum: 71facf2180596f678f821f4bef836873 docVersion: v1 speakeasyVersion: 1.525.0 generationVersion: 2.562.2 diff --git a/pkg/client/models/components/v3atlarconfig.go b/pkg/client/models/components/v3atlarconfig.go index 029c60172..c39825509 100644 --- a/pkg/client/models/components/v3atlarconfig.go +++ b/pkg/client/models/components/v3atlarconfig.go @@ -11,7 +11,7 @@ type V3AtlarConfig struct { BaseURL string `json:"baseUrl"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Atlar" json:"provider"` Secret string `json:"secret"` } diff --git a/pkg/client/models/components/v3bankingcircleconfig.go b/pkg/client/models/components/v3bankingcircleconfig.go index 320351127..b0a400d32 100644 --- a/pkg/client/models/components/v3bankingcircleconfig.go +++ b/pkg/client/models/components/v3bankingcircleconfig.go @@ -12,7 +12,7 @@ type V3BankingcircleConfig struct { Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` Password string `json:"password"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Bankingcircle" json:"provider"` UserCertificate string `json:"userCertificate"` UserCertificateKey string `json:"userCertificateKey"` diff --git a/pkg/client/models/components/v3columnconfig.go b/pkg/client/models/components/v3columnconfig.go index 7961cac05..0245e4b17 100644 --- a/pkg/client/models/components/v3columnconfig.go +++ b/pkg/client/models/components/v3columnconfig.go @@ -11,7 +11,7 @@ type V3ColumnConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Column" json:"provider"` } diff --git a/pkg/client/models/components/v3genericconfig.go b/pkg/client/models/components/v3genericconfig.go index 3d6cc5953..a36284ec2 100644 --- a/pkg/client/models/components/v3genericconfig.go +++ b/pkg/client/models/components/v3genericconfig.go @@ -11,7 +11,7 @@ type V3GenericConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Generic" json:"provider"` } diff --git a/pkg/client/models/components/v3increaseconfig.go b/pkg/client/models/components/v3increaseconfig.go index 0a8ab9571..d6dbed6c5 100644 --- a/pkg/client/models/components/v3increaseconfig.go +++ b/pkg/client/models/components/v3increaseconfig.go @@ -11,7 +11,7 @@ type V3IncreaseConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Increase" json:"provider"` WebhookSharedSecret string `json:"webhookSharedSecret"` } diff --git a/pkg/client/models/components/v3mangopayconfig.go b/pkg/client/models/components/v3mangopayconfig.go index 381fd08da..3957ea326 100644 --- a/pkg/client/models/components/v3mangopayconfig.go +++ b/pkg/client/models/components/v3mangopayconfig.go @@ -12,7 +12,7 @@ type V3MangopayConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Mangopay" json:"provider"` } diff --git a/pkg/client/models/components/v3modulrconfig.go b/pkg/client/models/components/v3modulrconfig.go index 82065ee90..2a7c18a1a 100644 --- a/pkg/client/models/components/v3modulrconfig.go +++ b/pkg/client/models/components/v3modulrconfig.go @@ -12,7 +12,7 @@ type V3ModulrConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Modulr" json:"provider"` } diff --git a/pkg/client/models/components/v3moneycorpconfig.go b/pkg/client/models/components/v3moneycorpconfig.go index 5c2246a92..b29a82b8e 100644 --- a/pkg/client/models/components/v3moneycorpconfig.go +++ b/pkg/client/models/components/v3moneycorpconfig.go @@ -12,7 +12,7 @@ type V3MoneycorpConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Moneycorp" json:"provider"` } diff --git a/pkg/client/models/components/v3qontoconfig.go b/pkg/client/models/components/v3qontoconfig.go index 94c74d009..b752890a7 100644 --- a/pkg/client/models/components/v3qontoconfig.go +++ b/pkg/client/models/components/v3qontoconfig.go @@ -12,7 +12,7 @@ type V3QontoConfig struct { Endpoint string `json:"endpoint"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Qonto" json:"provider"` StagingToken *string `json:"stagingToken,omitempty"` } diff --git a/pkg/client/models/components/v3stripeconfig.go b/pkg/client/models/components/v3stripeconfig.go index f26482be2..a09bf1586 100644 --- a/pkg/client/models/components/v3stripeconfig.go +++ b/pkg/client/models/components/v3stripeconfig.go @@ -10,7 +10,7 @@ type V3StripeConfig struct { APIKey string `json:"apiKey"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Stripe" json:"provider"` } diff --git a/pkg/client/models/components/v3wiseconfig.go b/pkg/client/models/components/v3wiseconfig.go index 198505c00..ee2f88733 100644 --- a/pkg/client/models/components/v3wiseconfig.go +++ b/pkg/client/models/components/v3wiseconfig.go @@ -10,7 +10,7 @@ type V3WiseConfig struct { APIKey string `json:"apiKey"` Name string `json:"name"` PageSize *int64 `default:"25" json:"pageSize"` - PollingPeriod *string `default:"2m" json:"pollingPeriod"` + PollingPeriod *string `json:"pollingPeriod,omitempty"` Provider *string `default:"Wise" json:"provider"` WebhookPublicKey string `json:"webhookPublicKey"` } diff --git a/test/e2e/api_connectors_test.go b/test/e2e/api_connectors_test.go index 70f00a41a..cc839d8be 100644 --- a/test/e2e/api_connectors_test.go +++ b/test/e2e/api_connectors_test.go @@ -77,8 +77,20 @@ var _ = Context("Payments API Connectors", Serial, func() { getRes, err := app.GetValue().SDK().Payments.V3.GetConnectorConfig(ctx, connectorID) Expect(err).To(BeNil()) Expect(getRes.V3GetConnectorConfigResponse).NotTo(BeNil()) - Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig).To(Equal(connectorConf)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Name).To(Equal(connectorConf.Name)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Provider).To(Equal(connectorConf.Provider)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.Directory).To(Equal(connectorConf.Directory)) + Expect(getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.PageSize).To(Equal(connectorConf.PageSize)) Expect(getRes.V3GetConnectorConfigResponse.Data.Type).To(Equal(components.V3ConnectorConfigTypeDummypay)) + + getResPollingPeriod, err := time.ParseDuration( + *getRes.V3GetConnectorConfigResponse.Data.V3DummypayConfig.PollingPeriod, + ) + Expect(err).To(BeNil()) + configPollingPeriod, err := time.ParseDuration(*connectorConf.PollingPeriod) + Expect(err).To(BeNil()) + + Expect(getResPollingPeriod).To(Equal(configPollingPeriod)) }) It("can install a connector with v2 API", func() { @@ -91,7 +103,7 @@ var _ = Context("Payments API Connectors", Serial, func() { connectorConf := components.ConnectorConfig{ DummyPayConfig: &components.DummyPayConfig{ Name: fmt.Sprintf("connector-%s", id.String()), - FilePollingPeriod: pointer.For("30s"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, }, @@ -163,7 +175,7 @@ var _ = Context("Payments API Connectors", Serial, func() { config := components.ConnectorConfig{ DummyPayConfig: &components.DummyPayConfig{ Name: "some name", - FilePollingPeriod: pointer.For("2m"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, }, @@ -186,7 +198,7 @@ var _ = Context("Payments API Connectors", Serial, func() { Expect(err).To(BeNil()) blockTillWorkflowComplete(ctx, connectorID, "run-tasks-") - config.PollingPeriod = pointer.For("2m0s") + config.PollingPeriod = pointer.For("30m0s") _, err = app.GetValue().SDK().Payments.V3.V3UpdateConnectorConfig(ctx, connectorID, &components.V3UpdateConnectorRequest{ V3DummypayConfig: config, }) @@ -569,7 +581,7 @@ func newV3ConnectorConfigFn() func(id uuid.UUID) *components.V3DummypayConfig { LinkFlowError: pointer.For(false), Name: fmt.Sprintf("connector-%s", id.String()), PageSize: pointer.For(int64(30)), - PollingPeriod: pointer.For("30s"), + PollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), UpdateLinkFlowError: pointer.For(false), } @@ -586,7 +598,7 @@ func newV2ConnectorConfigFn() func(id uuid.UUID) *components.DummyPayConfig { return &components.DummyPayConfig{ Name: fmt.Sprintf("connector-%s", id.String()), - FilePollingPeriod: pointer.For("30s"), + FilePollingPeriod: pointer.For("30m"), Provider: pointer.For("Dummypay"), Directory: dir, } diff --git a/tools/compile-configs/main.go b/tools/compile-configs/main.go index a2ae46a54..4bff660d6 100644 --- a/tools/compile-configs/main.go +++ b/tools/compile-configs/main.go @@ -159,6 +159,9 @@ func readConfig(name string, caserName string) (V3Config, error) { } name := "" + if field.Tag == nil { + continue + } tagValue := strings.Trim(field.Tag.Value, "`") arr := strings.Split(tagValue, " ") for _, tag := range arr { @@ -170,17 +173,44 @@ func readConfig(name string, caserName string) (V3Config, error) { switch fields[0] { case "json": name = strings.Trim(fields[1], "\"") - typ := field.Type.(*ast.Ident).Name + // Determine the field type name supporting identifiers, selectors (e.g., time.Duration), and pointers + var typName string + switch t := field.Type.(type) { + case *ast.Ident: + typName = t.Name + case *ast.SelectorExpr: + // Qualified type like pkg.Type -> use the selected identifier + typName = t.Sel.Name + case *ast.StarExpr: + // Pointer to something + switch x := t.X.(type) { + case *ast.Ident: + typName = x.Name + case *ast.SelectorExpr: + typName = x.Sel.Name + default: + return V3Config{}, fmt.Errorf("unsupported type expr: %T", field.Type) + } + default: + return V3Config{}, fmt.Errorf("unsupported type expr: %T", field.Type) + } + fieldType := "" - switch typ { + switch typName { case "string": fieldType = "string" case "int", "int32", "int64", "uint32", "uint64": fieldType = "integer" case "bool": fieldType = "boolean" + case "Duration": + // time.Duration is represented as a string in JSON/schema + fieldType = "string" + case "PollingPeriod": + // sharedconfig.PollingPeriod is represented as a string in JSON/schema + fieldType = "string" default: - return V3Config{}, fmt.Errorf("invalid type: %s", typ) + return V3Config{}, fmt.Errorf("invalid type: %s", typName) } properties[name] = Property{ Type: fieldType,