From 5e7271dedf9cd10026e2b1b7a0167b0b8b805c6d Mon Sep 17 00:00:00 2001 From: vanitabhagwat <92561664+vanitabhagwat@users.noreply.github.com> Date: Mon, 26 Feb 2024 08:07:36 -0800 Subject: [PATCH] feat: adding client id for tracking requests to registry (#84) * feat: adding client id for tracking requests to registry --------- Co-authored-by: vbhagwat --- go/internal/feast/featurestore.go | 15 +- .../onlinestore/sqliteonlinestore_test.go | 3 +- go/internal/feast/registry/http.go | 11 +- go/internal/feast/registry/http_test.go | 455 ++++++++++++++++++ go/internal/feast/registry/repoconfig.go | 32 +- go/internal/feast/registry/repoconfig_test.go | 130 ++++- 6 files changed, 619 insertions(+), 27 deletions(-) create mode 100644 go/internal/feast/registry/http_test.go diff --git a/go/internal/feast/featurestore.go b/go/internal/feast/featurestore.go index 650cebfc23..9c5e1a9f63 100644 --- a/go/internal/feast/featurestore.go +++ b/go/internal/feast/featurestore.go @@ -49,8 +49,11 @@ func NewFeatureStore(config *registry.RepoConfig, callback transformation.Transf if err != nil { return nil, err } - - registry, err := registry.NewRegistry(config.GetRegistryConfig(), config.RepoPath, config.Project) + registryConfig, err := config.GetRegistryConfig() + if err != nil { + return nil, err + } + registry, err := registry.NewRegistry(registryConfig, config.RepoPath, config.Project) if err != nil { return nil, err } @@ -58,10 +61,10 @@ func NewFeatureStore(config *registry.RepoConfig, callback transformation.Transf if err != nil { return nil, err } - sanitizedProjectName := strings.Replace(config.Project, "_", "-", -1) - productName := os.Getenv("PRODUCT") - endpoint := fmt.Sprintf("%s-transformations.%s.svc.cluster.local:80", sanitizedProjectName, productName) - transformationService, _ := transformation.NewGrpcTransformationService(config, endpoint) + sanitizedProjectName := strings.Replace(config.Project, "_", "-", -1) + productName := os.Getenv("PRODUCT") + endpoint := fmt.Sprintf("%s-transformations.%s.svc.cluster.local:80", sanitizedProjectName, productName) + transformationService, _ := transformation.NewGrpcTransformationService(config, endpoint) return &FeatureStore{ config: config, diff --git a/go/internal/feast/onlinestore/sqliteonlinestore_test.go b/go/internal/feast/onlinestore/sqliteonlinestore_test.go index 9a56f4df1a..929af6d16b 100644 --- a/go/internal/feast/onlinestore/sqliteonlinestore_test.go +++ b/go/internal/feast/onlinestore/sqliteonlinestore_test.go @@ -21,9 +21,10 @@ func TestSqliteAndFeatureRepoSetup(t *testing.T) { err := test.SetupCleanFeatureRepo(dir) assert.Nil(t, err) config, err := registry.NewRepoConfigFromFile(feature_repo_path) + registryConfig, err := config.GetRegistryConfig() assert.Nil(t, err) assert.Equal(t, "my_project", config.Project) - assert.Equal(t, "data/registry.db", config.GetRegistryConfig().Path) + assert.Equal(t, "data/registry.db", registryConfig.Path) assert.Equal(t, "local", config.Provider) assert.Equal(t, map[string]interface{}{ "path": "data/online_store.db", diff --git a/go/internal/feast/registry/http.go b/go/internal/feast/registry/http.go index 8d0150887f..d1d69c1b3a 100644 --- a/go/internal/feast/registry/http.go +++ b/go/internal/feast/registry/http.go @@ -3,19 +3,18 @@ package registry import ( "crypto/tls" "fmt" + "github.com/feast-dev/feast/go/protos/feast/core" + "github.com/rs/zerolog/log" + "google.golang.org/protobuf/proto" "io" "net/http" "time" - - "google.golang.org/protobuf/proto" - - "github.com/feast-dev/feast/go/protos/feast/core" - "github.com/rs/zerolog/log" ) type HttpRegistryStore struct { project string endpoint string + clientId string client http.Client } @@ -38,6 +37,7 @@ func NewHttpRegistryStore(config *RegistryConfig, project string) (*HttpRegistry hrs := &HttpRegistryStore{ project: project, endpoint: config.Path, + clientId: config.ClientId, client: http.Client{ Transport: tr, Timeout: 5 * time.Second, @@ -72,6 +72,7 @@ func (r *HttpRegistryStore) makeHttpRequest(url string) (*http.Response, error) } req.Header.Add("Accept", "application/x-protobuf") + req.Header.Add("Client-Id", r.clientId) resp, err := r.client.Do(req) if err != nil { diff --git a/go/internal/feast/registry/http_test.go b/go/internal/feast/registry/http_test.go new file mode 100644 index 0000000000..56660c3ca2 --- /dev/null +++ b/go/internal/feast/registry/http_test.go @@ -0,0 +1,455 @@ +package registry + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/feast-dev/feast/go/protos/feast/core" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/proto" +) + +func TestMakeHttpRequestReturnsResponseOnSuccess(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + } + + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Call the method under test + resp, err := store.makeHttpRequest(server.URL) + + // Assert that there was no error and the response status is OK + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestMakeHttpRequestReturnsErrorOnInvalidUrl(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + } + + // Call the method under test with an invalid URL + resp, err := store.makeHttpRequest("http://invalid_url") + + // Assert that there was an error + assert.NotNil(t, err) + assert.Nil(t, resp) +} + +func TestMakeHttpRequestReturnsErrorOnNonOkStatus(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + } + + // Create a mock HTTP server that returns a status code other than OK + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + // Call the method under test + resp, err := store.makeHttpRequest(server.URL) + + // Assert that there was an error + assert.NotNil(t, err) + // If resp is not nil, then check the StatusCode + if resp != nil { + assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) + } +} +func TestMakeHttpRequestIncludesClientId(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + clientId: "test_client_id", + } + + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Assert that the request includes the correct Client-Id header + assert.Equal(t, "test_client_id", r.Header.Get("Client-Id")) + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Call the method under test + _, err := store.makeHttpRequest(server.URL) + + // Assert that there was no error + assert.Nil(t, err) +} +func TestLoadProtobufMessages(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Write some data to the response + w.Write([]byte("test_data")) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a messageProcessor that checks the data it receives + messageProcessor := func(data []byte) error { + if string(data) != "test_data" { + return errors.New("messageProcessor received unexpected data") + } + return nil + } + + // Call the method under test + err := store.loadProtobufMessages(server.URL+"/test_path", messageProcessor) + + // Assert that there was no error + assert.Nil(t, err) +} + +func TestLoadProtobufMessages_Error(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Write some data to the response + w.Write([]byte("test_data")) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a messageProcessor that always returns an error + messageProcessor := func(data []byte) error { + return errors.New("test error") + } + + // Call the method under test + err := store.loadProtobufMessages(server.URL+"/test_path", messageProcessor) + + // Assert that the method returns the error from the messageProcessor + assert.NotNil(t, err) + assert.Equal(t, "test error", err.Error()) +} + +func TestLoadEntities(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a dummy EntityList + entityList := &core.EntityList{ + Entities: []*core.Entity{ + {Spec: &core.EntitySpecV2{Name: "test_entity"}}, + }, + } + // Marshal it to protobuf + data, err := proto.Marshal(entityList) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal EntityList") + } + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.loadEntities(registry) + + // Assert that there was no error and that the registry now contains the entity + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.Entities)) + assert.Equal(t, "test_entity", registry.Entities[0].Spec.Name) +} + +func TestLoadDatasources(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a dummy DataSourceList + dataSourceList := &core.DataSourceList{ + Datasources: []*core.DataSource{ + {Name: "test_datasource"}, + }, + } + // Marshal it to protobuf + data, err := proto.Marshal(dataSourceList) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal DataSourceList") + } + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.loadDatasources(registry) + + // Assert that there was no error and that the registry now contains the datasource + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.DataSources)) + assert.Equal(t, "test_datasource", registry.DataSources[0].Name) +} + +func TestLoadFeatureViews(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a dummy FeatureViewList + featureViewList := &core.FeatureViewList{ + Featureviews: []*core.FeatureView{ + {Spec: &core.FeatureViewSpec{Name: "test_feature_view"}}, + }, + } + // Marshal it to protobuf + data, err := proto.Marshal(featureViewList) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal FeatureViewList") + } + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.loadFeatureViews(registry) + + // Assert that there was no error and that the registry now contains the feature view + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.FeatureViews)) + assert.Equal(t, "test_feature_view", registry.FeatureViews[0].Spec.Name) +} + +func TestLoadOnDemandFeatureViews(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a dummy OnDemandFeatureViewList + odFeatureViewList := &core.OnDemandFeatureViewList{ + Ondemandfeatureviews: []*core.OnDemandFeatureView{ + {Spec: &core.OnDemandFeatureViewSpec{Name: "test_view"}}, + }, + } + // Marshal it to protobuf + data, err := proto.Marshal(odFeatureViewList) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal OnDemandFeatureViewList") + } + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.loadOnDemandFeatureViews(registry) + + // Assert that there was no error and that the registry now contains the on-demand feature view + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.OnDemandFeatureViews)) + assert.Equal(t, "test_view", registry.OnDemandFeatureViews[0].Spec.Name) +} + +func TestLoadFeatureServices(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Create a dummy FeatureServiceList + featureServiceList := &core.FeatureServiceList{ + Featureservices: []*core.FeatureService{ + {Spec: &core.FeatureServiceSpec{Name: "test_feature_service"}}, + }, + } + // Marshal it to protobuf + data, err := proto.Marshal(featureServiceList) + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal FeatureServiceList") + } + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.loadFeatureServices(registry) + + // Assert that there was no error and that the registry now contains the feature service + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.FeatureServices)) + assert.Equal(t, "test_feature_service", registry.FeatureServices[0].Spec.Name) +} + +func TestGetRegistryProto(t *testing.T) { + // Create a mock HTTP server + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var data []byte + var err error + + switch r.URL.Path { + case "/projects/test_project/entities": + entityList := &core.EntityList{ + Entities: []*core.Entity{ + {Spec: &core.EntitySpecV2{Name: "test_entity"}}, + }, + } + data, err = proto.Marshal(entityList) + case "/projects/test_project/data_sources": + dataSourceList := &core.DataSourceList{ + Datasources: []*core.DataSource{ + {Name: "test_datasource"}, + }, + } + data, err = proto.Marshal(dataSourceList) + case "/projects/test_project/feature_views": + featureViewList := &core.FeatureViewList{ + Featureviews: []*core.FeatureView{ + {Spec: &core.FeatureViewSpec{Name: "test_feature_view"}}, + }, + } + data, err = proto.Marshal(featureViewList) + case "/projects/test_project/on_demand_feature_views": + odFeatureViewList := &core.OnDemandFeatureViewList{ + Ondemandfeatureviews: []*core.OnDemandFeatureView{ + {Spec: &core.OnDemandFeatureViewSpec{Name: "test_view"}}, + }, + } + data, err = proto.Marshal(odFeatureViewList) + case "/projects/test_project/feature_services": + featureServiceList := &core.FeatureServiceList{ + Featureservices: []*core.FeatureService{ + {Spec: &core.FeatureServiceSpec{Name: "test_feature_service"}}, + }, + } + data, err = proto.Marshal(featureServiceList) + default: + t.Fatalf("Unexpected path: %s", r.URL.Path) + } + + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal list") + } + + // Write the protobuf data to the response + w.Write(data) + })) + // Close the server when test finishes + defer server.Close() + + // Create a new HttpRegistryStore with the mock server's URL as the endpoint + store := &HttpRegistryStore{ + endpoint: server.URL, + project: "test_project", + } + + // Call the method under test + registry, err := store.GetRegistryProto() + + // Assert that there was no error and that the registry now contains the expected data + assert.Nil(t, err) + assert.Equal(t, 1, len(registry.Entities)) + assert.Equal(t, "test_entity", registry.Entities[0].Spec.Name) + assert.Equal(t, 1, len(registry.DataSources)) + assert.Equal(t, "test_datasource", registry.DataSources[0].Name) + assert.Equal(t, 1, len(registry.FeatureViews)) + assert.Equal(t, "test_feature_view", registry.FeatureViews[0].Spec.Name) + assert.Equal(t, 1, len(registry.OnDemandFeatureViews)) + assert.Equal(t, "test_view", registry.OnDemandFeatureViews[0].Spec.Name) + assert.Equal(t, 1, len(registry.FeatureServices)) + assert.Equal(t, "test_feature_service", registry.FeatureServices[0].Spec.Name) +} + +func TestUpdateRegistryProto(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + } + + // Create a new empty Registry + registry := &core.Registry{} + + // Call the method under test + err := store.UpdateRegistryProto(registry) + + // Assert that the method returns a NotImplementedError + assert.NotNil(t, err) + assert.IsType(t, &NotImplementedError{}, err) + assert.Equal(t, "UpdateRegistryProto", err.(*NotImplementedError).FunctionName) +} +func TestTeardown(t *testing.T) { + // Create a new HttpRegistryStore + store := &HttpRegistryStore{ + endpoint: "http://localhost", + project: "test_project", + } + + // Call the method under test + err := store.Teardown() + + // Assert that the method returns a NotImplementedError + assert.NotNil(t, err) + assert.IsType(t, &NotImplementedError{}, err) + assert.Equal(t, "Teardown", err.(*NotImplementedError).FunctionName) +} diff --git a/go/internal/feast/registry/repoconfig.go b/go/internal/feast/registry/repoconfig.go index c878928e6a..2b140ad5da 100644 --- a/go/internal/feast/registry/repoconfig.go +++ b/go/internal/feast/registry/repoconfig.go @@ -2,6 +2,7 @@ package registry import ( "encoding/json" + "fmt" "io/ioutil" "path/filepath" @@ -9,7 +10,8 @@ import ( ) const ( - defaultCacheTtlSeconds = 600 + defaultCacheTtlSeconds = int64(600) + defaultClientID = "Unknown" ) type RepoConfig struct { @@ -37,6 +39,7 @@ type RepoConfig struct { type RegistryConfig struct { RegistryStoreType string `json:"registry_store_type"` Path string `json:"path"` + ClientId string `json:"client_id" default:"Unknown"` CacheTtlSeconds int64 `json:"cache_ttl_seconds" default:"600"` } @@ -74,9 +77,9 @@ func NewRepoConfigFromFile(repoPath string) (*RepoConfig, error) { return &config, nil } -func (r *RepoConfig) GetRegistryConfig() *RegistryConfig { +func (r *RepoConfig) GetRegistryConfig() (*RegistryConfig, error) { if registryConfigMap, ok := r.Registry.(map[string]interface{}); ok { - registryConfig := RegistryConfig{CacheTtlSeconds: defaultCacheTtlSeconds} + registryConfig := RegistryConfig{CacheTtlSeconds: defaultCacheTtlSeconds, ClientId: defaultClientID} for k, v := range registryConfigMap { switch k { case "path": @@ -87,23 +90,28 @@ func (r *RepoConfig) GetRegistryConfig() *RegistryConfig { if value, ok := v.(string); ok { registryConfig.RegistryStoreType = value } + case "client_id": + if value, ok := v.(string); ok { + registryConfig.ClientId = value + } case "cache_ttl_seconds": // cache_ttl_seconds defaulted to type float64. Ex: "cache_ttl_seconds": 60 in registryConfigMap - if value, ok := v.(float64); ok { + switch value := v.(type) { + case float64: registryConfig.CacheTtlSeconds = int64(value) - } - - if value, ok := v.(int32); ok { + case int: registryConfig.CacheTtlSeconds = int64(value) - } - - if value, ok := v.(int64); ok { + case int32: + registryConfig.CacheTtlSeconds = int64(value) + case int64: registryConfig.CacheTtlSeconds = value + default: + return nil, fmt.Errorf("unexpected type %T for CacheTtlSeconds", v) } } } - return ®istryConfig + return ®istryConfig, nil } else { - return &RegistryConfig{Path: r.Registry.(string), CacheTtlSeconds: defaultCacheTtlSeconds} + return &RegistryConfig{Path: r.Registry.(string), ClientId: defaultClientID, CacheTtlSeconds: defaultCacheTtlSeconds}, nil } } diff --git a/go/internal/feast/registry/repoconfig_test.go b/go/internal/feast/registry/repoconfig_test.go index 848977886c..540ffd0b6c 100644 --- a/go/internal/feast/registry/repoconfig_test.go +++ b/go/internal/feast/registry/repoconfig_test.go @@ -1,8 +1,10 @@ package registry import ( + "fmt" "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -26,10 +28,11 @@ online_store: err = os.WriteFile(filePath, data, 0666) assert.Nil(t, err) config, err := NewRepoConfigFromFile(dir) + registryConfig, err := config.GetRegistryConfig() assert.Nil(t, err) assert.Equal(t, "feature_repo", config.Project) assert.Equal(t, dir, config.RepoPath) - assert.Equal(t, "data/registry.db", config.GetRegistryConfig().Path) + assert.Equal(t, "data/registry.db", registryConfig.Path) assert.Equal(t, "local", config.Provider) assert.Equal(t, map[string]interface{}{ "type": "redis", @@ -50,6 +53,7 @@ func TestNewRepoConfigRegistryMap(t *testing.T) { data := []byte(` registry: path: data/registry.db + client_id: "test_client_id" project: feature_repo provider: local online_store: @@ -59,10 +63,12 @@ online_store: err = os.WriteFile(filePath, data, 0666) assert.Nil(t, err) config, err := NewRepoConfigFromFile(dir) + registryConfig, err := config.GetRegistryConfig() assert.Nil(t, err) assert.Equal(t, "feature_repo", config.Project) assert.Equal(t, dir, config.RepoPath) - assert.Equal(t, "data/registry.db", config.GetRegistryConfig().Path) + assert.Equal(t, "data/registry.db", registryConfig.Path) + assert.Equal(t, "test_client_id", registryConfig.ClientId) assert.Equal(t, "local", config.Provider) assert.Equal(t, map[string]interface{}{ "type": "redis", @@ -83,6 +89,7 @@ func TestNewRepoConfigRegistryConfig(t *testing.T) { data := []byte(` registry: path: data/registry.db + client_id: "test_client_id" project: feature_repo provider: local online_store: @@ -92,7 +99,124 @@ online_store: err = os.WriteFile(filePath, data, 0666) assert.Nil(t, err) config, err := NewRepoConfigFromFile(dir) + registryConfig, err := config.GetRegistryConfig() assert.Nil(t, err) assert.Equal(t, dir, config.RepoPath) - assert.Equal(t, "data/registry.db", config.GetRegistryConfig().Path) + assert.Equal(t, "data/registry.db", registryConfig.Path) + assert.Equal(t, "test_client_id", registryConfig.ClientId) +} +func TestNewRepoConfigFromJSON(t *testing.T) { + // Create a temporary directory for the test + dir, err := os.MkdirTemp("", "feature_repo_*") + assert.Nil(t, err) + defer func() { + assert.Nil(t, os.RemoveAll(dir)) + }() + + // Define a JSON string for the test + registry_path := filepath.Join(dir, "data/registry.db") + + configJSON := `{ + "project": "feature_repo", + "registry": "$REGISTRY_PATH", + "provider": "local", + "online_store": { + "type": "redis", + "connection_string": "localhost:6379" + } + }` + + replacements := map[string]string{ + "$REGISTRY_PATH": registry_path, + } + + // Replace the variables in the JSON string + for variable, replacement := range replacements { + configJSON = strings.ReplaceAll(configJSON, variable, replacement) + } + + // Call the function under test + config, err := NewRepoConfigFromJSON(dir, configJSON) + registryConfig, err := config.GetRegistryConfig() + // Assert that there was no error and that the config was correctly parsed + assert.Nil(t, err) + assert.Equal(t, "feature_repo", config.Project) + assert.Equal(t, filepath.Join(dir, "data/registry.db"), registryConfig.Path) + assert.Equal(t, "local", config.Provider) + assert.Equal(t, map[string]interface{}{ + "type": "redis", + "connection_string": "localhost:6379", + }, config.OnlineStore) + assert.Empty(t, config.OfflineStore) + assert.Empty(t, config.FeatureServer) + assert.Empty(t, config.Flags) +} + +func TestGetRegistryConfig_Map(t *testing.T) { + // Create a RepoConfig with a map Registry + config := &RepoConfig{ + Registry: map[string]interface{}{ + "path": "data/registry.db", + "registry_store_type": "local", + "client_id": "test_client_id", + "cache_ttl_seconds": 60, + }, + } + + // Call the method under test + registryConfig, _ := config.GetRegistryConfig() + + fmt.Println(registryConfig) + + // Assert that the method correctly processed the map + assert.Equal(t, "data/registry.db", registryConfig.Path) + assert.Equal(t, "local", registryConfig.RegistryStoreType) + assert.Equal(t, int64(60), registryConfig.CacheTtlSeconds) + assert.Equal(t, "test_client_id", registryConfig.ClientId) +} + +func TestGetRegistryConfig_String(t *testing.T) { + // Create a RepoConfig with a string Registry + config := &RepoConfig{ + Registry: "data/registry.db", + } + + // Call the method under test + registryConfig, _ := config.GetRegistryConfig() + + // Assert that the method correctly processed the string + assert.Equal(t, "data/registry.db", registryConfig.Path) + assert.Equal(t, defaultClientID, registryConfig.ClientId) + println(registryConfig.CacheTtlSeconds) + assert.Empty(t, registryConfig.RegistryStoreType) + assert.Equal(t, defaultCacheTtlSeconds, registryConfig.CacheTtlSeconds) +} + +func TestGetRegistryConfig_CacheTtlSecondsTypes(t *testing.T) { + // Create RepoConfigs with different types for cache_ttl_seconds + configs := []*RepoConfig{ + { + Registry: map[string]interface{}{ + "cache_ttl_seconds": float64(60), + }, + }, + { + Registry: map[string]interface{}{ + "cache_ttl_seconds": int32(60), + }, + }, + { + Registry: map[string]interface{}{ + "cache_ttl_seconds": int64(60), + }, + }, + } + + for _, config := range configs { + // Call the method under test + registryConfig, _ := config.GetRegistryConfig() + + // Assert that the method correctly processed cache_ttl_seconds + assert.Equal(t, int64(60), registryConfig.CacheTtlSeconds) + } }