diff --git a/pkg/datastore/client.go b/pkg/datastore/client.go index 37a8462..757197c 100644 --- a/pkg/datastore/client.go +++ b/pkg/datastore/client.go @@ -49,36 +49,34 @@ var ( } ) -// Config holds datastore client configuration. -type Config struct { - // AuthConfig is passed to the auth package for authentication. - // Can be nil to use defaults. - AuthConfig *auth.Config - - // APIURL is the base URL for the Datastore API. - // Defaults to production if empty. - APIURL string -} +// ClientOption is an option for a Datastore client. +type ClientOption func(*clientOptionsInternal) -// configKey is the key for storing Config in context. -type configKey struct{} +// clientOptionsInternal holds internal client configuration that can be modified by ClientOption. +type clientOptionsInternal struct { + authConfig *auth.Config + logger *slog.Logger + baseURL string +} -// WithConfig returns a new context with the given datastore config. -// This also sets the auth config if provided. -func WithConfig(ctx context.Context, cfg *Config) context.Context { - if cfg.AuthConfig != nil { - ctx = auth.WithConfig(ctx, cfg.AuthConfig) +// WithEndpoint returns a ClientOption that sets the API base URL. +func WithEndpoint(url string) ClientOption { + return func(o *clientOptionsInternal) { + o.baseURL = url } - return context.WithValue(ctx, configKey{}, cfg) } -// getConfig retrieves the datastore config from context, or returns defaults. -func getConfig(ctx context.Context) *Config { - if cfg, ok := ctx.Value(configKey{}).(*Config); ok && cfg != nil { - return cfg +// WithLogger returns a ClientOption that sets the logger. +func WithLogger(logger *slog.Logger) ClientOption { + return func(o *clientOptionsInternal) { + o.logger = logger } - return &Config{ - APIURL: defaultAPIURL, +} + +// WithAuth returns a ClientOption that sets the authentication configuration. +func WithAuth(cfg *auth.Config) ClientOption { + return func(o *clientOptionsInternal) { + o.authConfig = cfg } } @@ -93,37 +91,52 @@ type Client struct { // NewClient creates a new Datastore client. // If projectID is empty, it will be fetched from the GCP metadata server. -// Configuration can be provided via WithConfig in the context. -func NewClient(ctx context.Context, projectID string) (*Client, error) { - return NewClientWithDatabase(ctx, projectID, "") +// Options can be provided to configure the client. +func NewClient(ctx context.Context, projectID string, opts ...ClientOption) (*Client, error) { + return NewClientWithDatabase(ctx, projectID, "", opts...) } // NewClientWithDatabase creates a new Datastore client with a specific database. -// Configuration can be provided via WithConfig in the context. -func NewClientWithDatabase(ctx context.Context, projID, dbID string) (*Client, error) { - logger := slog.Default() - cfg := getConfig(ctx) +// Options can be provided to configure the client. +func NewClientWithDatabase(ctx context.Context, projID, dbID string, opts ...ClientOption) (*Client, error) { + // Apply default internal options + options := &clientOptionsInternal{ + baseURL: defaultAPIURL, + logger: slog.Default(), + } + + // Apply provided options + for _, opt := range opts { + opt(options) + } + // --- Existing NewClientWithDatabase logic starts here --- if projID == "" { + // Inject auth config into context before fetching project ID + fetchCtx := ctx + if options.authConfig != nil { + fetchCtx = auth.WithConfig(ctx, options.authConfig) + } + if !testing.Testing() { - logger.InfoContext(ctx, "project ID not provided, fetching from metadata server") + options.logger.InfoContext(ctx, "project ID not provided, fetching from metadata server") } - pid, err := auth.ProjectID(ctx) + pid, err := auth.ProjectID(fetchCtx) if err != nil { - logger.ErrorContext(ctx, "failed to get project ID from metadata server", "error", err) + options.logger.ErrorContext(ctx, "failed to get project ID from metadata server", "error", err) return nil, fmt.Errorf("project ID required: %w", err) } projID = pid if !testing.Testing() { - logger.InfoContext(ctx, "fetched project ID from metadata server", "project_id", projID) + options.logger.InfoContext(ctx, "fetched project ID from metadata server", "project_id", projID) } } if !testing.Testing() { - logger.InfoContext(ctx, "creating datastore client", "project_id", projID, "database_id", dbID) + options.logger.InfoContext(ctx, "creating datastore client", "project_id", projID, "database_id", dbID) } - baseURL := cfg.APIURL + baseURL := options.baseURL if baseURL == "" { baseURL = defaultAPIURL } @@ -132,8 +145,8 @@ func NewClientWithDatabase(ctx context.Context, projID, dbID string) (*Client, e projectID: projID, databaseID: dbID, baseURL: baseURL, - authConfig: cfg.AuthConfig, - logger: logger, + authConfig: options.authConfig, // Use authConfig from options + logger: options.logger, // Use logger from options }, nil } diff --git a/pkg/datastore/client_test.go b/pkg/datastore/client_test.go index eecec37..bd15d9f 100644 --- a/pkg/datastore/client_test.go +++ b/pkg/datastore/client_test.go @@ -59,16 +59,18 @@ func TestNewClientWithDatabase(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.WithConfig(context.Background(), &datastore.Config{ - APIURL: apiServer.URL, - AuthConfig: &auth.Config{ - MetadataURL: metadataServer.URL, - SkipADC: true, - }, - }) - + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } // Test with explicit databaseID - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "custom-db") + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "custom-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } @@ -77,23 +79,6 @@ func TestNewClientWithDatabase(t *testing.T) { } } -func TestWithConfig(t *testing.T) { - // Test that config can be set in context - cfg := &datastore.Config{ - APIURL: "http://test", - AuthConfig: &auth.Config{ - MetadataURL: "http://metadata", - SkipADC: true, - }, - } - ctx := datastore.WithConfig(context.Background(), cfg) - - // Context should be non-nil - if ctx == nil { - t.Fatal("expected non-nil context") - } -} - func TestNewClientWithDatabaseEmptyProjectID(t *testing.T) { // Setup mock servers metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -131,16 +116,18 @@ func TestNewClientWithDatabaseEmptyProjectID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.WithConfig(context.Background(), &datastore.Config{ - APIURL: apiServer.URL, - AuthConfig: &auth.Config{ - MetadataURL: metadataServer.URL, - SkipADC: true, - }, - }) - + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } // Test with empty projectID - should fetch from metadata - client, err := datastore.NewClientWithDatabase(ctx, "", "my-db") + client, err := datastore.NewClientWithDatabase( + context.Background(), + "", + "my-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase with empty projectID failed: %v", err) } @@ -184,16 +171,18 @@ func TestNewClientWithDatabaseProjectIDFetchFailure(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.WithConfig(context.Background(), &datastore.Config{ - APIURL: apiServer.URL, - AuthConfig: &auth.Config{ - MetadataURL: metadataServer.URL, - SkipADC: true, - }, - }) - + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } // Test with empty projectID and failing metadata server - client, err := datastore.NewClientWithDatabase(ctx, "", "my-db") + client, err := datastore.NewClientWithDatabase( + context.Background(), + "", + "my-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err == nil { t.Fatal("expected error when projectID fetch fails, got nil") } diff --git a/pkg/datastore/entity_coverage_test.go b/pkg/datastore/entity_coverage_test.go index 75b97d8..2bcdae4 100644 --- a/pkg/datastore/entity_coverage_test.go +++ b/pkg/datastore/entity_coverage_test.go @@ -314,10 +314,10 @@ func TestNewClientWithDatabase_Coverage(t *testing.T) { })) defer apiServer.Close() - testCtx := TestConfig(ctx, metadataServer.URL, apiServer.URL) + opts := TestConfig(ctx, metadataServer.URL, apiServer.URL) // Test with empty project (error case) - _, err := NewClientWithDatabase(testCtx, "", "test-db") + _, err := NewClientWithDatabase(ctx, "", "test-db", opts...) if err == nil { t.Error("Expected error for empty project ID, got nil") } diff --git a/pkg/datastore/entity_test.go b/pkg/datastore/entity_test.go index 2a92924..2790dbe 100644 --- a/pkg/datastore/entity_test.go +++ b/pkg/datastore/entity_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -459,9 +460,16 @@ func TestGetWithDecodeError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -469,7 +477,7 @@ func TestGetWithDecodeError(t *testing.T) { key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error with missing properties") } @@ -555,9 +563,16 @@ func TestDecodeValueInvalidInteger(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -565,7 +580,7 @@ func TestDecodeValueInvalidInteger(t *testing.T) { key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error with invalid integer format") } else { @@ -631,9 +646,16 @@ func TestDecodeValueWrongTypeForInteger(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -641,7 +663,7 @@ func TestDecodeValueWrongTypeForInteger(t *testing.T) { key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error with wrong type for integer") } else { @@ -707,9 +729,16 @@ func TestDecodeValueInvalidTimestamp(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -717,7 +746,7 @@ func TestDecodeValueInvalidTimestamp(t *testing.T) { key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error with invalid timestamp format") } else { @@ -786,9 +815,16 @@ func TestGetMultiDecodeError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -799,7 +835,7 @@ func TestGetMultiDecodeError(t *testing.T) { } var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) + err = client.GetMulti(context.Background(), keys, &entities) // Changed ctx to context.Background() if err == nil { t.Error("expected error when one entity has decode error") } diff --git a/pkg/datastore/http_test.go b/pkg/datastore/http_test.go index 1809683..5835206 100644 --- a/pkg/datastore/http_test.go +++ b/pkg/datastore/http_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -65,9 +66,16 @@ func TestDoRequestRetryOn5xxError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -75,7 +83,7 @@ func TestDoRequestRetryOn5xxError(t *testing.T) { // This should succeed after retries key := datastore.NameKey("TestKind", "retry-test", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) if err != nil { t.Fatalf("Put should succeed after retries, got: %v", err) } @@ -124,9 +132,16 @@ func TestDoRequestFailsOn4xxError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -134,7 +149,7 @@ func TestDoRequestFailsOn4xxError(t *testing.T) { // This should fail immediately without retry on 4xx key := datastore.NameKey("TestKind", "bad-request", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) if err == nil { t.Fatal("expected error on 4xx response") } @@ -187,13 +202,22 @@ func TestDoRequestContextCancellation(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } // Create context that we'll cancel + var ctx context.Context // Declare ctx here ctx, cancel := context.WithCancel(context.Background()) // Cancel after a short delay @@ -254,16 +278,23 @@ func TestGetWithHTTPError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("TestKind", "test", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) if err == nil { t.Fatal("expected error on 404") @@ -302,24 +333,27 @@ func TestPutWithHTTPError(t *testing.T) { defer metadataServer.Close() apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Return 403 Forbidden w.WriteHeader(http.StatusForbidden) - if _, err := w.Write([]byte(`{"error":"permission denied"}`)); err != nil { - t.Logf("write failed: %v", err) - } })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("TestKind", "test", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) if err == nil { t.Fatal("expected error on 403") @@ -368,16 +402,23 @@ func TestDoRequestAllRetriesFail(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("TestKind", "test", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) if err == nil { t.Fatal("expected error after all retries") @@ -429,16 +470,23 @@ func TestDoRequestUnexpectedSuccess(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) if err == nil { t.Error("expected error for unexpected 2xx status") @@ -484,16 +532,23 @@ func TestDoRequestWithReadBodyError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) entity := &testEntity{Name: "test", Count: 1} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) // Should get an error related to response parsing if err != nil { t.Logf("Got expected error with incomplete response: %v", err) diff --git a/pkg/datastore/key_range_integration_test.go b/pkg/datastore/key_range_integration_test.go new file mode 100644 index 0000000..692e744 --- /dev/null +++ b/pkg/datastore/key_range_integration_test.go @@ -0,0 +1,73 @@ +package datastore + +import ( + "context" + "errors" + "fmt" + "testing" +) + +func TestMock_KeyRangeQuery(t *testing.T) { + client, cleanup := NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Define entity type + type CacheEntry struct { + Value string `datastore:"value"` + } + + // Store some entities + keys := []string{"user:alice.j", "user:bob.j", "user:charlie.j", "post:1.j", "post:2.j"} + for _, keyName := range keys { + key := NameKey("CacheEntry", keyName, nil) + entity := &CacheEntry{ + Value: keyName, + } + if _, err := client.Put(ctx, key, entity); err != nil { + t.Fatalf("Put(%s): %v", keyName, err) + } + } + + // Query with __key__ range filter + start := NameKey("CacheEntry", "user:.j", nil) + end := NameKey("CacheEntry", "user:\xff.j", nil) + + q := NewQuery("CacheEntry"). + Filter("__key__ >=", start). + Filter("__key__ <", end). + KeysOnly() + + it := client.Run(ctx, q) + var found []string + for { + key, err := it.Next(nil) + if err != nil { + if errors.Is(err, Done) { + break + } + t.Fatalf("Next: %v", err) + } + found = append(found, key.Name) + } + + fmt.Printf("Found keys: %v\n", found) + + // Should find 3 user keys + if len(found) != 3 { + t.Errorf("Found %d keys; want 3. Keys: %v", len(found), found) + } + + // Verify they're the right ones + wantKeys := map[string]bool{ + "user:alice.j": true, + "user:bob.j": true, + "user:charlie.j": true, + } + for _, k := range found { + if !wantKeys[k] { + t.Errorf("Unexpected key: %s", k) + } + } +} diff --git a/pkg/datastore/mock_client.go b/pkg/datastore/mock_client.go index 41356c7..b297dfa 100644 --- a/pkg/datastore/mock_client.go +++ b/pkg/datastore/mock_client.go @@ -17,18 +17,17 @@ func NewMockClient(t *testing.T) (client *Client, cleanup func()) { // Create mock servers metadataURL, apiURL, cleanup := mock.NewMockServers(t) - // Create context with test configuration - ctx := WithConfig(context.Background(), &Config{ - APIURL: apiURL, - AuthConfig: &auth.Config{ + // Create client with mock endpoints + var err error + client, err = NewClient( + context.Background(), + "test-project", + WithEndpoint(apiURL), + WithAuth(&auth.Config{ MetadataURL: metadataURL, SkipADC: true, - }, - }) - - // Create client - var err error - client, err = NewClient(ctx, "test-project") + }), + ) if err != nil { t.Fatalf("failed to create mock client: %v", err) } diff --git a/pkg/datastore/operations_delete_test.go b/pkg/datastore/operations_delete_test.go index 70a7fab..5c9eb39 100644 --- a/pkg/datastore/operations_delete_test.go +++ b/pkg/datastore/operations_delete_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" // Add missing import "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -197,16 +198,24 @@ func TestDeleteWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "del-db") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "del-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } // Delete with databaseID key := datastore.NameKey("TestKind", "to-delete", nil) - err = client.Delete(ctx, key) + err = client.Delete(context.Background(), key) if err != nil { t.Fatalf("Delete with databaseID failed: %v", err) } @@ -256,9 +265,17 @@ func TestMultiDeleteWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multidel-db") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "multidel-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } @@ -268,7 +285,7 @@ func TestMultiDeleteWithDatabaseID(t *testing.T) { datastore.NameKey("TestKind", "key1", nil), datastore.NameKey("TestKind", "key2", nil), } - err = client.DeleteMulti(ctx, keys) + err = client.DeleteMulti(context.Background(), keys) if err != nil { t.Fatalf("MultiDelete with databaseID failed: %v", err) } @@ -360,9 +377,16 @@ func TestDeleteMultiWithErrors(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -372,7 +396,7 @@ func TestDeleteMultiWithErrors(t *testing.T) { datastore.NameKey("TestKind", "key2", nil), } - err = client.DeleteMulti(ctx, keys) + err = client.DeleteMulti(context.Background(), keys) if err == nil { t.Fatal("expected error on server failure") } @@ -492,14 +516,21 @@ func TestDeleteAllByKindQueryFailure(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } - err = client.DeleteAllByKind(ctx, "TestKind") + err = client.DeleteAllByKind(context.Background(), "TestKind") if err == nil { t.Error("expected error when query fails") @@ -591,15 +622,22 @@ func TestDeleteWithServerError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - err = client.Delete(ctx, key) + err = client.Delete(context.Background(), key) if err == nil { t.Error("expected error on persistent server failure") @@ -645,12 +683,21 @@ func TestDeleteWithContextCancellation(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } + var ctx context.Context // Declare ctx here ctx, cancel := context.WithCancel(context.Background()) cancel() // Cancel immediately @@ -697,15 +744,23 @@ func TestDeleteAllRetriesFail(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - err = client.Delete(ctx, key) + err = client.Delete(context.Background(), key) if err == nil { t.Error("expected error after all retries exhausted") } @@ -770,15 +825,23 @@ func TestDeleteWithJSONMarshalError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - err = client.Delete(ctx, key) + err = client.Delete(context.Background(), key) if err != nil { t.Logf("Delete completed with: %v", err) } @@ -826,13 +889,21 @@ func TestDeleteAllByKindEmptyBatch(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } - err = client.DeleteAllByKind(ctx, "EmptyKind") + err = client.DeleteAllByKind(context.Background(), "EmptyKind") if err != nil { t.Logf("DeleteAllByKind with empty batch: %v", err) } @@ -880,8 +951,16 @@ func TestDeleteMultiMixedResults(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -891,7 +970,7 @@ func TestDeleteMultiMixedResults(t *testing.T) { datastore.NameKey("Test", "key2", nil), } - err = client.DeleteMulti(ctx, keys) + err = client.DeleteMulti(context.Background(), keys) // May or may not error depending on implementation if err != nil { t.Logf("DeleteMulti with mismatched results: %v", err) diff --git a/pkg/datastore/operations_get_test.go b/pkg/datastore/operations_get_test.go index df58dd8..8c734d1 100644 --- a/pkg/datastore/operations_get_test.go +++ b/pkg/datastore/operations_get_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -203,9 +204,17 @@ func TestMultiGetWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multiget-db") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "multiget-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } @@ -216,7 +225,7 @@ func TestMultiGetWithDatabaseID(t *testing.T) { datastore.NameKey("TestKind", "key2", nil), } var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) + err = client.GetMulti(context.Background(), keys, &entities) // Expect error since entities don't exist var multiErr datastore.MultiError if !errors.As(err, &multiErr) { @@ -410,25 +419,36 @@ func TestGetWithInvalidJSONResponse(t *testing.T) { defer metadataServer.Close() apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Return invalid JSON - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`{invalid json`)); err != nil { - t.Logf("write failed: %v", err) + if strings.Contains(r.URL.Path, ":lookup") { + // Return malformed JSON + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte("{invalid json")); err != nil { + t.Logf("write failed: %v", err) + } + return } + w.WriteHeader(http.StatusNotFound) })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) if err == nil { t.Error("expected error for invalid JSON response") @@ -493,16 +513,21 @@ func TestGetMultiWithServerError(t *testing.T) { defer metadataServer.Close() apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Always return 401 to simulate server error w.WriteHeader(http.StatusUnauthorized) - if _, err := w.Write([]byte(`{"error":"unauthorized"}`)); err != nil { - t.Logf("write failed: %v", err) - } })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -512,7 +537,7 @@ func TestGetMultiWithServerError(t *testing.T) { } var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) + err = client.GetMulti(context.Background(), keys, &entities) // Changed ctx to context.Background() if err == nil { t.Error("expected error on unauthorized") @@ -593,8 +618,16 @@ func TestGetMultiPartialNotFound(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -605,7 +638,7 @@ func TestGetMultiPartialNotFound(t *testing.T) { } var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) + err = client.GetMulti(context.Background(), keys, &entities) // Changed ctx to context.Background() if err == nil { t.Error("expected error when some entities are missing") } else { @@ -675,9 +708,9 @@ func TestGetWithJSONUnmarshalError(t *testing.T) { apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, ":lookup") { - // Return invalid JSON + // Return malformed JSON w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte(`{"found": [{"entity": "not-an-object"}]}`)); err != nil { + if _, err := w.Write([]byte("{invalid json")); err != nil { t.Logf("write failed: %v", err) } return @@ -686,8 +719,16 @@ func TestGetWithJSONUnmarshalError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -695,7 +736,7 @@ func TestGetWithJSONUnmarshalError(t *testing.T) { key := datastore.NameKey("Test", "key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error with invalid entity format") } @@ -759,8 +800,16 @@ func TestGetWithStringIDKey(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -771,7 +820,7 @@ func TestGetWithStringIDKey(t *testing.T) { key := datastore.IDKey("TestKind", 12345, nil) var entity TestEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) if err != nil { t.Fatalf("Get with string ID key failed: %v", err) } @@ -839,8 +888,16 @@ func TestGetWithFloat64IDKey(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -851,7 +908,7 @@ func TestGetWithFloat64IDKey(t *testing.T) { key := datastore.IDKey("TestKind", 67890, nil) var entity TestEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() if err != nil { t.Fatalf("Get with float64 ID key failed: %v", err) } @@ -919,8 +976,16 @@ func TestGetWithInvalidStringIDFormat(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -931,7 +996,7 @@ func TestGetWithInvalidStringIDFormat(t *testing.T) { key := datastore.IDKey("TestKind", 12345, nil) var entity TestEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) // Changed ctx to context.Background() // May or may not error depending on parsing behavior if err != nil { t.Logf("Get with invalid string ID format failed: %v", err) @@ -980,8 +1045,16 @@ func TestGetJSONUnmarshalError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -989,7 +1062,7 @@ func TestGetJSONUnmarshalError(t *testing.T) { key := datastore.NameKey("Test", "test-key", nil) var entity testEntity - err = client.Get(ctx, key, &entity) + err = client.Get(context.Background(), key, &entity) if err == nil { t.Error("expected error with malformed JSON") } diff --git a/pkg/datastore/operations_misc_test.go b/pkg/datastore/operations_misc_test.go index a9f5c5d..9db3410 100644 --- a/pkg/datastore/operations_misc_test.go +++ b/pkg/datastore/operations_misc_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" // Add missing import "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -275,16 +276,24 @@ func TestAllKeysWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "query-db") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "query-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } // Query with databaseID query := datastore.NewQuery("TestKind").KeysOnly() - keys, err := client.AllKeys(ctx, query) + keys, err := client.AllKeys(context.Background(), query) if err != nil { t.Fatalf("AllKeys with databaseID failed: %v", err) } @@ -571,293 +580,25 @@ func TestAllKeysWithInvalidResponse(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - query := datastore.NewQuery("Test").KeysOnly() - _, err = client.AllKeys(ctx, query) - - if err == nil { - t.Error("expected error for invalid JSON") - } -} - -func TestKeyFromJSONInvalidPathElement(t *testing.T) { - // Test with non-map path element - keyData := map[string]any{ - "path": []any{ - "invalid-string-instead-of-map", - }, - } - - metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Metadata-Flavor") != "Google" { - w.WriteHeader(http.StatusForbidden) - return - } - if r.URL.Path == "/project/project-id" { - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("test-project")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - if r.URL.Path == "/instance/service-accounts/default/token" { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "access_token": "test-token", - "expires_in": 3600, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer metadataServer.Close() - - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, ":commit") { - // Return response with invalid key in mutation result - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []map[string]any{ - { - "key": keyData, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - realClient, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - key := datastore.NameKey("Test", "key", nil) - entity := &testEntity{Name: "test"} - - // Try Put which will parse the returned key - _, err = realClient.Put(ctx, key, entity) - if err == nil { - t.Log("Put succeeded despite invalid path element (API may handle gracefully)") - } else { - t.Logf("Put failed as expected: %v", err) - } -} - -func TestKeyFromJSONInvalidIDString(t *testing.T) { - keyData := map[string]any{ - "path": []any{ - map[string]any{ - "kind": "Test", - "id": "not-a-number", - }, - }, - } - - metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Metadata-Flavor") != "Google" { - w.WriteHeader(http.StatusForbidden) - return - } - if r.URL.Path == "/project/project-id" { - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("test-project")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - if r.URL.Path == "/instance/service-accounts/default/token" { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "access_token": "test-token", - "expires_in": 3600, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer metadataServer.Close() - - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, ":commit") { - // Return response with invalid ID string in mutation result - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []map[string]any{ - { - "key": keyData, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - realClient, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - key := datastore.NameKey("Test", "key", nil) - entity := &testEntity{Name: "test"} - - // Try Put which will parse the returned key - _, err = realClient.Put(ctx, key, entity) - if err == nil { - t.Log("Put succeeded despite invalid ID string (API may handle gracefully)") - } else { - t.Logf("Put failed as expected: %v", err) - } -} - -func TestKeyFromJSONIDAsFloat(t *testing.T) { - keyData := map[string]any{ - "path": []any{ - map[string]any{ - "kind": "Test", - "id": float64(12345), - }, - }, - } - - metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Metadata-Flavor") != "Google" { - w.WriteHeader(http.StatusForbidden) - return - } - if r.URL.Path == "/project/project-id" { - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("test-project")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - if r.URL.Path == "/instance/service-accounts/default/token" { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "access_token": "test-token", - "expires_in": 3600, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer metadataServer.Close() - - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, ":lookup") { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "found": []map[string]any{ - { - "entity": map[string]any{ - "key": keyData, - "properties": map[string]any{ - "name": map[string]any{"stringValue": "test"}, - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - realClient, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - key := datastore.NameKey("Test", "key", nil) - var entity testEntity - - err = realClient.Get(ctx, key, &entity) - if err != nil { - t.Errorf("unexpected error with float64 ID: %v", err) + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, } -} - -func TestAllKeysInvalidJSON(t *testing.T) { - metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Metadata-Flavor") != "Google" { - w.WriteHeader(http.StatusForbidden) - return - } - if r.URL.Path == "/project/project-id" { - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte("test-project")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - if r.URL.Path == "/instance/service-accounts/default/token" { - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "access_token": "test-token", - "expires_in": 3600, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer metadataServer.Close() - - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, ":runQuery") { - // Return invalid JSON - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte("{")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } query := datastore.NewQuery("Test").KeysOnly() + _, err = client.AllKeys(context.Background(), query) // Changed ctx to context.Background() - _, err = client.AllKeys(ctx, query) if err == nil { - t.Error("expected error with invalid JSON") + t.Error("expected error for invalid JSON") } } @@ -935,15 +676,23 @@ func TestAllKeysWithBatching(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } query := datastore.NewQuery("Test").KeysOnly() - keys, err := client.AllKeys(ctx, query) + keys, err := client.AllKeys(context.Background(), query) // Changed ctx to context.Background() if err != nil { t.Logf("AllKeys with many results: %v", err) } else if len(keys) != 50 { @@ -1001,15 +750,23 @@ func TestAllKeysKeyFromJSONError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } query := datastore.NewQuery("Test").KeysOnly() - _, err = client.AllKeys(ctx, query) + _, err = client.AllKeys(context.Background(), query) // Changed ctx to context.Background() if err == nil { t.Error("expected error with invalid key format") } @@ -1067,14 +824,22 @@ func TestAllKeysEmptyPathInKey(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } query := datastore.NewQuery("TestKind").KeysOnly() - _, err = client.AllKeys(ctx, query) + _, err = client.AllKeys(context.Background(), query) // Changed ctx to context.Background() if err == nil { t.Error("expected error with empty path in key") } @@ -1132,14 +897,22 @@ func TestAllKeysInvalidPathElement(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } query := datastore.NewQuery("TestKind").KeysOnly() - _, err = client.AllKeys(ctx, query) + _, err = client.AllKeys(context.Background(), query) // Changed ctx to context.Background() if err == nil { t.Error("expected error with invalid path element") } diff --git a/pkg/datastore/operations_put_test.go b/pkg/datastore/operations_put_test.go index 325e297..57dfc27 100644 --- a/pkg/datastore/operations_put_test.go +++ b/pkg/datastore/operations_put_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -408,9 +409,16 @@ func TestPutMultiWithServerError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -425,7 +433,7 @@ func TestPutMultiWithServerError(t *testing.T) { {Name: "entity2", Count: 2}, } - _, err = client.PutMulti(ctx, keys, entities) + _, err = client.PutMulti(context.Background(), keys, entities) // Changed ctx to context.Background() if err == nil { t.Error("expected error on server failure") @@ -504,16 +512,23 @@ func TestPutWithAccessTokenError(t *testing.T) { })) defer metadataServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, "http://unused") - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithAuth(authConfig), + // No WithEndpoint needed as the error happens before API call + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) entity := &testEntity{Name: "test"} - _, err = client.Put(ctx, key, entity) + _, err = client.Put(context.Background(), key, entity) // Changed ctx to context.Background() if err == nil { t.Error("expected error when access token fails") } @@ -557,8 +572,16 @@ func TestPutMultiRequestMarshalError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -572,7 +595,7 @@ func TestPutMultiRequestMarshalError(t *testing.T) { {Name: "test1", Count: 123}, } - _, err = client.PutMulti(ctx, keys, entities) + _, err = client.PutMulti(context.Background(), keys, entities) // Changed ctx to context.Background() if err != nil { t.Logf("PutMulti completed with: %v", err) } diff --git a/pkg/datastore/test_helper.go b/pkg/datastore/test_helper.go index 066d406..daf975b 100644 --- a/pkg/datastore/test_helper.go +++ b/pkg/datastore/test_helper.go @@ -6,20 +6,25 @@ import ( "github.com/codeGROOVE-dev/ds9/auth" ) -// TestConfig creates a context with test configuration for the given URLs. +// TestOptions creates client options for test configuration with the given URLs. // This is a helper for tests to easily configure mock servers. -// Use this with context.Background() or any existing context. // // Example: // -// ctx := datastore.TestConfig(context.Background(), metadataURL, apiURL) -// client, err := datastore.NewClient(ctx, "test-project") -func TestConfig(ctx context.Context, metadataURL, apiURL string) context.Context { - return WithConfig(ctx, &Config{ - APIURL: apiURL, - AuthConfig: &auth.Config{ +// opts := datastore.TestOptions(metadataURL, apiURL) +// client, err := datastore.NewClient(ctx, "test-project", opts...) +func TestOptions(metadataURL, apiURL string) []ClientOption { + return []ClientOption{ + WithEndpoint(apiURL), + WithAuth(&auth.Config{ MetadataURL: metadataURL, SkipADC: true, - }, - }) + }), + } +} + +// TestConfig creates client options for test configuration with the given URLs. +// Deprecated: TestConfig is deprecated. Use TestOptions instead. +func TestConfig(_ context.Context, metadataURL, apiURL string) []ClientOption { + return TestOptions(metadataURL, apiURL) } diff --git a/pkg/datastore/transaction_test.go b/pkg/datastore/transaction_test.go index 892ac7c..50e7fcf 100644 --- a/pkg/datastore/transaction_test.go +++ b/pkg/datastore/transaction_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -257,13 +258,23 @@ func TestTransactionWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "tx-db") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClientWithDatabase( + context.Background(), + "test-project", + "tx-db", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) } + ctx := context.Background() + // Run transaction with databaseID _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { key := datastore.NameKey("TestKind", "tx-test", nil) @@ -355,14 +366,21 @@ func TestTransactionBeginFailure(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { return nil }) @@ -371,7 +389,7 @@ func TestTransactionBeginFailure(t *testing.T) { } if !strings.Contains(err.Error(), "500") { - t.Errorf("expected error to mention 500 status, got: %v", err) + t.Errorf("expected 500 error, got: %v", err) } } @@ -452,16 +470,23 @@ func TestTransactionCommitAbortedRetry(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } // This should succeed after retries key := datastore.NameKey("TestKind", "tx-retry", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { // Changed ctx to context.Background() _, err := tx.Put(key, &testEntity{Name: "test", Count: 1}) return err }) @@ -540,16 +565,23 @@ func TestTransactionMaxRetriesExceeded(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } // This should fail after max retries key := datastore.NameKey("TestKind", "tx-max-retry", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { _, err := tx.Put(key, &testEntity{Name: "test", Count: 1}) return err }) @@ -717,15 +749,22 @@ func TestTransactionGetWithInvalidResponse(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { var entity testEntity return tx.Get(key, &entity) }) @@ -801,15 +840,22 @@ func TestTransactionWithNonRetriableError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { _, err := tx.Put(key, &testEntity{Name: "test", Count: 1}) return err }) @@ -865,14 +911,21 @@ func TestTransactionWithInvalidTxResponse(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { return nil }) @@ -965,15 +1018,22 @@ func TestTransactionGetWithDecodeError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "key", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { var entity testEntity return tx.Get(key, &entity) }) @@ -1043,15 +1103,22 @@ func TestTransactionGetMissingEntity(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "nonexistent", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { var entity testEntity err := tx.Get(key, &entity) if err == nil { @@ -1128,9 +1195,10 @@ func TestTransactionGetDecodeError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + opts := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") + client, err := datastore.NewClient(ctx, "test-project", opts...) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -1200,9 +1268,10 @@ func TestTransactionCommitInvalidResponse(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + opts := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") + client, err := datastore.NewClient(ctx, "test-project", opts...) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -1267,9 +1336,10 @@ func TestTransactionCommitUnmarshalError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + opts := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") + client, err := datastore.NewClient(ctx, "test-project", opts...) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -1347,9 +1417,10 @@ func TestTransactionGetNotFound(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + opts := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") + client, err := datastore.NewClient(ctx, "test-project", opts...) if err != nil { t.Fatalf("NewClient failed: %v", err) } @@ -1404,15 +1475,22 @@ func TestTransactionGetAccessTokenError(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "test-key", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { var entity testEntity err := tx.Get(key, &entity) if err == nil { @@ -1484,15 +1562,22 @@ func TestTransactionGetNonOKStatus(t *testing.T) { })) defer apiServer.Close() - ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - - client, err := datastore.NewClient(ctx, "test-project") + authConfig := &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + } + client, err := datastore.NewClient( + context.Background(), + "test-project", + datastore.WithEndpoint(apiServer.URL), + datastore.WithAuth(authConfig), + ) if err != nil { t.Fatalf("NewClient failed: %v", err) } key := datastore.NameKey("Test", "test-key", nil) - _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + _, err = client.RunInTransaction(context.Background(), func(tx *datastore.Transaction) error { var entity testEntity return tx.Get(key, &entity) }) diff --git a/pkg/mock/mock.go b/pkg/mock/mock.go index 52879b0..0d0d61c 100644 --- a/pkg/mock/mock.go +++ b/pkg/mock/mock.go @@ -1015,118 +1015,189 @@ func (s *Store) handleAllocateIDs(w http.ResponseWriter, r *http.Request) { } } -// matchesFilter checks if an entity matches a filter. -// -//nolint:gocognit,nestif // Complex logic required for proper filter evaluation with multiple types and operators -func matchesFilter(entity map[string]any, filterMap map[string]any) bool { - // Handle propertyFilter - if propFilter, ok := filterMap["propertyFilter"].(map[string]any); ok { - property, ok := propFilter["property"].(map[string]any) - if !ok { - return true // Invalid filter, allow all - } - propertyName, ok := property["name"].(string) - if !ok { - return true - } - operator, ok := propFilter["op"].(string) - if !ok { - return true - } - filterValue := propFilter["value"] +// extractFilterKeyData extracts key data from a filter value, handling both wrapped and direct formats. +func extractFilterKeyData(filterValue any) (map[string]any, bool) { + fkd, ok := filterValue.(map[string]any) + if !ok { + return nil, false + } + // Check if it's wrapped in keyValue + if kv, hasKeyValue := fkd["keyValue"].(map[string]any); hasKeyValue { + return kv, true + } + // It's a direct key + return fkd, true +} - // Handle HAS_ANCESTOR - if operator == "HAS_ANCESTOR" { - ancestorKeyData, ok := filterValue.(map[string]any) - if !ok { - // Try keyValue if wrapped - kv, ok := filterValue.(map[string]any) - if !ok { - return false - } - ak, ok := kv["keyValue"].(map[string]any) - if !ok { - return false - } - ancestorKeyData = ak - } - // Check if entity key has prefix of ancestor key path - entityKeyData, ok := entity["key"].(map[string]any) - if !ok { - return false - } - return isAncestor(ancestorKeyData, entityKeyData) - } +// matchesKeyFilter checks if an entity matches a __key__ filter. +func matchesKeyFilter(entity map[string]any, operator string, filterValue any) bool { + entityKeyData, ok := entity["key"].(map[string]any) + if !ok { + return false + } + + filterKeyData, ok := extractFilterKeyData(filterValue) + if !ok { + return false + } - // Get entity properties - properties, ok := entity["properties"].(map[string]any) + // Compare keys based on operator + cmpResult := compareKeys(entityKeyData, filterKeyData) + switch operator { + case "EQUAL": + return cmpResult == 0 + case "GREATER_THAN": + return cmpResult > 0 + case "GREATER_THAN_OR_EQUAL": + return cmpResult >= 0 + case "LESS_THAN": + return cmpResult < 0 + case "LESS_THAN_OR_EQUAL": + return cmpResult <= 0 + default: + return false + } +} + +// matchesAncestorFilter checks if an entity matches a HAS_ANCESTOR filter. +func matchesAncestorFilter(entity map[string]any, filterValue any) bool { + ancestorKeyData, ok := filterValue.(map[string]any) + if !ok { + // Try keyValue if wrapped + kv, ok := filterValue.(map[string]any) if !ok { return false } - entityProp, ok := properties[propertyName].(map[string]any) + ak, ok := kv["keyValue"].(map[string]any) if !ok { - return false // Property doesn't exist + return false } + ancestorKeyData = ak + } + // Check if entity key has prefix of ancestor key path + entityKeyData, ok := entity["key"].(map[string]any) + if !ok { + return false + } + return isAncestor(ancestorKeyData, entityKeyData) +} - // Extract entity value based on type - var entityValue any - if intVal, ok := entityProp["integerValue"].(string); ok { +// extractEntityValue extracts a typed value from an entity property. +func extractEntityValue(entityProp map[string]any) any { + if intVal, ok := entityProp["integerValue"].(string); ok { + var i int64 + if _, err := fmt.Sscanf(intVal, "%d", &i); err == nil { + return i + } + } else if strVal, ok := entityProp["stringValue"].(string); ok { + return strVal + } else if boolVal, ok := entityProp["booleanValue"].(bool); ok { + return boolVal + } else if floatVal, ok := entityProp["doubleValue"].(float64); ok { + return floatVal + } + return nil +} + +// extractFilterValue extracts a typed value from a filter value. +func extractFilterValue(filterValue any) any { + if fv, ok := filterValue.(map[string]any); ok { + if intVal, ok := fv["integerValue"].(string); ok { var i int64 if _, err := fmt.Sscanf(intVal, "%d", &i); err == nil { - entityValue = i - } - } else if strVal, ok := entityProp["stringValue"].(string); ok { - entityValue = strVal - } else if boolVal, ok := entityProp["booleanValue"].(bool); ok { - entityValue = boolVal - } else if floatVal, ok := entityProp["doubleValue"].(float64); ok { - entityValue = floatVal - } - - // Extract filter value - var filterVal any - if fv, ok := filterValue.(map[string]any); ok { - if intVal, ok := fv["integerValue"].(string); ok { - var i int64 - if _, err := fmt.Sscanf(intVal, "%d", &i); err == nil { - filterVal = i - } - } else if strVal, ok := fv["stringValue"].(string); ok { - filterVal = strVal + return i } + } else if strVal, ok := fv["stringValue"].(string); ok { + return strVal } + } + return nil +} - // Compare based on operator - switch operator { - case "EQUAL": - return entityValue == filterVal - case "GREATER_THAN": - if ev, ok := entityValue.(int64); ok { - if fv, ok := filterVal.(int64); ok { - return ev > fv - } +// comparePropertyValues compares entity and filter values based on the operator. +func comparePropertyValues(entityValue, filterVal any, operator string) bool { + switch operator { + case "EQUAL": + return entityValue == filterVal + case "GREATER_THAN": + if ev, ok := entityValue.(int64); ok { + if fv, ok := filterVal.(int64); ok { + return ev > fv } - case "GREATER_THAN_OR_EQUAL": - if ev, ok := entityValue.(int64); ok { - if fv, ok := filterVal.(int64); ok { - return ev >= fv - } + } + case "GREATER_THAN_OR_EQUAL": + if ev, ok := entityValue.(int64); ok { + if fv, ok := filterVal.(int64); ok { + return ev >= fv } - case "LESS_THAN": - if ev, ok := entityValue.(int64); ok { - if fv, ok := filterVal.(int64); ok { - return ev < fv - } + } + case "LESS_THAN": + if ev, ok := entityValue.(int64); ok { + if fv, ok := filterVal.(int64); ok { + return ev < fv } - case "LESS_THAN_OR_EQUAL": - if ev, ok := entityValue.(int64); ok { - if fv, ok := filterVal.(int64); ok { - return ev <= fv - } + } + case "LESS_THAN_OR_EQUAL": + if ev, ok := entityValue.(int64); ok { + if fv, ok := filterVal.(int64); ok { + return ev <= fv } - default: - return false } + default: + return false + } + return false +} + +// matchesPropertyFilter checks if an entity matches a property filter. +func matchesPropertyFilter(entity map[string]any, propFilter map[string]any) bool { + property, ok := propFilter["property"].(map[string]any) + if !ok { + return true // Invalid filter, allow all + } + propertyName, ok := property["name"].(string) + if !ok { + return true + } + operator, ok := propFilter["op"].(string) + if !ok { + return true + } + filterValue := propFilter["value"] + + // Handle __key__ filters (special property) + if propertyName == "__key__" { + return matchesKeyFilter(entity, operator, filterValue) + } + + // Handle HAS_ANCESTOR + if operator == "HAS_ANCESTOR" { + return matchesAncestorFilter(entity, filterValue) + } + + // Get entity properties + properties, ok := entity["properties"].(map[string]any) + if !ok { + return false + } + entityProp, ok := properties[propertyName].(map[string]any) + if !ok { + return false // Property doesn't exist + } + + // Extract entity and filter values + entityValue := extractEntityValue(entityProp) + filterVal := extractFilterValue(filterValue) + + // Compare based on operator + return comparePropertyValues(entityValue, filterVal, operator) +} + +// matchesFilter checks if an entity matches a filter. +func matchesFilter(entity map[string]any, filterMap map[string]any) bool { + // Handle propertyFilter + if propFilter, ok := filterMap["propertyFilter"].(map[string]any); ok { + return matchesPropertyFilter(entity, propFilter) } // Handle compositeFilter (AND/OR) @@ -1167,6 +1238,108 @@ func matchesFilter(entity map[string]any, filterMap map[string]any) bool { return true // No filter or unrecognized filter, allow all } +// compareKeys compares two Datastore keys and returns: +// -1 if keyA < keyB +// +// 0 if keyA == keyB +// 1 if keyA > keyB +func compareKeys(keyA, keyB map[string]any) int { + // Extract key strings for comparison + strA := keyToSortString(keyA) + strB := keyToSortString(keyB) + + if strA < strB { + return -1 + } + if strA > strB { + return 1 + } + return 0 +} + +// keyToSortStringFromEntityValue extracts sort string from entityValue format. +func keyToSortStringFromEntityValue(entityValue map[string]any) string { + props, ok := entityValue["properties"].(map[string]any) + if !ok { + return "" + } + + // Extract kind + kindProp, ok := props["Kind"].(map[string]any) + if !ok { + return "" + } + kind, ok := kindProp["stringValue"].(string) + if !ok { + return "" + } + + // Extract namespace + namespace := "" + if nsProp, ok := props["Namespace"].(map[string]any); ok { + if ns, ok := nsProp["stringValue"].(string); ok && ns != "" { + namespace = ns + } + } + + // Extract name or ID + if nameProp, ok := props["Name"].(map[string]any); ok { + if name, ok := nameProp["stringValue"].(string); ok && name != "" { + return namespace + "!" + kind + "/" + name + } + } + if idProp, ok := props["ID"].(map[string]any); ok { + if idInt, ok := idProp["integerValue"].(float64); ok && idInt != 0 { + return namespace + "!" + kind + "/" + fmt.Sprintf("%.0f", idInt) + } + if idStr, ok := idProp["stringValue"].(string); ok && idStr != "" { + return namespace + "!" + kind + "/" + idStr + } + } + return namespace + "!" + kind + "/" +} + +// keyToSortString converts a key to a string for sorting/comparison. +// Format: "namespace!kind/name_or_id" +// Supports both standard key format (with "path") and entityValue format (with "properties"). +func keyToSortString(keyData map[string]any) string { + // Try entityValue format first (used by encoded filter values) + if entityValue, ok := keyData["entityValue"].(map[string]any); ok { + return keyToSortStringFromEntityValue(entityValue) + } + + // Try standard path format (used by stored entities) + path, ok := keyData["path"].([]any) + if !ok || len(path) == 0 { + return "" + } + pathElem, ok := path[0].(map[string]any) + if !ok { + return "" + } + kind, ok := pathElem["kind"].(string) + if !ok { + return "" + } + + // Extract namespace + namespace := "" + if pid, ok := keyData["partitionId"].(map[string]any); ok { + if ns, ok := pid["namespaceId"].(string); ok { + namespace = ns + } + } + + // Handle both name and ID keys + if name, ok := pathElem["name"].(string); ok { + return namespace + "!" + kind + "/" + name + } + if id, ok := pathElem["id"].(string); ok { + return namespace + "!" + kind + "/" + id + } + return namespace + "!" + kind + "/" +} + // isAncestor checks if ancestorKey is a prefix of entityKey. func isAncestor(ancestorKey, entityKey map[string]any) bool { ancPath, ok1 := ancestorKey["path"].([]any)