diff --git a/.golangci.yml b/.golangci.yml index 3e2f4d9..a4696b6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -214,6 +214,8 @@ linters: - r - w - f + - h + - q - err exclusions: diff --git a/auth/auth.go b/auth/auth.go index 6d5d0e0..532b239 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -22,38 +22,52 @@ const ( maxBodySize = 10 * 1024 * 1024 // 10MB defaultTimeout = 30 * time.Second metadataFlavor = "Google" + //nolint:revive // GCP metadata server only supports HTTP + defaultMetadataURL = "http://metadata.google.internal/computeMetadata/v1" ) -var ( - metadataURL = "http://metadata.google.internal/computeMetadata/v1" //nolint:revive // GCP metadata server only supports HTTP - isTestMode = false +var httpClient = &http.Client{ + Timeout: defaultTimeout, +} - httpClient = &http.Client{ - Timeout: defaultTimeout, - } -) +// Config holds auth configuration. +type Config struct { + // MetadataURL is the URL for the GCP metadata server. + // Defaults to the production metadata server if empty. + MetadataURL string + + // SkipADC skips Application Default Credentials and goes straight to metadata server. + // Useful for testing to ensure mock servers are used. + SkipADC bool +} -// SetMetadataURL sets a custom metadata server URL for testing. -// Returns a function that restores the original URL. -// WARNING: This function should only be called in test code. -// Set DS9_ALLOW_TEST_OVERRIDES=true to enable in non-test environments. -func SetMetadataURL(urlStr string) func() { - old := metadataURL - oldTestMode := isTestMode - metadataURL = urlStr - isTestMode = true // Enable test mode to skip ADC - return func() { - metadataURL = old - isTestMode = oldTestMode +// configKey is the key for storing Config in context. +type configKey struct{} + +// WithConfig returns a new context with the given auth config. +func WithConfig(ctx context.Context, cfg *Config) context.Context { + return context.WithValue(ctx, configKey{}, cfg) +} + +// getConfig retrieves the auth config from context, or returns defaults. +func getConfig(ctx context.Context) *Config { + if cfg, ok := ctx.Value(configKey{}).(*Config); ok && cfg != nil { + return cfg + } + return &Config{ + MetadataURL: defaultMetadataURL, + SkipADC: false, } } // AccessToken retrieves a GCP access token. // It tries Application Default Credentials first, then falls back to the metadata server. -// In test mode, ADC is skipped to ensure mock servers are used. +// Configuration can be provided via auth.WithConfig in the context. func AccessToken(ctx context.Context) (string, error) { - // Skip ADC in test mode to ensure tests use mock metadata server - if !isTestMode { + cfg := getConfig(ctx) + + // Skip ADC if configured (useful for testing to ensure mock metadata server is used) + if !cfg.SkipADC { // Try Application Default Credentials first (for local development) token, err := accessTokenFromADC(ctx) if err == nil { @@ -165,7 +179,8 @@ func exchangeRefreshToken(ctx context.Context, clientID, clientSecret, refreshTo // accessTokenFromMetadata retrieves an access token from the GCP metadata server. // This is used when running on GCP (GCE, GKE, Cloud Run, etc.). func accessTokenFromMetadata(ctx context.Context) (string, error) { - reqURL := metadataURL + "/instance/service-accounts/default/token" + cfg := getConfig(ctx) + reqURL := cfg.MetadataURL + "/instance/service-accounts/default/token" req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody) if err != nil { @@ -206,7 +221,8 @@ func accessTokenFromMetadata(ctx context.Context) (string, error) { // ProjectID retrieves the project ID from the GCP metadata server. func ProjectID(ctx context.Context) (string, error) { - reqURL := metadataURL + "/project/project-id" + cfg := getConfig(ctx) + reqURL := cfg.MetadataURL + "/project/project-id" req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, http.NoBody) if err != nil { diff --git a/auth/auth_test.go b/auth/auth_test.go index a29d73f..e02e51a 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -12,30 +12,27 @@ import ( "testing" ) -func TestSetMetadataURL(t *testing.T) { - originalURL := metadataURL - originalTestMode := isTestMode - - // Set custom URL - restore := SetMetadataURL("http://custom-metadata") - - if metadataURL != "http://custom-metadata" { - t.Errorf("expected metadataURL to be http://custom-metadata, got %s", metadataURL) +func TestWithConfig(t *testing.T) { + // Test that config can be set in context + cfg := &Config{ + MetadataURL: "http://custom-metadata", + SkipADC: true, } + ctx := WithConfig(context.Background(), cfg) - if !isTestMode { - t.Error("expected isTestMode to be true") + // Context should be non-nil + if ctx == nil { + t.Fatal("expected non-nil context") } - // Restore - restore() - - if metadataURL != originalURL { - t.Errorf("expected metadataURL to be restored to %s, got %s", originalURL, metadataURL) + // Verify config is retrievable + retrievedCfg := getConfig(ctx) + if retrievedCfg.MetadataURL != "http://custom-metadata" { + t.Errorf("expected MetadataURL to be http://custom-metadata, got %s", retrievedCfg.MetadataURL) } - if isTestMode != originalTestMode { - t.Errorf("expected isTestMode to be restored to %v, got %v", originalTestMode, isTestMode) + if !retrievedCfg.SkipADC { + t.Error("expected SkipADC to be true") } } @@ -111,10 +108,11 @@ func TestAccessTokenFromMetadata(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) - ctx := context.Background() token, err := accessTokenFromMetadata(ctx) if tt.wantErr { @@ -153,12 +151,12 @@ func TestAccessToken(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) - ctx := context.Background() - - // In test mode, should use metadata server + // With SkipADC=true, should use metadata server token, err := AccessToken(ctx) if err != nil { t.Fatalf("AccessToken failed: %v", err) @@ -353,10 +351,10 @@ func TestProjectID(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) projectID, err := ProjectID(ctx) if tt.wantErr { @@ -379,10 +377,10 @@ func TestProjectID(t *testing.T) { func TestAccessTokenMetadataServerDown(t *testing.T) { // Point to non-existent server - restore := SetMetadataURL("http://localhost:59999") - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: "http://localhost:59999", + SkipADC: true, + }) _, err := accessTokenFromMetadata(ctx) if err == nil { @@ -396,10 +394,10 @@ func TestAccessTokenMetadataServerDown(t *testing.T) { func TestProjectIDMetadataServerDown(t *testing.T) { // Point to non-existent server - restore := SetMetadataURL("http://localhost:59998") - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: "http://localhost:59998", + SkipADC: true, + }) _, err := ProjectID(ctx) if err == nil { @@ -465,10 +463,10 @@ func TestAccessTokenFromMetadataReadError(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) _, err := accessTokenFromMetadata(ctx) if err == nil { @@ -488,10 +486,10 @@ func TestProjectIDReadError(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) _, err := ProjectID(ctx) if err == nil { @@ -521,10 +519,10 @@ func TestAccessTokenFromMetadataWithMalformedJSON(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) _, err := accessTokenFromMetadata(ctx) // Should either succeed (if parser is lenient) or fail with parse error if err != nil { @@ -547,10 +545,10 @@ func TestProjectIDWithEmptyResponse(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) projectID, err := ProjectID(ctx) if err != nil { t.Fatalf("ProjectID with empty response failed: %v", err) @@ -650,15 +648,16 @@ func TestAccessTokenFallbackToMetadata(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) // Ensure no ADC credentials are available if err := os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS"); err != nil { t.Fatalf("failed to unset env var: %v", err) } - ctx := context.Background() token, err := AccessToken(ctx) if err != nil { t.Fatalf("AccessToken failed: %v", err) @@ -688,10 +687,10 @@ func TestProjectIDInvalidJSON(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) projectID, err := ProjectID(ctx) if err != nil { @@ -750,10 +749,10 @@ func TestAccessTokenFromADCDefaultLocation(t *testing.T) { // Test ProjectID with request error func TestProjectIDRequestError(t *testing.T) { // Set invalid URL to trigger request error - restore := SetMetadataURL("http://invalid-host-that-does-not-exist-12345") - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: "http://invalid-host-that-does-not-exist-12345", + SkipADC: true, + }) _, err := ProjectID(ctx) if err == nil { @@ -777,10 +776,10 @@ func TestAccessTokenFromMetadataTypeError(t *testing.T) { })) defer server.Close() - restore := SetMetadataURL(server.URL) - defer restore() - - ctx := context.Background() + ctx := WithConfig(context.Background(), &Config{ + MetadataURL: server.URL, + SkipADC: true, + }) _, err := accessTokenFromMetadata(ctx) if err == nil { diff --git a/go.mod b/go.mod index c484c74..e253b8c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/codeGROOVE-dev/ds9 -go 1.23 +go 1.25 diff --git a/pkg/datastore/client.go b/pkg/datastore/client.go index 9b0e5e0..37a8462 100644 --- a/pkg/datastore/client.go +++ b/pkg/datastore/client.go @@ -3,8 +3,6 @@ // It uses only the Go standard library and makes direct REST API calls // to the Datastore API. Authentication is handled via the GCP metadata // server when running on GCP, or via Application Default Credentials. -// -//nolint:revive // Public structs required for API compatibility with cloud.google.com/go/datastore package datastore import ( @@ -12,7 +10,6 @@ import ( "fmt" "log/slog" "net/http" - "sync/atomic" "testing" "time" @@ -28,11 +25,11 @@ const ( jitterFraction = 0.25 // 25% jitter ) -var ( - // atomicAPIURL stores the API URL for thread-safe access. - // Use getAPIURL() to read and setAPIURL() to write. - atomicAPIURL atomic.Pointer[string] +const ( + defaultAPIURL = "https://datastore.googleapis.com/v1" +) +var ( httpClient = &http.Client{ Timeout: defaultTimeout, Transport: &http.Transport{ @@ -52,60 +49,60 @@ var ( } ) -//nolint:gochecknoinits // Required for thread-safe initialization of atomic pointer -func init() { - defaultURL := "https://datastore.googleapis.com/v1" - atomicAPIURL.Store(&defaultURL) -} +// 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 -// getAPIURL returns the current API URL in a thread-safe manner. -func getAPIURL() string { - return *atomicAPIURL.Load() + // APIURL is the base URL for the Datastore API. + // Defaults to production if empty. + APIURL string } -// setAPIURL sets the API URL in a thread-safe manner. -func setAPIURL(url string) { - atomicAPIURL.Store(&url) +// configKey is the key for storing Config in context. +type configKey struct{} + +// 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) + } + return context.WithValue(ctx, configKey{}, cfg) } -// SetTestURLs configures custom metadata and API URLs for testing. -// This is intended for use by testing packages like ds9mock. -// Returns a function that restores the original URLs. -// WARNING: This function should only be called in test code. -// Set DS9_ALLOW_TEST_OVERRIDES=true to enable in non-test environments. -// -// Example: -// -// restore := ds9.SetTestURLs("http://localhost:8080", "http://localhost:9090") -// defer restore() -func SetTestURLs(metadata, api string) (restore func()) { - // Auth package will log warning if called outside test environment - oldAPI := getAPIURL() - setAPIURL(api) - restoreAuth := auth.SetMetadataURL(metadata) - return func() { - setAPIURL(oldAPI) - restoreAuth() +// 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 + } + return &Config{ + APIURL: defaultAPIURL, } } // Client is a Google Cloud Datastore client. type Client struct { logger *slog.Logger + authConfig *auth.Config // Auth configuration for this client projectID string databaseID string - baseURL string // API base URL, defaults to production but can be overridden for testing + baseURL string // API base URL, defaults to production } // 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, "") } // 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) if projID == "" { if !testing.Testing() { @@ -126,10 +123,16 @@ func NewClientWithDatabase(ctx context.Context, projID, dbID string) (*Client, e logger.InfoContext(ctx, "creating datastore client", "project_id", projID, "database_id", dbID) } + baseURL := cfg.APIURL + if baseURL == "" { + baseURL = defaultAPIURL + } + return &Client{ projectID: projID, databaseID: dbID, - baseURL: getAPIURL(), + baseURL: baseURL, + authConfig: cfg.AuthConfig, logger: logger, }, nil } @@ -140,3 +143,13 @@ func NewClientWithDatabase(ctx context.Context, projID, dbID string) (*Client, e func (*Client) Close() error { return nil } + +// withClientConfig returns a context with the client's auth configuration injected. +// This ensures that operations use the client's auth settings even if the caller +// passes a bare context.Background(). +func (c *Client) withClientConfig(ctx context.Context) context.Context { + if c.authConfig != nil { + ctx = auth.WithConfig(ctx, c.authConfig) + } + return ctx +} diff --git a/pkg/datastore/client_test.go b/pkg/datastore/client_test.go index 1feaf81..eecec37 100644 --- a/pkg/datastore/client_test.go +++ b/pkg/datastore/client_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/datastore" ) @@ -58,10 +59,13 @@ func TestNewClientWithDatabase(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() + ctx := datastore.WithConfig(context.Background(), &datastore.Config{ + APIURL: apiServer.URL, + AuthConfig: &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + }, + }) // Test with explicit databaseID client, err := datastore.NewClientWithDatabase(ctx, "test-project", "custom-db") @@ -73,16 +77,21 @@ func TestNewClientWithDatabase(t *testing.T) { } } -func TestSetTestURLs(t *testing.T) { - // Save original values - restore := datastore.SetTestURLs("http://test1", "http://test2") - - // Restore should work - restore() +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) - // Should be chainable - restore2 := datastore.SetTestURLs("http://test3", "http://test4") - restore2() + // Context should be non-nil + if ctx == nil { + t.Fatal("expected non-nil context") + } } func TestNewClientWithDatabaseEmptyProjectID(t *testing.T) { @@ -122,10 +131,13 @@ func TestNewClientWithDatabaseEmptyProjectID(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() + ctx := datastore.WithConfig(context.Background(), &datastore.Config{ + APIURL: apiServer.URL, + AuthConfig: &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + }, + }) // Test with empty projectID - should fetch from metadata client, err := datastore.NewClientWithDatabase(ctx, "", "my-db") @@ -172,10 +184,13 @@ func TestNewClientWithDatabaseProjectIDFetchFailure(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() + ctx := datastore.WithConfig(context.Background(), &datastore.Config{ + APIURL: apiServer.URL, + AuthConfig: &auth.Config{ + MetadataURL: metadataServer.URL, + SkipADC: true, + }, + }) // Test with empty projectID and failing metadata server client, err := datastore.NewClientWithDatabase(ctx, "", "my-db") diff --git a/pkg/datastore/comprehensive_coverage_test.go b/pkg/datastore/comprehensive_coverage_test.go index 7d34116..4859713 100644 --- a/pkg/datastore/comprehensive_coverage_test.go +++ b/pkg/datastore/comprehensive_coverage_test.go @@ -99,11 +99,9 @@ func TestCountComprehensive(t *testing.T) { count, err := client.Count(ctx, q) if err != nil { t.Logf("Count with multiple filters: %v (may not be supported)", err) - } else { + } else if count != 2 { // Should count 2, 3 = 2 entities - if count != 2 { - t.Logf("Expected count 2 with multiple filters, got %d (composite filters may not be supported)", count) - } + t.Logf("Expected count 2 with multiple filters, got %d (composite filters may not be supported)", count) } }) } @@ -134,10 +132,8 @@ func TestGetAllComprehensive(t *testing.T) { keys, err := client.GetAll(ctx, q, &entities) if err != nil { t.Logf("GetAll with order: %v (ordering may not be implemented)", err) - } else { - if len(keys) != 5 { - t.Errorf("Expected 5 keys, got %d", len(keys)) - } + } else if len(keys) != 5 { + t.Errorf("Expected 5 keys, got %d", len(keys)) } }) @@ -160,10 +156,8 @@ func TestGetAllComprehensive(t *testing.T) { keys, err := client.GetAll(ctx, q, &entities) if err != nil { t.Logf("GetAll with offset: %v (offset may not be implemented)", err) - } else { - if len(keys) > 5 { - t.Errorf("Got too many keys: %d", len(keys)) - } + } else if len(keys) > 5 { + t.Errorf("Got too many keys: %d", len(keys)) } }) diff --git a/pkg/datastore/entity_coverage_test.go b/pkg/datastore/entity_coverage_test.go index 3d8017e..3875bfa 100644 --- a/pkg/datastore/entity_coverage_test.go +++ b/pkg/datastore/entity_coverage_test.go @@ -3,6 +3,8 @@ package datastore import ( "context" "errors" + "net/http" + "net/http/httptest" "testing" "time" ) @@ -292,19 +294,30 @@ func TestGet_ErrorCases(t *testing.T) { } func TestNewClientWithDatabase_Coverage(t *testing.T) { - SetTestURLs("http://localhost:8080/datastore", "http://localhost:8080/token") + // Use NewMockClient instead of hardcoded URLs to avoid port conflicts + client, cleanup := NewMockClient(t) + defer cleanup() + + if client == nil { + t.Fatal("expected non-nil client") + } + // Test with database ID using mock servers ctx := context.Background() + metadataServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer metadataServer.Close() - // Test with database ID - _, err := NewClientWithDatabase(ctx, "test-project", "test-db") - if err != nil { - // Expected to fail without real backend, but we exercise the code path - t.Logf("NewClientWithDatabase failed as expected: %v", err) - } + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer apiServer.Close() + + testCtx := TestConfig(ctx, metadataServer.URL, apiServer.URL) // Test with empty project (error case) - _, err = NewClientWithDatabase(ctx, "", "test-db") + _, err := NewClientWithDatabase(testCtx, "", "test-db") 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 fa07fcc..2a92924 100644 --- a/pkg/datastore/entity_test.go +++ b/pkg/datastore/entity_test.go @@ -459,15 +459,13 @@ func TestGetWithDecodeError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) var entity testEntity @@ -557,15 +555,13 @@ func TestDecodeValueInvalidInteger(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) var entity testEntity @@ -635,15 +631,13 @@ func TestDecodeValueWrongTypeForInteger(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) var entity testEntity @@ -713,15 +707,13 @@ func TestDecodeValueInvalidTimestamp(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) var entity testEntity @@ -794,15 +786,13 @@ func TestGetMultiDecodeError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - ctx := context.Background() keys := []*datastore.Key{ datastore.NameKey("Test", "key1", nil), datastore.NameKey("Test", "key2", nil), diff --git a/pkg/datastore/http_test.go b/pkg/datastore/http_test.go index 58cbbfc..1809683 100644 --- a/pkg/datastore/http_test.go +++ b/pkg/datastore/http_test.go @@ -65,10 +65,8 @@ func TestDoRequestRetryOn5xxError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -126,10 +124,8 @@ func TestDoRequestFailsOn4xxError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -191,10 +187,8 @@ func TestDoRequestContextCancellation(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") + 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) } @@ -260,10 +254,8 @@ func TestGetWithHTTPError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -318,10 +310,8 @@ func TestPutWithHTTPError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -378,10 +368,8 @@ func TestDoRequestAllRetriesFail(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -441,10 +429,8 @@ func TestDoRequestUnexpectedSuccess(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -498,10 +484,8 @@ func TestDoRequestWithReadBodyError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) diff --git a/pkg/datastore/iterator.go b/pkg/datastore/iterator.go index f33262b..f9b7c53 100644 --- a/pkg/datastore/iterator.go +++ b/pkg/datastore/iterator.go @@ -112,7 +112,7 @@ func (it *Iterator) fetch() error { } var result struct { - Batch struct { + Batch struct { //nolint:govet // Local anonymous struct for JSON unmarshaling EntityResults []struct { Entity map[string]any `json:"entity"` Cursor string `json:"cursor"` diff --git a/pkg/datastore/iterator_coverage_test.go b/pkg/datastore/iterator_coverage_test.go index f915dd4..8b2ab5c 100644 --- a/pkg/datastore/iterator_coverage_test.go +++ b/pkg/datastore/iterator_coverage_test.go @@ -209,7 +209,7 @@ func TestIteratorKeysOnly(t *testing.T) { Data string } key, err := it.Next(&entity) - if err == ErrDone { + if errors.Is(err, ErrDone) { break } if err != nil { diff --git a/pkg/datastore/mock_client.go b/pkg/datastore/mock_client.go index f728318..41356c7 100644 --- a/pkg/datastore/mock_client.go +++ b/pkg/datastore/mock_client.go @@ -4,11 +4,12 @@ import ( "context" "testing" + "github.com/codeGROOVE-dev/ds9/auth" "github.com/codeGROOVE-dev/ds9/pkg/mock" ) // NewMockClient creates a datastore client connected to mock servers with in-memory storage. -// This is a convenience wrapper that avoids import cycles when writing tests in package datastore. +// This is a convenience wrapper for testing. // Returns the client and a cleanup function that should be deferred. func NewMockClient(t *testing.T) (client *Client, cleanup func()) { t.Helper() @@ -16,23 +17,21 @@ func NewMockClient(t *testing.T) (client *Client, cleanup func()) { // Create mock servers metadataURL, apiURL, cleanup := mock.NewMockServers(t) - // Set test URLs - restore := SetTestURLs(metadataURL, apiURL) + // Create context with test configuration + ctx := WithConfig(context.Background(), &Config{ + APIURL: apiURL, + AuthConfig: &auth.Config{ + MetadataURL: metadataURL, + SkipADC: true, + }, + }) // Create client - ctx := context.Background() var err error client, err = NewClient(ctx, "test-project") if err != nil { t.Fatalf("failed to create mock client: %v", err) } - // Wrap cleanup to restore URLs - originalCleanup := cleanup - cleanup = func() { - restore() - originalCleanup() - } - return client, cleanup } diff --git a/pkg/datastore/mutation.go b/pkg/datastore/mutation.go index 6fb7874..c008b80 100644 --- a/pkg/datastore/mutation.go +++ b/pkg/datastore/mutation.go @@ -25,9 +25,9 @@ const ( // Mutation represents a pending datastore mutation. type Mutation struct { - op MutationOp key *Key entity any + op MutationOp } // NewInsert creates an insert mutation. @@ -72,6 +72,7 @@ func NewDelete(k *Key) *Mutation { // Mutate applies one or more mutations atomically. // API compatible with cloud.google.com/go/datastore. func (c *Client) Mutate(ctx context.Context, muts ...*Mutation) ([]*Key, error) { + ctx = c.withClientConfig(ctx) if len(muts) == 0 { return nil, nil } diff --git a/pkg/datastore/operations.go b/pkg/datastore/operations.go index d8eae0a..77d768a 100644 --- a/pkg/datastore/operations.go +++ b/pkg/datastore/operations.go @@ -15,6 +15,8 @@ import ( // dst must be a pointer to a struct. // Returns ErrNoSuchEntity if the key is not found. func (c *Client) Get(ctx context.Context, key *Key, dst any) error { + ctx = c.withClientConfig(ctx) + if key == nil { c.logger.WarnContext(ctx, "Get called with nil key") return errors.New("key cannot be nil") @@ -73,6 +75,7 @@ func (c *Client) Get(ctx context.Context, key *Key, dst any) error { // src must be a struct or pointer to struct. // Returns the key (useful for auto-generated IDs in the future). func (c *Client) Put(ctx context.Context, key *Key, src any) (*Key, error) { + ctx = c.withClientConfig(ctx) if key == nil { c.logger.WarnContext(ctx, "Put called with nil key") return nil, errors.New("key cannot be nil") @@ -119,6 +122,7 @@ func (c *Client) Put(ctx context.Context, key *Key, src any) (*Key, error) { // Delete deletes the entity with the given key. func (c *Client) Delete(ctx context.Context, key *Key) error { + ctx = c.withClientConfig(ctx) if key == nil { c.logger.WarnContext(ctx, "Delete called with nil key") return errors.New("key cannot be nil") @@ -162,6 +166,7 @@ func (c *Client) Delete(ctx context.Context, key *Key) error { // Returns ErrNoSuchEntity if any key is not found. // This matches the API of cloud.google.com/go/datastore. func (c *Client) GetMulti(ctx context.Context, keys []*Key, dst any) error { + ctx = c.withClientConfig(ctx) if len(keys) == 0 { c.logger.WarnContext(ctx, "GetMulti called with no keys") return errors.New("keys cannot be empty") @@ -256,6 +261,7 @@ func (c *Client) GetMulti(ctx context.Context, keys []*Key, dst any) error { // Returns the keys (same as input) and any error. // This matches the API of cloud.google.com/go/datastore. func (c *Client) PutMulti(ctx context.Context, keys []*Key, src any) ([]*Key, error) { + ctx = c.withClientConfig(ctx) if len(keys) == 0 { c.logger.WarnContext(ctx, "PutMulti called with no keys") return nil, errors.New("keys cannot be empty") @@ -329,6 +335,7 @@ func (c *Client) PutMulti(ctx context.Context, keys []*Key, src any) ([]*Key, er // DeleteMulti deletes multiple entities with their keys. // This matches the API of cloud.google.com/go/datastore. func (c *Client) DeleteMulti(ctx context.Context, keys []*Key) error { + ctx = c.withClientConfig(ctx) if len(keys) == 0 { c.logger.WarnContext(ctx, "DeleteMulti called with no keys") return errors.New("keys cannot be empty") @@ -383,6 +390,7 @@ func (c *Client) DeleteMulti(ctx context.Context, keys []*Key) error { // DeleteAllByKind deletes all entities of a given kind. // This method queries for all keys and then deletes them in batches. func (c *Client) DeleteAllByKind(ctx context.Context, kind string) error { + ctx = c.withClientConfig(ctx) c.logger.InfoContext(ctx, "deleting all entities by kind", "kind", kind) // Query for all keys of this kind @@ -412,6 +420,7 @@ func (c *Client) DeleteAllByKind(ctx context.Context, kind string) error { // Returns keys with IDs filled in. Complete keys are returned unchanged. // API compatible with cloud.google.com/go/datastore. func (c *Client) AllocateIDs(ctx context.Context, keys []*Key) ([]*Key, error) { + ctx = c.withClientConfig(ctx) if len(keys) == 0 { return keys, nil } diff --git a/pkg/datastore/operations_delete_test.go b/pkg/datastore/operations_delete_test.go new file mode 100644 index 0000000..55923ac --- /dev/null +++ b/pkg/datastore/operations_delete_test.go @@ -0,0 +1,875 @@ +package datastore_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/codeGROOVE-dev/ds9/pkg/datastore" +) + +func TestDelete(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put entity + entity := &testEntity{ + Name: "test-item", + Count: 42, + Active: true, + } + + key := datastore.NameKey("TestKind", "test-key", nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Delete entity + err = client.Delete(ctx, key) + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + + // Verify it's gone + var retrieved testEntity + err = client.Get(ctx, key, &retrieved) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity after delete, got %v", err) + } +} + +func TestMultiDelete(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put multiple entities + entities := []testEntity{ + {Name: "item-1", Count: 1}, + {Name: "item-2", Count: 2}, + {Name: "item-3", Count: 3}, + } + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + datastore.NameKey("TestKind", "key-2", nil), + datastore.NameKey("TestKind", "key-3", nil), + } + + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("MultiPut failed: %v", err) + } + + // MultiDelete + err = client.DeleteMulti(ctx, keys) + if err != nil { + t.Fatalf("MultiDelete failed: %v", err) + } + + // Verify they're gone by trying to get them + var retrieved []testEntity + err = client.GetMulti(ctx, keys, &retrieved) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity after delete, got %v", err) + } +} + +func TestMultiDeleteEmptyKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + var keys []*datastore.Key + + err := client.DeleteMulti(ctx, keys) + if err == nil { + t.Error("expected error for empty keys, got nil") + } +} + +func TestDeleteWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + err := client.Delete(ctx, nil) + if err == nil { + t.Error("expected error for nil key, got nil") + } +} + +func TestMultiDeleteWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + nil, + } + + err := client.DeleteMulti(ctx, keys) + if err == nil { + t.Error("expected error for nil key in slice, got nil") + } +} + +func TestMultiDeleteEmptySlice(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Call MultiDelete with empty slice - should return error + err := client.DeleteMulti(ctx, []*datastore.Key{}) + if err == nil { + t.Error("expected error for MultiDelete with empty keys, got nil") + } +} + +func TestDeleteWithDatabaseID(t *testing.T) { + // Setup with databaseID + 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 r.Header.Get("Authorization") != "Bearer test-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]any{ + "mutationResults": []any{}, + }); err != nil { + t.Logf("encode failed: %v", err) + } + })) + defer apiServer.Close() + + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + + client, err := datastore.NewClientWithDatabase(ctx, "test-project", "del-db") + if err != nil { + t.Fatalf("NewClientWithDatabase failed: %v", err) + } + + // Delete with databaseID + key := datastore.NameKey("TestKind", "to-delete", nil) + err = client.Delete(ctx, key) + if err != nil { + t.Fatalf("Delete with databaseID failed: %v", err) + } +} + +func TestMultiDeleteWithDatabaseID(t *testing.T) { + // Setup with databaseID + 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 r.Header.Get("Authorization") != "Bearer test-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]any{ + "mutationResults": []any{}, + }); err != nil { + t.Logf("encode failed: %v", err) + } + })) + defer apiServer.Close() + + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + + client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multidel-db") + if err != nil { + t.Fatalf("NewClientWithDatabase failed: %v", err) + } + + // MultiDelete with databaseID + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key1", nil), + datastore.NameKey("TestKind", "key2", nil), + } + err = client.DeleteMulti(ctx, keys) + if err != nil { + t.Fatalf("MultiDelete with databaseID failed: %v", err) + } +} + +func TestDeleteAllByKind(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put multiple entities of the same kind + for i := range 5 { + entity := &testEntity{ + Name: "item", + Count: int64(i), + } + key := datastore.NameKey("DeleteKind", string(rune('a'+i)), nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + } + + // Delete all entities of this kind + err := client.DeleteAllByKind(ctx, "DeleteKind") + if err != nil { + t.Fatalf("DeleteAllByKind failed: %v", err) + } + + // Verify all deleted + query := datastore.NewQuery("DeleteKind").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys failed: %v", err) + } + + if len(keys) != 0 { + t.Errorf("expected 0 keys after DeleteAllByKind, got %d", len(keys)) + } +} + +func TestDeleteAllByKindEmpty(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Delete from non-existent kind + err := client.DeleteAllByKind(ctx, "NonExistentKind") + if err != nil { + t.Errorf("DeleteAllByKind on empty kind should not error, got: %v", err) + } +} + +func TestDeleteMultiWithErrors(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) { + // Return server error + w.WriteHeader(http.StatusInternalServerError) + if _, err := w.Write([]byte(`{"error":"internal error"}`)); 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key1", nil), + datastore.NameKey("TestKind", "key2", nil), + } + + err = client.DeleteMulti(ctx, keys) + if err == nil { + t.Fatal("expected error on server failure") + } +} + +func TestDeleteWithNonexistentKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Delete non-existent key (should not error) + key := datastore.NameKey("Test", "nonexistent", nil) + err := client.Delete(ctx, key) + if err != nil { + t.Errorf("Delete of non-existent key should not error, got: %v", err) + } +} + +func TestDeleteMultiEmptySlice(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + err := client.DeleteMulti(ctx, []*datastore.Key{}) + if err == nil { + t.Error("expected error for empty keys") + } +} + +func TestDeleteAllByKindWithNoEntities(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Delete from kind with no entities + err := client.DeleteAllByKind(ctx, "NonExistentKind") + if err != nil { + t.Errorf("DeleteAllByKind on empty kind should not error, got: %v", err) + } +} + +func TestDeleteAllByKindWithManyEntities(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put many entities + for i := range 25 { + key := datastore.NameKey("ManyDelete", fmt.Sprintf("key-%d", i), nil) + entity := &testEntity{Name: "test", Count: int64(i)} + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + } + + // Delete all + err := client.DeleteAllByKind(ctx, "ManyDelete") + if err != nil { + t.Fatalf("DeleteAllByKind failed: %v", err) + } + + // Verify all deleted + query := datastore.NewQuery("ManyDelete").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys failed: %v", err) + } + + if len(keys) != 0 { + t.Errorf("expected 0 keys after DeleteAllByKind, got %d", len(keys)) + } +} + +func TestDeleteAllByKindQueryFailure(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) { + // Fail on query request + if strings.Contains(r.URL.Path, "runQuery") { + w.WriteHeader(http.StatusInternalServerError) + if _, err := w.Write([]byte(`{"error":"query failed"}`)); err != nil { + t.Logf("write failed: %v", err) + } + return + } + w.WriteHeader(http.StatusOK) + })) + 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) + } + + err = client.DeleteAllByKind(ctx, "TestKind") + + if err == nil { + t.Error("expected error when query fails") + } +} + +func TestDeleteMultiPartialSuccess(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put some entities + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + entities := []testEntity{ + {Name: "entity1", Count: 1}, + {Name: "entity2", Count: 2}, + } + + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("PutMulti failed: %v", err) + } + + // Delete them (should succeed) + err = client.DeleteMulti(ctx, keys) + if err != nil { + t.Fatalf("DeleteMulti failed: %v", err) + } + + // Verify deletion + var retrieved []testEntity + err = client.GetMulti(ctx, keys, &retrieved) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity after delete, got: %v", err) + } +} + +func TestDeleteWithServerError(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() + + attemptCount := 0 + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attemptCount++ + // Always return 503 + w.WriteHeader(http.StatusServiceUnavailable) + if _, err := w.Write([]byte(`{"error":"unavailable"}`)); 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + key := datastore.NameKey("Test", "key", nil) + err = client.Delete(ctx, key) + + if err == nil { + t.Error("expected error on persistent server failure") + } + + // Should have retried + if attemptCount < 2 { + t.Errorf("expected multiple attempts, got %d", attemptCount) + } +} + +func TestDeleteWithContextCancellation(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) { + // Slow response + time.Sleep(100 * time.Millisecond) + w.WriteHeader(http.StatusServiceUnavailable) + })) + 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) + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + key := datastore.NameKey("Test", "key", nil) + err = client.Delete(ctx, key) + + if err == nil { + t.Error("expected error when context is cancelled") + } +} + +func TestDeleteAllRetriesFail(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() + + requestCount := 0 + apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + // Always return 503 to force retries + w.WriteHeader(http.StatusServiceUnavailable) + })) + 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) + } + + key := datastore.NameKey("Test", "key", nil) + + err = client.Delete(ctx, key) + if err == nil { + t.Error("expected error after all retries exhausted") + } + + if !strings.Contains(err.Error(), "attempts") { + t.Errorf("expected error message about attempts, got: %v", err) + } + + if requestCount != 3 { + t.Errorf("expected 3 retry attempts, got %d", requestCount) + } +} + +func TestDeleteMultiWithEmptyKeysSlice(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + var keys []*datastore.Key // Empty + + err := client.DeleteMulti(ctx, keys) + // Mock may behave differently - log the result + if err != nil { + t.Logf("DeleteMulti with empty keys: %v", err) + } +} + +func TestDeleteWithJSONMarshalError(t *testing.T) { + // This is hard to trigger since we control the JSON structure + // But we can test with a context that gets cancelled + 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) { + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]any{}); err != nil { + t.Logf("encode failed: %v", err) + } + })) + 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) + } + + key := datastore.NameKey("Test", "key", nil) + + err = client.Delete(ctx, key) + if err != nil { + t.Logf("Delete completed with: %v", err) + } +} + +func TestDeleteAllByKindEmptyBatch(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 empty batch + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{}, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + err = client.DeleteAllByKind(ctx, "EmptyKind") + if err != nil { + t.Logf("DeleteAllByKind with empty batch: %v", err) + } +} + +func TestDeleteMultiMixedResults(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, ":commit") { + w.Header().Set("Content-Type", "application/json") + // Return empty mutation results + if err := json.NewEncoder(w).Encode(map[string]any{ + "mutationResults": []any{}, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + err = client.DeleteMulti(ctx, 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 new file mode 100644 index 0000000..c341436 --- /dev/null +++ b/pkg/datastore/operations_get_test.go @@ -0,0 +1,987 @@ +package datastore_test + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/codeGROOVE-dev/ds9/pkg/datastore" +) + +func TestGetNotFound(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + key := datastore.NameKey("TestKind", "nonexistent", nil) + var entity testEntity + err := client.Get(ctx, key, &entity) + + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity, got %v", err) + } +} + +func TestMultiGetNotFound(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put only one entity + entity := &testEntity{Name: "exists", Count: 1} + key1 := datastore.NameKey("TestKind", "exists", nil) + _, err := client.Put(ctx, key1, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Try to get multiple, one missing + keys := []*datastore.Key{ + key1, + datastore.NameKey("TestKind", "missing", nil), + } + + var retrieved []testEntity + err = client.GetMulti(ctx, keys, &retrieved) + + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity when some keys missing, got %v", err) + } +} + +func TestMultiGetEmptyKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + var keys []*datastore.Key + var retrieved []testEntity + + err := client.GetMulti(ctx, keys, &retrieved) + if err == nil { + t.Error("expected error for empty keys, got nil") + } +} + +func TestGetWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + var entity testEntity + err := client.Get(ctx, nil, &entity) + if err == nil { + t.Error("expected error for nil key, got nil") + } +} + +func TestMultiGetWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + nil, + datastore.NameKey("TestKind", "key-2", nil), + } + + var entities []testEntity + err := client.GetMulti(ctx, keys, &entities) + if err == nil { + t.Error("expected error for nil key in slice, got nil") + } +} + +func TestMultiGetPartialResults(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put some entities + entities := []testEntity{ + {Name: "item-1", Count: 1}, + {Name: "item-3", Count: 3}, + } + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + datastore.NameKey("TestKind", "key-3", nil), + } + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("MultiPut failed: %v", err) + } + + // Try to get more keys than exist + getAllKeys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + datastore.NameKey("TestKind", "key-2", nil), // doesn't exist + datastore.NameKey("TestKind", "key-3", nil), + } + + var retrieved []testEntity + err = client.GetMulti(ctx, getAllKeys, &retrieved) + if err == nil { + t.Error("expected error when some keys don't exist") + } +} + +func TestMultiGetEmptySlices(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Call MultiGet with empty slices - should return error + var entities []testEntity + err := client.GetMulti(ctx, []*datastore.Key{}, &entities) + if err == nil { + t.Error("expected error for MultiGet with empty keys, got nil") + } +} + +func TestMultiGetWithDatabaseID(t *testing.T) { + // Setup with databaseID + 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 r.Header.Get("Authorization") != "Bearer test-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + // Return missing entities to trigger datastore.ErrNoSuchEntity + if err := json.NewEncoder(w).Encode(map[string]any{ + "found": []any{}, + "missing": []any{ + map[string]any{"entity": map[string]any{"key": map[string]any{}}}, + }, + }); err != nil { + t.Logf("encode failed: %v", err) + } + })) + defer apiServer.Close() + + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + + client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multiget-db") + if err != nil { + t.Fatalf("NewClientWithDatabase failed: %v", err) + } + + // MultiGet with databaseID + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key1", nil), + datastore.NameKey("TestKind", "key2", nil), + } + var entities []testEntity + err = client.GetMulti(ctx, keys, &entities) + // Expect error since entities don't exist + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity, got: %v", err) + } +} + +func TestGetMultiWithMismatchedSliceSize(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put one entity + key1 := datastore.NameKey("TestKind", "key1", nil) + entity := &testEntity{Name: "test", Count: 1} + _, err := client.Put(ctx, key1, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Try to get with wrong slice type + keys := []*datastore.Key{key1} + var retrieved []testEntity + + // This should work + err = client.GetMulti(ctx, keys, &retrieved) + if err != nil { + t.Fatalf("GetMulti failed: %v", err) + } + + if len(retrieved) != 1 { + t.Errorf("expected 1 entity, got %d", len(retrieved)) + } +} + +func TestGetMultiMixedResults(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put some entities + key1 := datastore.NameKey("Mixed", "exists1", nil) + key2 := datastore.NameKey("Mixed", "exists2", nil) + key3 := datastore.NameKey("Mixed", "missing", nil) + + entities := []testEntity{ + {Name: "entity1", Count: 1}, + {Name: "entity2", Count: 2}, + } + + _, err := client.PutMulti(ctx, []*datastore.Key{key1, key2}, entities) + if err != nil { + t.Fatalf("PutMulti failed: %v", err) + } + + // Try to get mix of existing and non-existing + keys := []*datastore.Key{key1, key2, key3} + var retrieved []testEntity + + err = client.GetMulti(ctx, keys, &retrieved) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity for mixed results, got: %v", err) + } +} + +func TestGetMultiAllMissing(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + keys := []*datastore.Key{ + datastore.NameKey("Missing", "key1", nil), + datastore.NameKey("Missing", "key2", nil), + datastore.NameKey("Missing", "key3", nil), + } + + var entities []testEntity + err := client.GetMulti(ctx, keys, &entities) + + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity when all keys missing, got: %v", err) + } +} + +func TestGetMultiWithSliceMismatch(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put entity + key := datastore.NameKey("Test", "key1", nil) + entity := &testEntity{Name: "test", Count: 1} + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // GetMulti with destination not being a pointer to slice + var notSlice testEntity + err = client.GetMulti(ctx, []*datastore.Key{key}, notSlice) + if err == nil { + t.Error("expected error when dst is not pointer to slice") + } +} + +func TestGetMultiEmptySlice(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + var entities []testEntity + err := client.GetMulti(ctx, []*datastore.Key{}, &entities) + if err == nil { + t.Error("expected error for empty keys") + } +} + +func TestGetWithNonPointerDst(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put entity + key := datastore.NameKey("Test", "key", nil) + entity := &testEntity{Name: "test", Count: 1} + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Try to get into non-pointer + var notPointer testEntity + err = client.Get(ctx, key, notPointer) // Should be ¬Pointer + if err == nil { + t.Error("expected error when dst is not a pointer") + } +} + +func TestGetMultiWithNonSliceDst(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + } + + // Pass a non-slice as destination + var notSlice string + err := client.GetMulti(ctx, keys, ¬Slice) + + if err == nil { + t.Error("expected error when dst is not a slice") + } +} + +func TestGetWithInvalidJSONResponse(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) { + // 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) + } + })) + 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) + } + + key := datastore.NameKey("Test", "key", nil) + var entity testEntity + err = client.Get(ctx, key, &entity) + + if err == nil { + t.Error("expected error for invalid JSON response") + } +} + +func TestGetMultiWithNilInResults(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put one entity + key1 := datastore.NameKey("Test", "exists", nil) + entity := &testEntity{Name: "test", Count: 1} + _, err := client.Put(ctx, key1, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Try to get multiple with one missing + keys := []*datastore.Key{ + key1, + datastore.NameKey("Test", "missing", nil), + datastore.NameKey("Test", "missing2", nil), + } + + var entities []testEntity + err = client.GetMulti(ctx, keys, &entities) + + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity when some keys missing, got: %v", err) + } +} + +func TestGetMultiWithServerError(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) { + 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + } + + var entities []testEntity + err = client.GetMulti(ctx, keys, &entities) + + if err == nil { + t.Error("expected error on unauthorized") + } + + if !strings.Contains(err.Error(), "401") { + t.Errorf("expected 401 error, got: %v", err) + } +} + +func TestGetMultiPartialNotFound(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, ":lookup") { + // Return one found, one missing + 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": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "Test", + "name": "key1", + }, + }, + }, + "properties": map[string]any{ + "name": map[string]any{"stringValue": "test1"}, + }, + }, + }, + }, + "missing": []map[string]any{ + { + "key": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "Test", + "name": "key2", + }, + }, + }, + }, + }, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + var entities []testEntity + err = client.GetMulti(ctx, keys, &entities) + if err == nil { + t.Error("expected error when some entities are missing") + } else { + t.Logf("GetMulti with missing entities failed as expected: %v", err) + } +} + +func TestGetWithNonPointer(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + key := datastore.NameKey("Test", "key", nil) + var entity testEntity // non-pointer + + err := client.Get(ctx, key, entity) // Pass by value + if err == nil { + t.Error("expected error when dst is not a pointer") + } +} + +func TestGetMultiMismatchedLength(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + var entities []testEntity // Empty slice + + err := client.GetMulti(ctx, keys, &entities) + // This should work - GetMulti should populate the slice + if err != nil { + t.Logf("GetMulti with empty slice: %v", err) + } +} + +func TestGetWithJSONUnmarshalError(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, ":lookup") { + // Return invalid JSON + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte(`{"found": [{"entity": "not-an-object"}]}`)); 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + key := datastore.NameKey("Test", "key", nil) + var entity testEntity + + err = client.Get(ctx, key, &entity) + if err == nil { + t.Error("expected error with invalid entity format") + } +} + +func TestGetWithStringIDKey(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, ":lookup") { + w.Header().Set("Content-Type", "application/json") + // Return entity with ID as string + if err := json.NewEncoder(w).Encode(map[string]any{ + "found": []map[string]any{ + { + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "TestKind", + "id": "12345", // ID as string + }, + }, + }, + "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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + type TestEntity struct { + Name string `datastore:"name"` + } + + key := datastore.IDKey("TestKind", 12345, nil) + var entity TestEntity + err = client.Get(ctx, key, &entity) + if err != nil { + t.Fatalf("Get with string ID key failed: %v", err) + } + + if entity.Name != "test" { + t.Errorf("expected name 'test', got %q", entity.Name) + } +} + +func TestGetWithFloat64IDKey(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, ":lookup") { + w.Header().Set("Content-Type", "application/json") + // Return entity with ID as float64 + if err := json.NewEncoder(w).Encode(map[string]any{ + "found": []map[string]any{ + { + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "TestKind", + "id": float64(67890), // ID as float64 + }, + }, + }, + "properties": map[string]any{ + "value": map[string]any{"integerValue": "42"}, + }, + }, + }, + }, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + type TestEntity struct { + Value int64 `datastore:"value"` + } + + key := datastore.IDKey("TestKind", 67890, nil) + var entity TestEntity + err = client.Get(ctx, key, &entity) + if err != nil { + t.Fatalf("Get with float64 ID key failed: %v", err) + } + + if entity.Value != 42 { + t.Errorf("expected value 42, got %d", entity.Value) + } +} + +func TestGetWithInvalidStringIDFormat(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, ":lookup") { + w.Header().Set("Content-Type", "application/json") + // Return entity with invalid ID string format + if err := json.NewEncoder(w).Encode(map[string]any{ + "found": []map[string]any{ + { + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "TestKind", + "id": "not-a-number", // Invalid ID format + }, + }, + }, + "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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + type TestEntity struct { + Name string `datastore:"name"` + } + + key := datastore.IDKey("TestKind", 12345, nil) + var entity TestEntity + err = client.Get(ctx, key, &entity) + // May or may not error depending on parsing behavior + if err != nil { + t.Logf("Get with invalid string ID format failed: %v", err) + } else { + t.Logf("Get with invalid string ID format succeeded unexpectedly") + } +} + +func TestGetJSONUnmarshalError(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, ":lookup") { + // Return malformed JSON + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write([]byte("not valid 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + key := datastore.NameKey("Test", "test-key", nil) + var entity testEntity + + err = client.Get(ctx, 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 new file mode 100644 index 0000000..777e2f2 --- /dev/null +++ b/pkg/datastore/operations_misc_test.go @@ -0,0 +1,1506 @@ +package datastore_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/codeGROOVE-dev/ds9/pkg/datastore" +) + +func TestAllKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put multiple entities + for i := range 5 { + entity := &testEntity{ + Name: "test-item", + Count: int64(i), + } + key := datastore.NameKey("TestKind", string(rune('a'+i)), nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + } + + // Query for all keys + query := datastore.NewQuery("TestKind").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys failed: %v", err) + } + + if len(keys) != 5 { + t.Errorf("expected 5 keys, got %d", len(keys)) + } +} + +func TestAllKeysWithLimit(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put multiple entities + for i := range 10 { + entity := &testEntity{ + Name: "test-item", + Count: int64(i), + } + key := datastore.NameKey("TestKind", string(rune('a'+i)), nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + } + + // Query with limit + query := datastore.NewQuery("TestKind").KeysOnly().Limit(3) + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys failed: %v", err) + } + + if len(keys) != 3 { + t.Errorf("expected 3 keys, got %d", len(keys)) + } +} + +func TestIDKey(t *testing.T) { + key := datastore.IDKey("TestKind", 12345, nil) + + if key.Kind != "TestKind" { + t.Errorf("expected Kind %q, got %q", "TestKind", key.Kind) + } + + if key.ID != 12345 { + t.Errorf("expected ID %d, got %d", 12345, key.ID) + } + + if key.Name != "" { + t.Errorf("expected empty Name, got %q", key.Name) + } +} + +func TestNameKey(t *testing.T) { + key := datastore.NameKey("TestKind", "test-name", nil) + + if key.Kind != "TestKind" { + t.Errorf("expected Kind %q, got %q", "TestKind", key.Kind) + } + + if key.Name != "test-name" { + t.Errorf("expected Name %q, got %q", "test-name", key.Name) + } + + if key.ID != 0 { + t.Errorf("expected ID 0, got %d", key.ID) + } +} + +func TestIDKeyOperations(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Test with ID key + entity := &testEntity{ + Name: "id-test", + Count: 123, + } + + key := datastore.IDKey("TestKind", 999, nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with ID key failed: %v", err) + } + + // Get with ID key + var retrieved testEntity + err = client.Get(ctx, key, &retrieved) + if err != nil { + t.Fatalf("Get with ID key failed: %v", err) + } + + if retrieved.Name != "id-test" { + t.Errorf("expected Name 'id-test', got %q", retrieved.Name) + } +} + +func TestAllKeysNonKeysOnlyQuery(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create a query without KeysOnly + query := datastore.NewQuery("TestKind") + _, err := client.AllKeys(ctx, query) + if err == nil { + t.Error("expected error for non-KeysOnly query, got nil") + } +} + +func TestKeyComparison(t *testing.T) { + nameKey1 := datastore.NameKey("Kind", "name", nil) + nameKey2 := datastore.NameKey("Kind", "name", nil) + + if nameKey1.Kind != nameKey2.Kind || nameKey1.Name != nameKey2.Name { + t.Error("identical name keys should have same values") + } + + idKey1 := datastore.IDKey("Kind", 123, nil) + idKey2 := datastore.IDKey("Kind", 123, nil) + + if idKey1.Kind != idKey2.Kind || idKey1.ID != idKey2.ID { + t.Error("identical ID keys should have same values") + } +} + +func TestLargeEntityBatch(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create a larger batch + const batchSize = 50 + entities := make([]testEntity, batchSize) + keys := make([]*datastore.Key, batchSize) + + for i := range batchSize { + entities[i] = testEntity{ + Name: "batch-item", + Count: int64(i), + } + keys[i] = datastore.NameKey("BatchKind", string(rune('0'+i/10))+string(rune('0'+i%10)), nil) + } + + // MultiPut + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("MultiPut failed: %v", err) + } + + // MultiGet + var retrieved []testEntity + err = client.GetMulti(ctx, keys, &retrieved) + if err != nil { + t.Fatalf("MultiGet failed: %v", err) + } + + if len(retrieved) != batchSize { + t.Errorf("expected %d entities, got %d", batchSize, len(retrieved)) + } + + // MultiDelete + err = client.DeleteMulti(ctx, keys) + if err != nil { + t.Fatalf("MultiDelete failed: %v", err) + } + + // Verify deletion + var retrieved2 []testEntity + err = client.GetMulti(ctx, keys, &retrieved2) + if !errors.Is(err, datastore.ErrNoSuchEntity) { + t.Errorf("expected datastore.ErrNoSuchEntity after batch delete, got %v", err) + } +} + +func TestAllKeysWithDatabaseID(t *testing.T) { + // Setup with databaseID + 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 r.Header.Get("Authorization") != "Bearer test-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{ + "entityResults": []any{}, + }, + }); err != nil { + t.Logf("encode failed: %v", err) + } + })) + defer apiServer.Close() + + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) + + client, err := datastore.NewClientWithDatabase(ctx, "test-project", "query-db") + if err != nil { + t.Fatalf("NewClientWithDatabase failed: %v", err) + } + + // Query with databaseID + query := datastore.NewQuery("TestKind").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys with databaseID failed: %v", err) + } + + if len(keys) != 0 { + t.Errorf("expected 0 keys, got %d", len(keys)) + } +} + +func TestHierarchicalKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create parent key + parentKey := datastore.NameKey("Parent", "parent1", nil) + parentEntity := &testEntity{ + Name: "parent", + Count: 1, + } + _, err := client.Put(ctx, parentKey, parentEntity) + if err != nil { + t.Fatalf("Put parent failed: %v", err) + } + + // Create child key with parent + childKey := datastore.NameKey("Child", "child1", parentKey) + childEntity := &testEntity{ + Name: "child", + Count: 2, + } + _, err = client.Put(ctx, childKey, childEntity) + if err != nil { + t.Fatalf("Put child failed: %v", err) + } + + // Get child + var retrieved testEntity + err = client.Get(ctx, childKey, &retrieved) + if err != nil { + t.Fatalf("Get child failed: %v", err) + } + + if retrieved.Name != "child" { + t.Errorf("expected child name 'child', got %q", retrieved.Name) + } +} + +func TestHierarchicalKeysMultiLevel(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create grandparent -> parent -> child hierarchy + grandparentKey := datastore.NameKey("Grandparent", "gp1", nil) + parentKey := datastore.NameKey("Parent", "p1", grandparentKey) + childKey := datastore.NameKey("Child", "c1", parentKey) + + entity := &testEntity{ + Name: "deep-child", + Count: 42, + } + + _, err := client.Put(ctx, childKey, entity) + if err != nil { + t.Fatalf("Put with multi-level hierarchy failed: %v", err) + } + + var retrieved testEntity + err = client.Get(ctx, childKey, &retrieved) + if err != nil { + t.Fatalf("Get with multi-level hierarchy failed: %v", err) + } + + if retrieved.Name != "deep-child" { + t.Errorf("expected name 'deep-child', got %q", retrieved.Name) + } +} + +func TestKeyFromJSONEdgeCases(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Test with ID key using integer ID + idKey := datastore.IDKey("TestKind", 12345, nil) + entity := &testEntity{Name: "id-test", Count: 1} + _, err := client.Put(ctx, idKey, entity) + if err != nil { + t.Fatalf("Put with ID key failed: %v", err) + } + + var retrieved testEntity + err = client.Get(ctx, idKey, &retrieved) + if err != nil { + t.Fatalf("Get with ID key failed: %v", err) + } + + if retrieved.Name != "id-test" { + t.Errorf("expected name 'id-test', got %q", retrieved.Name) + } +} + +func TestKeyWithOnlyKind(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Key with neither name nor ID should work (incomplete key) + // This gets an ID assigned by the datastore + key := &datastore.Key{Kind: "TestKind"} + entity := &testEntity{Name: "test", Count: 1} + + returnedKey, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with incomplete key failed: %v", err) + } + + // The returned key should have an ID + if returnedKey == nil { + t.Fatal("expected non-nil returned key") + } + + if returnedKey.Kind != "TestKind" { + t.Errorf("expected Kind 'TestKind', got %q", returnedKey.Kind) + } +} + +func TestAllKeysWithEmptyResult(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Query kind with no entities + query := datastore.NewQuery("EmptyKind").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys on empty kind failed: %v", err) + } + + if len(keys) != 0 { + t.Errorf("expected 0 keys, got %d", len(keys)) + } +} + +func TestAllKeysWithLargeResult(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Put many entities + for i := range 50 { + key := datastore.NameKey("LargeResult", fmt.Sprintf("key-%d", i), nil) + entity := &testEntity{Name: "test", Count: int64(i)} + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + } + + // Query all + query := datastore.NewQuery("LargeResult").KeysOnly() + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Fatalf("AllKeys failed: %v", err) + } + + if len(keys) != 50 { + t.Errorf("expected 50 keys, got %d", len(keys)) + } +} + +func TestDeepHierarchicalKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create 4-level hierarchy + gp := datastore.NameKey("GP", "gp1", nil) + p := datastore.NameKey("P", "p1", gp) + c := datastore.NameKey("C", "c1", p) + gc := datastore.NameKey("GC", "gc1", c) + + entity := &testEntity{Name: "great-grandchild", Count: 42} + _, err := client.Put(ctx, gc, entity) + if err != nil { + t.Fatalf("Put with 4-level hierarchy failed: %v", err) + } + + var retrieved testEntity + err = client.Get(ctx, gc, &retrieved) + if err != nil { + t.Fatalf("Get with 4-level hierarchy failed: %v", err) + } + + if retrieved.Name != "great-grandchild" { + t.Errorf("expected name 'great-grandchild', got %q", retrieved.Name) + } +} + +func TestIDKeyWithZeroID(t *testing.T) { + // Zero ID is valid + key := datastore.IDKey("Test", 0, nil) + if key.ID != 0 { + t.Errorf("expected ID 0, got %d", key.ID) + } + if key.Name != "" { + t.Errorf("expected empty Name, got %q", key.Name) + } +} + +func TestNameKeyWithEmptyName(t *testing.T) { + // Empty name is technically valid + key := datastore.NameKey("Test", "", nil) + if key.Name != "" { + t.Errorf("expected empty Name, got %q", key.Name) + } + if key.ID != 0 { + t.Errorf("expected ID 0, got %d", key.ID) + } +} + +func TestAllKeysQueryWithoutKeysOnly(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create query without KeysOnly + query := datastore.NewQuery("Test") + + _, err := client.AllKeys(ctx, query) + + if err == nil { + t.Error("expected error for query without KeysOnly") + } + + if !strings.Contains(err.Error(), "KeysOnly") { + t.Errorf("expected error to mention KeysOnly, got: %v", err) + } +} + +func TestAllKeysWithInvalidResponse(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) { + // Return invalid JSON + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte(`{malformed`)); 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") + 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) + } +} + +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") + 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 with invalid JSON") + } +} + +// Test Transaction commit with invalid response + +func TestAllKeysNotKeysOnlyError(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + query := datastore.NewQuery("Test") // Not KeysOnly + + _, err := client.AllKeys(ctx, query) + if err == nil { + t.Error("expected error when query is not KeysOnly") + } +} + +func TestAllKeysWithBatching(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 multiple key results + results := make([]map[string]any, 50) + for i := range 50 { + results[i] = map[string]any{ + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{ + map[string]any{ + "kind": "Test", + "name": fmt.Sprintf("key%d", i), + }, + }, + }, + }, + } + } + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{ + "entityResults": results, + }, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + query := datastore.NewQuery("Test").KeysOnly() + + keys, err := client.AllKeys(ctx, query) + if err != nil { + t.Logf("AllKeys with many results: %v", err) + } else if len(keys) != 50 { + t.Logf("Expected 50 keys, got %d", len(keys)) + } +} + +func TestAllKeysKeyFromJSONError(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 result with invalid key format + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{ + "entityResults": []map[string]any{ + { + "entity": map[string]any{ + "key": "not-a-map", // Invalid key format + }, + }, + }, + }, + }); 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) + 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 with invalid key format") + } +} + +func TestAllKeysEmptyPathInKey(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") { + w.Header().Set("Content-Type", "application/json") + // Return key with empty path array + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{ + "entityResults": []map[string]any{ + { + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{}, // Empty path + }, + }, + }, + }, + }, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + query := datastore.NewQuery("TestKind").KeysOnly() + _, err = client.AllKeys(ctx, query) + if err == nil { + t.Error("expected error with empty path in key") + } +} + +func TestAllKeysInvalidPathElement(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") { + w.Header().Set("Content-Type", "application/json") + // Return key with invalid path element (string instead of map) + if err := json.NewEncoder(w).Encode(map[string]any{ + "batch": map[string]any{ + "entityResults": []map[string]any{ + { + "entity": map[string]any{ + "key": map[string]any{ + "path": []any{"invalid-element"}, // String instead of map + }, + }, + }, + }, + }, + }); 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) + client, err := datastore.NewClient(ctx, "test-project") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + query := datastore.NewQuery("TestKind").KeysOnly() + _, err = client.AllKeys(ctx, query) + if err == nil { + t.Error("expected error with invalid path element") + } +} + +func TestBackwardsCompatibility(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Test 1: Close() method exists and can be called (even though it's a no-op) + t.Run("Close", func(t *testing.T) { + err := client.Close() + if err != nil { + t.Errorf("Close() returned error: %v", err) + } + }) + + // Test 2: RunInTransaction returns (*Commit, error) + t.Run("RunInTransactionSignature", func(t *testing.T) { + key := datastore.NameKey("TestKind", "test-tx-compat", nil) + + commit, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { + entity := &testEntity{ + Name: "transaction test", + Count: 100, + Active: true, + Score: 99.9, + UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), + } + _, err := tx.Put(key, entity) + return err + }) + if err != nil { + t.Fatalf("RunInTransaction failed: %v", err) + } + + if commit == nil { + t.Error("Expected non-nil Commit, got nil") + } + }) + + // Test 3: GetAll() method retrieves entities and returns keys + t.Run("GetAll", func(t *testing.T) { + // Setup: Create some test entities + entities := []testEntity{ + {Name: "entity1", Count: 1, Active: true, Score: 1.1, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, + {Name: "entity2", Count: 2, Active: false, Score: 2.2, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, + {Name: "entity3", Count: 3, Active: true, Score: 3.3, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, + } + + keys := []*datastore.Key{ + datastore.NameKey("GetAllTest", "key1", nil), + datastore.NameKey("GetAllTest", "key2", nil), + datastore.NameKey("GetAllTest", "key3", nil), + } + + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("PutMulti failed: %v", err) + } + + // Test GetAll + query := datastore.NewQuery("GetAllTest") + var results []testEntity + returnedKeys, err := client.GetAll(ctx, query, &results) + if err != nil { + t.Fatalf("GetAll failed: %v", err) + } + + if len(results) != 3 { + t.Errorf("Expected 3 entities, got %d", len(results)) + } + + if len(returnedKeys) != 3 { + t.Errorf("Expected 3 keys, got %d", len(returnedKeys)) + } + + // Verify entities were properly decoded + foundNames := make(map[string]bool) + for _, entity := range results { + foundNames[entity.Name] = true + } + + for _, expectedName := range []string{"entity1", "entity2", "entity3"} { + if !foundNames[expectedName] { + t.Errorf("Expected to find entity %s, but didn't", expectedName) + } + } + + // Verify keys match entities + for i, key := range returnedKeys { + if key.Kind != "GetAllTest" { + t.Errorf("Key %d has wrong kind: %s", i, key.Kind) + } + } + }) + + // Test 4: GetAll with limit + t.Run("GetAllWithLimit", func(t *testing.T) { + query := datastore.NewQuery("GetAllTest").Limit(2) + var results []testEntity + returnedKeys, err := client.GetAll(ctx, query, &results) + if err != nil { + t.Fatalf("GetAll with limit failed: %v", err) + } + + if len(results) != 2 { + t.Errorf("Expected 2 entities with limit, got %d", len(results)) + } + + if len(returnedKeys) != 2 { + t.Errorf("Expected 2 keys with limit, got %d", len(returnedKeys)) + } + }) +} + +func TestArraySliceSupport(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + t.Run("StringSlice", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "strings", nil) + entity := &arrayEntity{ + Strings: []string{"hello", "world", "test"}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with string slice failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(result.Strings) != 3 { + t.Errorf("Expected 3 strings, got %d", len(result.Strings)) + } + if result.Strings[0] != "hello" || result.Strings[1] != "world" || result.Strings[2] != "test" { + t.Errorf("String slice values incorrect: %v", result.Strings) + } + }) + + t.Run("Int64Slice", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "ints", nil) + entity := &arrayEntity{ + Ints: []int64{1, 2, 3, 42, 100}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with int64 slice failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(result.Ints) != 5 { + t.Errorf("Expected 5 ints, got %d", len(result.Ints)) + } + if result.Ints[3] != 42 { + t.Errorf("Expected Ints[3] = 42, got %d", result.Ints[3]) + } + }) + + t.Run("Float64Slice", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "floats", nil) + entity := &arrayEntity{ + Floats: []float64{1.1, 2.2, 3.3}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with float64 slice failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(result.Floats) != 3 { + t.Errorf("Expected 3 floats, got %d", len(result.Floats)) + } + if result.Floats[0] != 1.1 { + t.Errorf("Expected Floats[0] = 1.1, got %f", result.Floats[0]) + } + }) + + t.Run("BoolSlice", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "bools", nil) + entity := &arrayEntity{ + Bools: []bool{true, false, true}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with bool slice failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(result.Bools) != 3 { + t.Errorf("Expected 3 bools, got %d", len(result.Bools)) + } + if result.Bools[0] != true || result.Bools[1] != false { + t.Errorf("Bool slice values incorrect: %v", result.Bools) + } + }) + + t.Run("EmptySlices", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "empty", nil) + entity := &arrayEntity{ + Strings: []string{}, + Ints: []int64{}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with empty slices failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if result.Strings == nil || len(result.Strings) != 0 { + t.Errorf("Expected empty string slice, got %v", result.Strings) + } + if result.Ints == nil || len(result.Ints) != 0 { + t.Errorf("Expected empty int slice, got %v", result.Ints) + } + }) + + t.Run("MixedArrays", func(t *testing.T) { + key := datastore.NameKey("ArrayTest", "mixed", nil) + entity := &arrayEntity{ + Strings: []string{"a", "b"}, + Ints: []int64{10, 20, 30}, + Floats: []float64{1.5}, + Bools: []bool{true, false, true, false}, + } + + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put with mixed arrays failed: %v", err) + } + + var result arrayEntity + if err := client.Get(ctx, key, &result); err != nil { + t.Fatalf("Get failed: %v", err) + } + + if len(result.Strings) != 2 || len(result.Ints) != 3 || len(result.Floats) != 1 || len(result.Bools) != 4 { + t.Errorf("Mixed array lengths incorrect: strings=%d, ints=%d, floats=%d, bools=%d", + len(result.Strings), len(result.Ints), len(result.Floats), len(result.Bools)) + } + }) +} + +func TestAllocateIDs(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + t.Run("AllocateIncompleteKeys", func(t *testing.T) { + keys := []*datastore.Key{ + datastore.IncompleteKey("Task", nil), + datastore.IncompleteKey("Task", nil), + datastore.IncompleteKey("Task", nil), + } + + allocated, err := client.AllocateIDs(ctx, keys) + if err != nil { + t.Fatalf("AllocateIDs failed: %v", err) + } + + if len(allocated) != 3 { + t.Errorf("Expected 3 allocated keys, got %d", len(allocated)) + } + + for i, key := range allocated { + if key.Incomplete() { + t.Errorf("Key %d is still incomplete", i) + } + if key.ID == 0 { + t.Errorf("Key %d has zero ID", i) + } + } + }) + + t.Run("AllocateMixedKeys", func(t *testing.T) { + keys := []*datastore.Key{ + datastore.NameKey("Task", "complete", nil), + datastore.IncompleteKey("Task", nil), + datastore.IDKey("Task", 123, nil), + datastore.IncompleteKey("Task", nil), + } + + allocated, err := client.AllocateIDs(ctx, keys) + if err != nil { + t.Fatalf("AllocateIDs with mixed keys failed: %v", err) + } + + if len(allocated) != 4 { + t.Errorf("Expected 4 keys, got %d", len(allocated)) + } + + // First key should still be the named key + if allocated[0].Name != "complete" { + t.Errorf("First key should be unchanged") + } + + // Second key should now have an ID + if allocated[1].Incomplete() { + t.Errorf("Second key should be allocated") + } + + // Third key should be unchanged + if allocated[2].ID != 123 { + t.Errorf("Third key should be unchanged") + } + + // Fourth key should now have an ID + if allocated[3].Incomplete() { + t.Errorf("Fourth key should be allocated") + } + }) + + t.Run("AllocateEmptySlice", func(t *testing.T) { + keys := []*datastore.Key{} + + allocated, err := client.AllocateIDs(ctx, keys) + if err != nil { + t.Fatalf("AllocateIDs with empty slice failed: %v", err) + } + + if len(allocated) != 0 { + t.Errorf("Expected empty slice, got %d keys", len(allocated)) + } + }) + + t.Run("AllocateAllCompleteKeys", func(t *testing.T) { + keys := []*datastore.Key{ + datastore.NameKey("Task", "key1", nil), + datastore.IDKey("Task", 100, nil), + } + + allocated, err := client.AllocateIDs(ctx, keys) + if err != nil { + t.Fatalf("AllocateIDs with complete keys failed: %v", err) + } + + if len(allocated) != 2 { + t.Errorf("Expected 2 keys, got %d", len(allocated)) + } + + // Keys should be unchanged + if allocated[0].Name != "key1" || allocated[1].ID != 100 { + t.Errorf("Complete keys should be unchanged") + } + }) +} diff --git a/pkg/datastore/operations_put_test.go b/pkg/datastore/operations_put_test.go new file mode 100644 index 0000000..7f36ff8 --- /dev/null +++ b/pkg/datastore/operations_put_test.go @@ -0,0 +1,592 @@ +package datastore_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/codeGROOVE-dev/ds9/pkg/datastore" +) + +func TestPutAndGet(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create test entity + now := time.Now().UTC().Truncate(time.Second) + entity := &testEntity{ + Name: "test-item", + Count: 42, + Active: true, + Score: 3.14, + UpdatedAt: now, + Notes: "This is a test note", + } + + // Put entity + key := datastore.NameKey("TestKind", "test-key", nil) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Fatalf("Put failed: %v", err) + } + + // Get entity + var retrieved testEntity + err = client.Get(ctx, key, &retrieved) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + + // Verify fields + if retrieved.Name != entity.Name { + t.Errorf("Name: expected %q, got %q", entity.Name, retrieved.Name) + } + if retrieved.Count != entity.Count { + t.Errorf("Count: expected %d, got %d", entity.Count, retrieved.Count) + } + if retrieved.Active != entity.Active { + t.Errorf("Active: expected %v, got %v", entity.Active, retrieved.Active) + } + if retrieved.Score != entity.Score { + t.Errorf("Score: expected %f, got %f", entity.Score, retrieved.Score) + } + if !retrieved.UpdatedAt.Equal(entity.UpdatedAt) { + t.Errorf("UpdatedAt: expected %v, got %v", entity.UpdatedAt, retrieved.UpdatedAt) + } + if retrieved.Notes != entity.Notes { + t.Errorf("Notes: expected %q, got %q", entity.Notes, retrieved.Notes) + } +} + +func TestMultiPutAndMultiGet(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create test entities + now := time.Now().UTC().Truncate(time.Second) + entities := []testEntity{ + { + Name: "item-1", + Count: 1, + Active: true, + Score: 1.1, + UpdatedAt: now, + }, + { + Name: "item-2", + Count: 2, + Active: false, + Score: 2.2, + UpdatedAt: now, + }, + { + Name: "item-3", + Count: 3, + Active: true, + Score: 3.3, + UpdatedAt: now, + }, + } + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + datastore.NameKey("TestKind", "key-2", nil), + datastore.NameKey("TestKind", "key-3", nil), + } + + // MultiPut + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("MultiPut failed: %v", err) + } + + // MultiGet + var retrieved []testEntity + err = client.GetMulti(ctx, keys, &retrieved) + if err != nil { + t.Fatalf("MultiGet failed: %v", err) + } + + if len(retrieved) != 3 { + t.Fatalf("expected 3 entities, got %d", len(retrieved)) + } + + // Verify entities + for i, entity := range retrieved { + if entity.Name != entities[i].Name { + t.Errorf("entity %d: Name mismatch: expected %q, got %q", i, entities[i].Name, entity.Name) + } + if entity.Count != entities[i].Count { + t.Errorf("entity %d: Count mismatch: expected %d, got %d", i, entities[i].Count, entity.Count) + } + if entity.Active != entities[i].Active { + t.Errorf("entity %d: Active mismatch: expected %v, got %v", i, entities[i].Active, entity.Active) + } + if entity.Score != entities[i].Score { + t.Errorf("entity %d: Score mismatch: expected %f, got %f", i, entities[i].Score, entity.Score) + } + } +} + +func TestMultiPutEmptyKeys(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + var entities []testEntity + var keys []*datastore.Key + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error for empty keys, got nil") + } +} + +func TestPutWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + entity := &testEntity{Name: "test"} + _, err := client.Put(ctx, nil, entity) + if err == nil { + t.Error("expected error for nil key, got nil") + } +} + +func TestMultiPutWithNilKey(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + entities := []testEntity{ + {Name: "item-1"}, + {Name: "item-2"}, + } + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + nil, + } + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error for nil key in slice, got nil") + } +} + +func TestMultiPutMismatchedSlices(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + entities := []testEntity{ + {Name: "item-1"}, + {Name: "item-2"}, + } + + keys := []*datastore.Key{ + datastore.NameKey("TestKind", "key-1", nil), + } + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error for mismatched slices, got nil") + } +} + +func TestMultiPutEmptySlices(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Call MultiPut with empty slices - should return error + _, err := client.PutMulti(ctx, []*datastore.Key{}, []testEntity{}) + if err == nil { + t.Error("expected error for MultiPut with empty keys, got nil") + } +} + +func TestPutWithInvalidEntity(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + type InvalidEntity struct { + Map map[string]string // maps not supported + } + + key := datastore.NameKey("TestKind", "invalid", nil) + entity := &InvalidEntity{ + Map: map[string]string{"key": "value"}, + } + + _, err := client.Put(ctx, key, entity) + if err == nil { + t.Error("expected error for unsupported entity type") + } +} + +func TestPutMultiLargeBatch(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Create large batch + const size = 100 + entities := make([]testEntity, size) + keys := make([]*datastore.Key, size) + + for i := range size { + entities[i] = testEntity{ + Name: "large-batch", + Count: int64(i), + } + keys[i] = datastore.NameKey("LargeBatch", fmt.Sprintf("key-%d", i), nil) + } + + _, err := client.PutMulti(ctx, keys, entities) + if err != nil { + t.Fatalf("PutMulti with large batch failed: %v", err) + } + + // Verify a few + var retrieved testEntity + err = client.Get(ctx, keys[0], &retrieved) + if err != nil { + t.Fatalf("Get failed: %v", err) + } + + if retrieved.Count != 0 { + t.Errorf("expected Count 0, got %d", retrieved.Count) + } +} + +func TestPutMultiWithLengthMismatch(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Keys and entities with different lengths + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + entities := []testEntity{ + {Name: "only-one", Count: 1}, + } + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error when keys and entities have different lengths") + } +} + +func TestPutMultiEmptySlice(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Empty slices + _, err := client.PutMulti(ctx, []*datastore.Key{}, []testEntity{}) + if err == nil { + t.Error("expected error for empty slices") + } +} + +func TestPutWithNonPointerEntity(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + key := datastore.NameKey("Test", "key", nil) + entity := testEntity{Name: "test", Count: 1} // not a pointer + + // The mock implementation may accept non-pointers, but test with the real client + // For now, just test that it works (real Datastore would require pointer) + _, err := client.Put(ctx, key, entity) + if err != nil { + t.Logf("Put with non-pointer entity failed (expected with real client): %v", err) + } +} + +func TestPutMultiWithNonSliceSrc(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + } + + // Pass a non-slice as source + notSlice := "not a slice" + _, err := client.PutMulti(ctx, keys, notSlice) + + if err == nil { + t.Error("expected error when src is not a slice") + } +} + +func TestPutWithInvalidEntityStructure(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + // Entity with channel (unsupported type) + type BadEntity struct { + Ch chan int + Name string + } + + key := datastore.NameKey("Test", "bad", nil) + entity := &BadEntity{ + Name: "test", + Ch: make(chan int), + } + + _, err := client.Put(ctx, key, entity) + + if err == nil { + t.Error("expected error for unsupported entity type") + } +} + +func TestPutMultiWithServerError(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) { + w.WriteHeader(http.StatusBadRequest) + if _, err := w.Write([]byte(`{"error":"bad request"}`)); 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") + if err != nil { + t.Fatalf("NewClient failed: %v", err) + } + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + entities := []testEntity{ + {Name: "entity1", Count: 1}, + {Name: "entity2", Count: 2}, + } + + _, err = client.PutMulti(ctx, keys, entities) + + if err == nil { + t.Error("expected error on server failure") + } +} + +func TestPutMultiWithInvalidEntities(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + + type InvalidEntity struct { + Func func() `datastore:"func"` + } + + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + } + + entities := []InvalidEntity{ + {Func: func() {}}, + } + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Log("PutMulti with func field succeeded (mock may not validate types)") + } else { + t.Logf("PutMulti with func field failed as expected: %v", err) + } +} + +func TestPutWithNonStruct(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + key := datastore.NameKey("Test", "key", nil) + entity := "not a struct" + + _, err := client.Put(ctx, key, entity) + if err == nil { + t.Error("expected error when entity is not a struct") + } +} + +func TestPutMultiMismatchedLength(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + datastore.NameKey("Test", "key2", nil), + } + + entities := []testEntity{ + {Name: "test1"}, + // Missing second entity + } + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error with mismatched lengths") + } +} + +func TestPutWithAccessTokenError(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 + } + // Always return error for token + w.WriteHeader(http.StatusInternalServerError) + })) + defer metadataServer.Close() + + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, "http://unused") + + client, 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"} + + _, err = client.Put(ctx, key, entity) + if err == nil { + t.Error("expected error when access token fails") + } +} + +func TestPutMultiRequestMarshalError(t *testing.T) { + // This is hard to trigger directly, but we can test with encoding errors + 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) { + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(map[string]any{ + "mutationResults": []any{}, + }); err != nil { + t.Logf("encode failed: %v", err) + } + })) + 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) + } + + // Test with valid entities to exercise the code path + keys := []*datastore.Key{ + datastore.NameKey("Test", "key1", nil), + } + + entities := []testEntity{ + {Name: "test1", Count: 123}, + } + + _, err = client.PutMulti(ctx, keys, entities) + if err != nil { + t.Logf("PutMulti completed with: %v", err) + } +} + +func TestPutMultiLengthValidation(t *testing.T) { + client, cleanup := datastore.NewMockClient(t) + defer cleanup() + + ctx := context.Background() + keys := []*datastore.Key{datastore.NameKey("Test", "key1", nil)} + entities := []testEntity{{Name: "test1"}, {Name: "test2"}} + + _, err := client.PutMulti(ctx, keys, entities) + if err == nil { + t.Error("expected error with mismatched lengths") + } +} diff --git a/pkg/datastore/operations_test.go b/pkg/datastore/operations_test.go deleted file mode 100644 index 0612287..0000000 --- a/pkg/datastore/operations_test.go +++ /dev/null @@ -1,4001 +0,0 @@ -package datastore_test - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/codeGROOVE-dev/ds9/pkg/datastore" -) - -func TestPutAndGet(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create test entity - now := time.Now().UTC().Truncate(time.Second) - entity := &testEntity{ - Name: "test-item", - Count: 42, - Active: true, - Score: 3.14, - UpdatedAt: now, - Notes: "This is a test note", - } - - // Put entity - key := datastore.NameKey("TestKind", "test-key", nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Get entity - var retrieved testEntity - err = client.Get(ctx, key, &retrieved) - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - // Verify fields - if retrieved.Name != entity.Name { - t.Errorf("Name: expected %q, got %q", entity.Name, retrieved.Name) - } - if retrieved.Count != entity.Count { - t.Errorf("Count: expected %d, got %d", entity.Count, retrieved.Count) - } - if retrieved.Active != entity.Active { - t.Errorf("Active: expected %v, got %v", entity.Active, retrieved.Active) - } - if retrieved.Score != entity.Score { - t.Errorf("Score: expected %f, got %f", entity.Score, retrieved.Score) - } - if !retrieved.UpdatedAt.Equal(entity.UpdatedAt) { - t.Errorf("UpdatedAt: expected %v, got %v", entity.UpdatedAt, retrieved.UpdatedAt) - } - if retrieved.Notes != entity.Notes { - t.Errorf("Notes: expected %q, got %q", entity.Notes, retrieved.Notes) - } -} - -func TestGetNotFound(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - key := datastore.NameKey("TestKind", "nonexistent", nil) - var entity testEntity - err := client.Get(ctx, key, &entity) - - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity, got %v", err) - } -} - -func TestDelete(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put entity - entity := &testEntity{ - Name: "test-item", - Count: 42, - Active: true, - } - - key := datastore.NameKey("TestKind", "test-key", nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Delete entity - err = client.Delete(ctx, key) - if err != nil { - t.Fatalf("Delete failed: %v", err) - } - - // Verify it's gone - var retrieved testEntity - err = client.Get(ctx, key, &retrieved) - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity after delete, got %v", err) - } -} - -func TestAllKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put multiple entities - for i := range 5 { - entity := &testEntity{ - Name: "test-item", - Count: int64(i), - } - key := datastore.NameKey("TestKind", string(rune('a'+i)), nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - } - - // Query for all keys - query := datastore.NewQuery("TestKind").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys failed: %v", err) - } - - if len(keys) != 5 { - t.Errorf("expected 5 keys, got %d", len(keys)) - } -} - -func TestAllKeysWithLimit(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put multiple entities - for i := range 10 { - entity := &testEntity{ - Name: "test-item", - Count: int64(i), - } - key := datastore.NameKey("TestKind", string(rune('a'+i)), nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - } - - // Query with limit - query := datastore.NewQuery("TestKind").KeysOnly().Limit(3) - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys failed: %v", err) - } - - if len(keys) != 3 { - t.Errorf("expected 3 keys, got %d", len(keys)) - } -} - -func TestIDKey(t *testing.T) { - key := datastore.IDKey("TestKind", 12345, nil) - - if key.Kind != "TestKind" { - t.Errorf("expected Kind %q, got %q", "TestKind", key.Kind) - } - - if key.ID != 12345 { - t.Errorf("expected ID %d, got %d", 12345, key.ID) - } - - if key.Name != "" { - t.Errorf("expected empty Name, got %q", key.Name) - } -} - -func TestNameKey(t *testing.T) { - key := datastore.NameKey("TestKind", "test-name", nil) - - if key.Kind != "TestKind" { - t.Errorf("expected Kind %q, got %q", "TestKind", key.Kind) - } - - if key.Name != "test-name" { - t.Errorf("expected Name %q, got %q", "test-name", key.Name) - } - - if key.ID != 0 { - t.Errorf("expected ID 0, got %d", key.ID) - } -} - -func TestMultiPutAndMultiGet(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create test entities - now := time.Now().UTC().Truncate(time.Second) - entities := []testEntity{ - { - Name: "item-1", - Count: 1, - Active: true, - Score: 1.1, - UpdatedAt: now, - }, - { - Name: "item-2", - Count: 2, - Active: false, - Score: 2.2, - UpdatedAt: now, - }, - { - Name: "item-3", - Count: 3, - Active: true, - Score: 3.3, - UpdatedAt: now, - }, - } - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - datastore.NameKey("TestKind", "key-2", nil), - datastore.NameKey("TestKind", "key-3", nil), - } - - // MultiPut - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("MultiPut failed: %v", err) - } - - // MultiGet - var retrieved []testEntity - err = client.GetMulti(ctx, keys, &retrieved) - if err != nil { - t.Fatalf("MultiGet failed: %v", err) - } - - if len(retrieved) != 3 { - t.Fatalf("expected 3 entities, got %d", len(retrieved)) - } - - // Verify entities - for i, entity := range retrieved { - if entity.Name != entities[i].Name { - t.Errorf("entity %d: Name mismatch: expected %q, got %q", i, entities[i].Name, entity.Name) - } - if entity.Count != entities[i].Count { - t.Errorf("entity %d: Count mismatch: expected %d, got %d", i, entities[i].Count, entity.Count) - } - if entity.Active != entities[i].Active { - t.Errorf("entity %d: Active mismatch: expected %v, got %v", i, entities[i].Active, entity.Active) - } - if entity.Score != entities[i].Score { - t.Errorf("entity %d: Score mismatch: expected %f, got %f", i, entities[i].Score, entity.Score) - } - } -} - -func TestMultiGetNotFound(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put only one entity - entity := &testEntity{Name: "exists", Count: 1} - key1 := datastore.NameKey("TestKind", "exists", nil) - _, err := client.Put(ctx, key1, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Try to get multiple, one missing - keys := []*datastore.Key{ - key1, - datastore.NameKey("TestKind", "missing", nil), - } - - var retrieved []testEntity - err = client.GetMulti(ctx, keys, &retrieved) - - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity when some keys missing, got %v", err) - } -} - -func TestMultiDelete(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put multiple entities - entities := []testEntity{ - {Name: "item-1", Count: 1}, - {Name: "item-2", Count: 2}, - {Name: "item-3", Count: 3}, - } - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - datastore.NameKey("TestKind", "key-2", nil), - datastore.NameKey("TestKind", "key-3", nil), - } - - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("MultiPut failed: %v", err) - } - - // MultiDelete - err = client.DeleteMulti(ctx, keys) - if err != nil { - t.Fatalf("MultiDelete failed: %v", err) - } - - // Verify they're gone by trying to get them - var retrieved []testEntity - err = client.GetMulti(ctx, keys, &retrieved) - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity after delete, got %v", err) - } -} - -func TestMultiPutEmptyKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - var entities []testEntity - var keys []*datastore.Key - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error for empty keys, got nil") - } -} - -func TestMultiGetEmptyKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - var keys []*datastore.Key - var retrieved []testEntity - - err := client.GetMulti(ctx, keys, &retrieved) - if err == nil { - t.Error("expected error for empty keys, got nil") - } -} - -func TestMultiDeleteEmptyKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - var keys []*datastore.Key - - err := client.DeleteMulti(ctx, keys) - if err == nil { - t.Error("expected error for empty keys, got nil") - } -} - -func TestIDKeyOperations(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Test with ID key - entity := &testEntity{ - Name: "id-test", - Count: 123, - } - - key := datastore.IDKey("TestKind", 999, nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with ID key failed: %v", err) - } - - // Get with ID key - var retrieved testEntity - err = client.Get(ctx, key, &retrieved) - if err != nil { - t.Fatalf("Get with ID key failed: %v", err) - } - - if retrieved.Name != "id-test" { - t.Errorf("expected Name 'id-test', got %q", retrieved.Name) - } -} - -func TestPutWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - entity := &testEntity{Name: "test"} - _, err := client.Put(ctx, nil, entity) - if err == nil { - t.Error("expected error for nil key, got nil") - } -} - -func TestGetWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - var entity testEntity - err := client.Get(ctx, nil, &entity) - if err == nil { - t.Error("expected error for nil key, got nil") - } -} - -func TestDeleteWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - err := client.Delete(ctx, nil) - if err == nil { - t.Error("expected error for nil key, got nil") - } -} - -func TestMultiGetWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - nil, - datastore.NameKey("TestKind", "key-2", nil), - } - - var entities []testEntity - err := client.GetMulti(ctx, keys, &entities) - if err == nil { - t.Error("expected error for nil key in slice, got nil") - } -} - -func TestMultiPutWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - entities := []testEntity{ - {Name: "item-1"}, - {Name: "item-2"}, - } - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - nil, - } - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error for nil key in slice, got nil") - } -} - -func TestMultiDeleteWithNilKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - nil, - } - - err := client.DeleteMulti(ctx, keys) - if err == nil { - t.Error("expected error for nil key in slice, got nil") - } -} - -func TestMultiPutMismatchedSlices(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - entities := []testEntity{ - {Name: "item-1"}, - {Name: "item-2"}, - } - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - } - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error for mismatched slices, got nil") - } -} - -func TestAllKeysNonKeysOnlyQuery(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create a query without KeysOnly - query := datastore.NewQuery("TestKind") - _, err := client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error for non-KeysOnly query, got nil") - } -} - -func TestMultiGetPartialResults(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put some entities - entities := []testEntity{ - {Name: "item-1", Count: 1}, - {Name: "item-3", Count: 3}, - } - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - datastore.NameKey("TestKind", "key-3", nil), - } - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("MultiPut failed: %v", err) - } - - // Try to get more keys than exist - getAllKeys := []*datastore.Key{ - datastore.NameKey("TestKind", "key-1", nil), - datastore.NameKey("TestKind", "key-2", nil), // doesn't exist - datastore.NameKey("TestKind", "key-3", nil), - } - - var retrieved []testEntity - err = client.GetMulti(ctx, getAllKeys, &retrieved) - if err == nil { - t.Error("expected error when some keys don't exist") - } -} - -func TestKeyComparison(t *testing.T) { - nameKey1 := datastore.NameKey("Kind", "name", nil) - nameKey2 := datastore.NameKey("Kind", "name", nil) - - if nameKey1.Kind != nameKey2.Kind || nameKey1.Name != nameKey2.Name { - t.Error("identical name keys should have same values") - } - - idKey1 := datastore.IDKey("Kind", 123, nil) - idKey2 := datastore.IDKey("Kind", 123, nil) - - if idKey1.Kind != idKey2.Kind || idKey1.ID != idKey2.ID { - t.Error("identical ID keys should have same values") - } -} - -func TestLargeEntityBatch(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create a larger batch - const batchSize = 50 - entities := make([]testEntity, batchSize) - keys := make([]*datastore.Key, batchSize) - - for i := range batchSize { - entities[i] = testEntity{ - Name: "batch-item", - Count: int64(i), - } - keys[i] = datastore.NameKey("BatchKind", string(rune('0'+i/10))+string(rune('0'+i%10)), nil) - } - - // MultiPut - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("MultiPut failed: %v", err) - } - - // MultiGet - var retrieved []testEntity - err = client.GetMulti(ctx, keys, &retrieved) - if err != nil { - t.Fatalf("MultiGet failed: %v", err) - } - - if len(retrieved) != batchSize { - t.Errorf("expected %d entities, got %d", batchSize, len(retrieved)) - } - - // MultiDelete - err = client.DeleteMulti(ctx, keys) - if err != nil { - t.Fatalf("MultiDelete failed: %v", err) - } - - // Verify deletion - var retrieved2 []testEntity - err = client.GetMulti(ctx, keys, &retrieved2) - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity after batch delete, got %v", err) - } -} - -func TestMultiGetEmptySlices(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Call MultiGet with empty slices - should return error - var entities []testEntity - err := client.GetMulti(ctx, []*datastore.Key{}, &entities) - if err == nil { - t.Error("expected error for MultiGet with empty keys, got nil") - } -} - -func TestMultiPutEmptySlices(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Call MultiPut with empty slices - should return error - _, err := client.PutMulti(ctx, []*datastore.Key{}, []testEntity{}) - if err == nil { - t.Error("expected error for MultiPut with empty keys, got nil") - } -} - -func TestMultiDeleteEmptySlice(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Call MultiDelete with empty slice - should return error - err := client.DeleteMulti(ctx, []*datastore.Key{}) - if err == nil { - t.Error("expected error for MultiDelete with empty keys, got nil") - } -} - -func TestDeleteWithDatabaseID(t *testing.T) { - // Setup with databaseID - 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 r.Header.Get("Authorization") != "Bearer test-token" { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []any{}, - }); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "del-db") - if err != nil { - t.Fatalf("NewClientWithDatabase failed: %v", err) - } - - // Delete with databaseID - key := datastore.NameKey("TestKind", "to-delete", nil) - err = client.Delete(ctx, key) - if err != nil { - t.Fatalf("Delete with databaseID failed: %v", err) - } -} - -func TestAllKeysWithDatabaseID(t *testing.T) { - // Setup with databaseID - 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 r.Header.Get("Authorization") != "Bearer test-token" { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{ - "entityResults": []any{}, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "query-db") - if err != nil { - t.Fatalf("NewClientWithDatabase failed: %v", err) - } - - // Query with databaseID - query := datastore.NewQuery("TestKind").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys with databaseID failed: %v", err) - } - - if len(keys) != 0 { - t.Errorf("expected 0 keys, got %d", len(keys)) - } -} - -func TestMultiGetWithDatabaseID(t *testing.T) { - // Setup with databaseID - 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 r.Header.Get("Authorization") != "Bearer test-token" { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - // Return missing entities to trigger datastore.ErrNoSuchEntity - if err := json.NewEncoder(w).Encode(map[string]any{ - "found": []any{}, - "missing": []any{ - map[string]any{"entity": map[string]any{"key": map[string]any{}}}, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multiget-db") - if err != nil { - t.Fatalf("NewClientWithDatabase failed: %v", err) - } - - // MultiGet with databaseID - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key1", nil), - datastore.NameKey("TestKind", "key2", nil), - } - var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) - // Expect error since entities don't exist - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity, got: %v", err) - } -} - -func TestMultiDeleteWithDatabaseID(t *testing.T) { - // Setup with databaseID - 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 r.Header.Get("Authorization") != "Bearer test-token" { - w.WriteHeader(http.StatusUnauthorized) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []any{}, - }); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClientWithDatabase(ctx, "test-project", "multidel-db") - if err != nil { - t.Fatalf("NewClientWithDatabase failed: %v", err) - } - - // MultiDelete with databaseID - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key1", nil), - datastore.NameKey("TestKind", "key2", nil), - } - err = client.DeleteMulti(ctx, keys) - if err != nil { - t.Fatalf("MultiDelete with databaseID failed: %v", err) - } -} - -func TestDeleteAllByKind(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put multiple entities of the same kind - for i := range 5 { - entity := &testEntity{ - Name: "item", - Count: int64(i), - } - key := datastore.NameKey("DeleteKind", string(rune('a'+i)), nil) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - } - - // Delete all entities of this kind - err := client.DeleteAllByKind(ctx, "DeleteKind") - if err != nil { - t.Fatalf("DeleteAllByKind failed: %v", err) - } - - // Verify all deleted - query := datastore.NewQuery("DeleteKind").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys failed: %v", err) - } - - if len(keys) != 0 { - t.Errorf("expected 0 keys after DeleteAllByKind, got %d", len(keys)) - } -} - -func TestDeleteAllByKindEmpty(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Delete from non-existent kind - err := client.DeleteAllByKind(ctx, "NonExistentKind") - if err != nil { - t.Errorf("DeleteAllByKind on empty kind should not error, got: %v", err) - } -} - -func TestHierarchicalKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create parent key - parentKey := datastore.NameKey("Parent", "parent1", nil) - parentEntity := &testEntity{ - Name: "parent", - Count: 1, - } - _, err := client.Put(ctx, parentKey, parentEntity) - if err != nil { - t.Fatalf("Put parent failed: %v", err) - } - - // Create child key with parent - childKey := datastore.NameKey("Child", "child1", parentKey) - childEntity := &testEntity{ - Name: "child", - Count: 2, - } - _, err = client.Put(ctx, childKey, childEntity) - if err != nil { - t.Fatalf("Put child failed: %v", err) - } - - // Get child - var retrieved testEntity - err = client.Get(ctx, childKey, &retrieved) - if err != nil { - t.Fatalf("Get child failed: %v", err) - } - - if retrieved.Name != "child" { - t.Errorf("expected child name 'child', got %q", retrieved.Name) - } -} - -func TestHierarchicalKeysMultiLevel(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create grandparent -> parent -> child hierarchy - grandparentKey := datastore.NameKey("Grandparent", "gp1", nil) - parentKey := datastore.NameKey("Parent", "p1", grandparentKey) - childKey := datastore.NameKey("Child", "c1", parentKey) - - entity := &testEntity{ - Name: "deep-child", - Count: 42, - } - - _, err := client.Put(ctx, childKey, entity) - if err != nil { - t.Fatalf("Put with multi-level hierarchy failed: %v", err) - } - - var retrieved testEntity - err = client.Get(ctx, childKey, &retrieved) - if err != nil { - t.Fatalf("Get with multi-level hierarchy failed: %v", err) - } - - if retrieved.Name != "deep-child" { - t.Errorf("expected name 'deep-child', got %q", retrieved.Name) - } -} - -func TestPutWithInvalidEntity(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - type InvalidEntity struct { - Map map[string]string // maps not supported - } - - key := datastore.NameKey("TestKind", "invalid", nil) - entity := &InvalidEntity{ - Map: map[string]string{"key": "value"}, - } - - _, err := client.Put(ctx, key, entity) - if err == nil { - t.Error("expected error for unsupported entity type") - } -} - -func TestGetMultiWithMismatchedSliceSize(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put one entity - key1 := datastore.NameKey("TestKind", "key1", nil) - entity := &testEntity{Name: "test", Count: 1} - _, err := client.Put(ctx, key1, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Try to get with wrong slice type - keys := []*datastore.Key{key1} - var retrieved []testEntity - - // This should work - err = client.GetMulti(ctx, keys, &retrieved) - if err != nil { - t.Fatalf("GetMulti failed: %v", err) - } - - if len(retrieved) != 1 { - t.Errorf("expected 1 entity, got %d", len(retrieved)) - } -} - -func TestKeyFromJSONEdgeCases(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Test with ID key using integer ID - idKey := datastore.IDKey("TestKind", 12345, nil) - entity := &testEntity{Name: "id-test", Count: 1} - _, err := client.Put(ctx, idKey, entity) - if err != nil { - t.Fatalf("Put with ID key failed: %v", err) - } - - var retrieved testEntity - err = client.Get(ctx, idKey, &retrieved) - if err != nil { - t.Fatalf("Get with ID key failed: %v", err) - } - - if retrieved.Name != "id-test" { - t.Errorf("expected name 'id-test', got %q", retrieved.Name) - } -} - -func TestGetMultiMixedResults(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put some entities - key1 := datastore.NameKey("Mixed", "exists1", nil) - key2 := datastore.NameKey("Mixed", "exists2", nil) - key3 := datastore.NameKey("Mixed", "missing", nil) - - entities := []testEntity{ - {Name: "entity1", Count: 1}, - {Name: "entity2", Count: 2}, - } - - _, err := client.PutMulti(ctx, []*datastore.Key{key1, key2}, entities) - if err != nil { - t.Fatalf("PutMulti failed: %v", err) - } - - // Try to get mix of existing and non-existing - keys := []*datastore.Key{key1, key2, key3} - var retrieved []testEntity - - err = client.GetMulti(ctx, keys, &retrieved) - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity for mixed results, got: %v", err) - } -} - -func TestPutMultiLargeBatch(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create large batch - const size = 100 - entities := make([]testEntity, size) - keys := make([]*datastore.Key, size) - - for i := range size { - entities[i] = testEntity{ - Name: "large-batch", - Count: int64(i), - } - keys[i] = datastore.NameKey("LargeBatch", fmt.Sprintf("key-%d", i), nil) - } - - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("PutMulti with large batch failed: %v", err) - } - - // Verify a few - var retrieved testEntity - err = client.Get(ctx, keys[0], &retrieved) - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - if retrieved.Count != 0 { - t.Errorf("expected Count 0, got %d", retrieved.Count) - } -} - -func TestDeleteMultiWithErrors(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) { - // Return server error - w.WriteHeader(http.StatusInternalServerError) - if _, err := w.Write([]byte(`{"error":"internal error"}`)); err != nil { - t.Logf("write failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - keys := []*datastore.Key{ - datastore.NameKey("TestKind", "key1", nil), - datastore.NameKey("TestKind", "key2", nil), - } - - err = client.DeleteMulti(ctx, keys) - if err == nil { - t.Fatal("expected error on server failure") - } -} - -func TestKeyWithOnlyKind(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Key with neither name nor ID should work (incomplete key) - // This gets an ID assigned by the datastore - key := &datastore.Key{Kind: "TestKind"} - entity := &testEntity{Name: "test", Count: 1} - - returnedKey, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with incomplete key failed: %v", err) - } - - // The returned key should have an ID - if returnedKey == nil { - t.Fatal("expected non-nil returned key") - } - - if returnedKey.Kind != "TestKind" { - t.Errorf("expected Kind 'TestKind', got %q", returnedKey.Kind) - } -} - -func TestGetMultiAllMissing(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - keys := []*datastore.Key{ - datastore.NameKey("Missing", "key1", nil), - datastore.NameKey("Missing", "key2", nil), - datastore.NameKey("Missing", "key3", nil), - } - - var entities []testEntity - err := client.GetMulti(ctx, keys, &entities) - - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity when all keys missing, got: %v", err) - } -} - -func TestGetMultiWithSliceMismatch(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put entity - key := datastore.NameKey("Test", "key1", nil) - entity := &testEntity{Name: "test", Count: 1} - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // GetMulti with destination not being a pointer to slice - var notSlice testEntity - err = client.GetMulti(ctx, []*datastore.Key{key}, notSlice) - if err == nil { - t.Error("expected error when dst is not pointer to slice") - } -} - -func TestPutMultiWithLengthMismatch(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Keys and entities with different lengths - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - entities := []testEntity{ - {Name: "only-one", Count: 1}, - } - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error when keys and entities have different lengths") - } -} - -func TestDeleteWithNonexistentKey(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Delete non-existent key (should not error) - key := datastore.NameKey("Test", "nonexistent", nil) - err := client.Delete(ctx, key) - if err != nil { - t.Errorf("Delete of non-existent key should not error, got: %v", err) - } -} - -func TestAllKeysWithEmptyResult(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Query kind with no entities - query := datastore.NewQuery("EmptyKind").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys on empty kind failed: %v", err) - } - - if len(keys) != 0 { - t.Errorf("expected 0 keys, got %d", len(keys)) - } -} - -func TestAllKeysWithLargeResult(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put many entities - for i := range 50 { - key := datastore.NameKey("LargeResult", fmt.Sprintf("key-%d", i), nil) - entity := &testEntity{Name: "test", Count: int64(i)} - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - } - - // Query all - query := datastore.NewQuery("LargeResult").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys failed: %v", err) - } - - if len(keys) != 50 { - t.Errorf("expected 50 keys, got %d", len(keys)) - } -} - -func TestPutMultiEmptySlice(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Empty slices - _, err := client.PutMulti(ctx, []*datastore.Key{}, []testEntity{}) - if err == nil { - t.Error("expected error for empty slices") - } -} - -func TestGetMultiEmptySlice(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - var entities []testEntity - err := client.GetMulti(ctx, []*datastore.Key{}, &entities) - if err == nil { - t.Error("expected error for empty keys") - } -} - -func TestDeleteMultiEmptySlice(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - err := client.DeleteMulti(ctx, []*datastore.Key{}) - if err == nil { - t.Error("expected error for empty keys") - } -} - -func TestDeepHierarchicalKeys(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create 4-level hierarchy - gp := datastore.NameKey("GP", "gp1", nil) - p := datastore.NameKey("P", "p1", gp) - c := datastore.NameKey("C", "c1", p) - gc := datastore.NameKey("GC", "gc1", c) - - entity := &testEntity{Name: "great-grandchild", Count: 42} - _, err := client.Put(ctx, gc, entity) - if err != nil { - t.Fatalf("Put with 4-level hierarchy failed: %v", err) - } - - var retrieved testEntity - err = client.Get(ctx, gc, &retrieved) - if err != nil { - t.Fatalf("Get with 4-level hierarchy failed: %v", err) - } - - if retrieved.Name != "great-grandchild" { - t.Errorf("expected name 'great-grandchild', got %q", retrieved.Name) - } -} - -func TestGetWithNonPointerDst(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put entity - key := datastore.NameKey("Test", "key", nil) - entity := &testEntity{Name: "test", Count: 1} - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Try to get into non-pointer - var notPointer testEntity - err = client.Get(ctx, key, notPointer) // Should be ¬Pointer - if err == nil { - t.Error("expected error when dst is not a pointer") - } -} - -func TestPutWithNonPointerEntity(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - key := datastore.NameKey("Test", "key", nil) - entity := testEntity{Name: "test", Count: 1} // not a pointer - - // The mock implementation may accept non-pointers, but test with the real client - // For now, just test that it works (real Datastore would require pointer) - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Logf("Put with non-pointer entity failed (expected with real client): %v", err) - } -} - -func TestDeleteAllByKindWithNoEntities(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Delete from kind with no entities - err := client.DeleteAllByKind(ctx, "NonExistentKind") - if err != nil { - t.Errorf("DeleteAllByKind on empty kind should not error, got: %v", err) - } -} - -func TestDeleteAllByKindWithManyEntities(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put many entities - for i := range 25 { - key := datastore.NameKey("ManyDelete", fmt.Sprintf("key-%d", i), nil) - entity := &testEntity{Name: "test", Count: int64(i)} - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - } - - // Delete all - err := client.DeleteAllByKind(ctx, "ManyDelete") - if err != nil { - t.Fatalf("DeleteAllByKind failed: %v", err) - } - - // Verify all deleted - query := datastore.NewQuery("ManyDelete").KeysOnly() - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Fatalf("AllKeys failed: %v", err) - } - - if len(keys) != 0 { - t.Errorf("expected 0 keys after DeleteAllByKind, got %d", len(keys)) - } -} - -func TestIDKeyWithZeroID(t *testing.T) { - // Zero ID is valid - key := datastore.IDKey("Test", 0, nil) - if key.ID != 0 { - t.Errorf("expected ID 0, got %d", key.ID) - } - if key.Name != "" { - t.Errorf("expected empty Name, got %q", key.Name) - } -} - -func TestNameKeyWithEmptyName(t *testing.T) { - // Empty name is technically valid - key := datastore.NameKey("Test", "", nil) - if key.Name != "" { - t.Errorf("expected empty Name, got %q", key.Name) - } - if key.ID != 0 { - t.Errorf("expected ID 0, got %d", key.ID) - } -} - -func TestGetMultiWithNonSliceDst(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - } - - // Pass a non-slice as destination - var notSlice string - err := client.GetMulti(ctx, keys, ¬Slice) - - if err == nil { - t.Error("expected error when dst is not a slice") - } -} - -func TestPutMultiWithNonSliceSrc(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - } - - // Pass a non-slice as source - notSlice := "not a slice" - _, err := client.PutMulti(ctx, keys, notSlice) - - if err == nil { - t.Error("expected error when src is not a slice") - } -} - -func TestAllKeysQueryWithoutKeysOnly(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Create query without KeysOnly - query := datastore.NewQuery("Test") - - _, err := client.AllKeys(ctx, query) - - if err == nil { - t.Error("expected error for query without KeysOnly") - } - - if !strings.Contains(err.Error(), "KeysOnly") { - t.Errorf("expected error to mention KeysOnly, got: %v", err) - } -} - -func TestDeleteAllByKindQueryFailure(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) { - // Fail on query request - if strings.Contains(r.URL.Path, "runQuery") { - w.WriteHeader(http.StatusInternalServerError) - if _, err := w.Write([]byte(`{"error":"query failed"}`)); err != nil { - t.Logf("write failed: %v", err) - } - return - } - w.WriteHeader(http.StatusOK) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - err = client.DeleteAllByKind(ctx, "TestKind") - - if err == nil { - t.Error("expected error when query fails") - } -} - -func TestGetWithInvalidJSONResponse(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) { - // 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) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, 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 = client.Get(ctx, key, &entity) - - if err == nil { - t.Error("expected error for invalid JSON response") - } -} - -func TestPutWithInvalidEntityStructure(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Entity with channel (unsupported type) - type BadEntity struct { - Ch chan int - Name string - } - - key := datastore.NameKey("Test", "bad", nil) - entity := &BadEntity{ - Name: "test", - Ch: make(chan int), - } - - _, err := client.Put(ctx, key, entity) - - if err == nil { - t.Error("expected error for unsupported entity type") - } -} - -func TestGetMultiWithNilInResults(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put one entity - key1 := datastore.NameKey("Test", "exists", nil) - entity := &testEntity{Name: "test", Count: 1} - _, err := client.Put(ctx, key1, entity) - if err != nil { - t.Fatalf("Put failed: %v", err) - } - - // Try to get multiple with one missing - keys := []*datastore.Key{ - key1, - datastore.NameKey("Test", "missing", nil), - datastore.NameKey("Test", "missing2", nil), - } - - var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) - - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity when some keys missing, got: %v", err) - } -} - -func TestDeleteMultiPartialSuccess(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Put some entities - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - entities := []testEntity{ - {Name: "entity1", Count: 1}, - {Name: "entity2", Count: 2}, - } - - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("PutMulti failed: %v", err) - } - - // Delete them (should succeed) - err = client.DeleteMulti(ctx, keys) - if err != nil { - t.Fatalf("DeleteMulti failed: %v", err) - } - - // Verify deletion - var retrieved []testEntity - err = client.GetMulti(ctx, keys, &retrieved) - if !errors.Is(err, datastore.ErrNoSuchEntity) { - t.Errorf("expected datastore.ErrNoSuchEntity after delete, got: %v", err) - } -} - -func TestDeleteWithServerError(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() - - attemptCount := 0 - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - attemptCount++ - // Always return 503 - w.WriteHeader(http.StatusServiceUnavailable) - if _, err := w.Write([]byte(`{"error":"unavailable"}`)); err != nil { - t.Logf("write failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - key := datastore.NameKey("Test", "key", nil) - err = client.Delete(ctx, key) - - if err == nil { - t.Error("expected error on persistent server failure") - } - - // Should have retried - if attemptCount < 2 { - t.Errorf("expected multiple attempts, got %d", attemptCount) - } -} - -func TestPutMultiWithServerError(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) { - w.WriteHeader(http.StatusBadRequest) - if _, err := w.Write([]byte(`{"error":"bad request"}`)); err != nil { - t.Logf("write failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - entities := []testEntity{ - {Name: "entity1", Count: 1}, - {Name: "entity2", Count: 2}, - } - - _, err = client.PutMulti(ctx, keys, entities) - - if err == nil { - t.Error("expected error on server failure") - } -} - -func TestGetMultiWithServerError(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) { - w.WriteHeader(http.StatusUnauthorized) - if _, err := w.Write([]byte(`{"error":"unauthorized"}`)); err != nil { - t.Logf("write failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - client, err := datastore.NewClient(ctx, "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - } - - var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) - - if err == nil { - t.Error("expected error on unauthorized") - } - - if !strings.Contains(err.Error(), "401") { - t.Errorf("expected 401 error, got: %v", err) - } -} - -func TestAllKeysWithInvalidResponse(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) { - // Return invalid JSON - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`{malformed`)); err != nil { - t.Logf("write failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - ctx := context.Background() - 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 TestDeleteWithContextCancellation(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) { - // Slow response - time.Sleep(100 * time.Millisecond) - w.WriteHeader(http.StatusServiceUnavailable) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx, cancel := context.WithCancel(context.Background()) - cancel() // Cancel immediately - - key := datastore.NameKey("Test", "key", nil) - err = client.Delete(ctx, key) - - if err == nil { - t.Error("expected error when context is cancelled") - } -} - -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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - realClient, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - 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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - realClient, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - 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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - realClient, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - 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) - } -} - -func TestDeleteAllRetriesFail(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() - - requestCount := 0 - apiServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - requestCount++ - // Always return 503 to force retries - w.WriteHeader(http.StatusServiceUnavailable) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - - err = client.Delete(ctx, key) - if err == nil { - t.Error("expected error after all retries exhausted") - } - - if !strings.Contains(err.Error(), "attempts") { - t.Errorf("expected error message about attempts, got: %v", err) - } - - if requestCount != 3 { - t.Errorf("expected 3 retry attempts, got %d", requestCount) - } -} - -func TestGetMultiPartialNotFound(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, ":lookup") { - // Return one found, one missing - 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": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "Test", - "name": "key1", - }, - }, - }, - "properties": map[string]any{ - "name": map[string]any{"stringValue": "test1"}, - }, - }, - }, - }, - "missing": []map[string]any{ - { - "key": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "Test", - "name": "key2", - }, - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - var entities []testEntity - err = client.GetMulti(ctx, keys, &entities) - if err == nil { - t.Error("expected error when some entities are missing") - } else { - t.Logf("GetMulti with missing entities failed as expected: %v", err) - } -} - -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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - query := datastore.NewQuery("Test").KeysOnly() - - _, err = client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error with invalid JSON") - } -} - -// Test Transaction commit with invalid response - -func TestPutMultiWithInvalidEntities(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - type InvalidEntity struct { - Func func() `datastore:"func"` - } - - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - } - - entities := []InvalidEntity{ - {Func: func() {}}, - } - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Log("PutMulti with func field succeeded (mock may not validate types)") - } else { - t.Logf("PutMulti with func field failed as expected: %v", err) - } -} - -func TestGetWithNonPointer(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - var entity testEntity // non-pointer - - err := client.Get(ctx, key, entity) // Pass by value - if err == nil { - t.Error("expected error when dst is not a pointer") - } -} - -func TestPutWithNonStruct(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - entity := "not a struct" - - _, err := client.Put(ctx, key, entity) - if err == nil { - t.Error("expected error when entity is not a struct") - } -} - -func TestAllKeysNotKeysOnlyError(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - query := datastore.NewQuery("Test") // Not KeysOnly - - _, err := client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error when query is not KeysOnly") - } -} - -func TestGetMultiMismatchedLength(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - var entities []testEntity // Empty slice - - err := client.GetMulti(ctx, keys, &entities) - // This should work - GetMulti should populate the slice - if err != nil { - t.Logf("GetMulti with empty slice: %v", err) - } -} - -func TestPutMultiMismatchedLength(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - entities := []testEntity{ - {Name: "test1"}, - // Missing second entity - } - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error with mismatched lengths") - } -} - -func TestDeleteMultiWithEmptyKeysSlice(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - var keys []*datastore.Key // Empty - - err := client.DeleteMulti(ctx, keys) - // Mock may behave differently - log the result - if err != nil { - t.Logf("DeleteMulti with empty keys: %v", err) - } -} - -func TestGetWithJSONUnmarshalError(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, ":lookup") { - // Return invalid JSON - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte(`{"found": [{"entity": "not-an-object"}]}`)); err != nil { - t.Logf("write failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - var entity testEntity - - err = client.Get(ctx, key, &entity) - if err == nil { - t.Error("expected error with invalid entity format") - } -} - -func TestPutWithAccessTokenError(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 - } - // Always return error for token - w.WriteHeader(http.StatusInternalServerError) - })) - defer metadataServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, "http://unused") - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - entity := &testEntity{Name: "test"} - - _, err = client.Put(ctx, key, entity) - if err == nil { - t.Error("expected error when access token fails") - } -} - -func TestDeleteWithJSONMarshalError(t *testing.T) { - // This is hard to trigger since we control the JSON structure - // But we can test with a context that gets cancelled - 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) { - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{}); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - key := datastore.NameKey("Test", "key", nil) - - err = client.Delete(ctx, key) - if err != nil { - t.Logf("Delete completed with: %v", err) - } -} - -func TestAllKeysWithBatching(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 multiple key results - results := make([]map[string]any, 50) - for i := range 50 { - results[i] = map[string]any{ - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "Test", - "name": fmt.Sprintf("key%d", i), - }, - }, - }, - }, - } - } - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{ - "entityResults": results, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - query := datastore.NewQuery("Test").KeysOnly() - - keys, err := client.AllKeys(ctx, query) - if err != nil { - t.Logf("AllKeys with many results: %v", err) - } else if len(keys) != 50 { - t.Logf("Expected 50 keys, got %d", len(keys)) - } -} - -func TestAllKeysKeyFromJSONError(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 result with invalid key format - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{ - "entityResults": []map[string]any{ - { - "entity": map[string]any{ - "key": "not-a-map", // Invalid key format - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - query := datastore.NewQuery("Test").KeysOnly() - - _, err = client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error with invalid key format") - } -} - -func TestPutMultiRequestMarshalError(t *testing.T) { - // This is hard to trigger directly, but we can test with encoding errors - 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) { - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []any{}, - }); err != nil { - t.Logf("encode failed: %v", err) - } - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - - // Test with valid entities to exercise the code path - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - } - - entities := []testEntity{ - {Name: "test1", Count: 123}, - } - - _, err = client.PutMulti(ctx, keys, entities) - if err != nil { - t.Logf("PutMulti completed with: %v", err) - } -} - -func TestDeleteAllByKindEmptyBatch(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 empty batch - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{}, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - err = client.DeleteAllByKind(ctx, "EmptyKind") - if err != nil { - t.Logf("DeleteAllByKind with empty batch: %v", err) - } -} - -func TestAllKeysEmptyPathInKey(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") { - w.Header().Set("Content-Type", "application/json") - // Return key with empty path array - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{ - "entityResults": []map[string]any{ - { - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{}, // Empty path - }, - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - query := datastore.NewQuery("TestKind").KeysOnly() - _, err = client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error with empty path in key") - } -} - -func TestAllKeysInvalidPathElement(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") { - w.Header().Set("Content-Type", "application/json") - // Return key with invalid path element (string instead of map) - if err := json.NewEncoder(w).Encode(map[string]any{ - "batch": map[string]any{ - "entityResults": []map[string]any{ - { - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{"invalid-element"}, // String instead of map - }, - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - query := datastore.NewQuery("TestKind").KeysOnly() - _, err = client.AllKeys(ctx, query) - if err == nil { - t.Error("expected error with invalid path element") - } -} - -func TestGetWithStringIDKey(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, ":lookup") { - w.Header().Set("Content-Type", "application/json") - // Return entity with ID as string - if err := json.NewEncoder(w).Encode(map[string]any{ - "found": []map[string]any{ - { - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "TestKind", - "id": "12345", // ID as string - }, - }, - }, - "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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - type TestEntity struct { - Name string `datastore:"name"` - } - - ctx := context.Background() - key := datastore.IDKey("TestKind", 12345, nil) - var entity TestEntity - err = client.Get(ctx, key, &entity) - if err != nil { - t.Fatalf("Get with string ID key failed: %v", err) - } - - if entity.Name != "test" { - t.Errorf("expected name 'test', got %q", entity.Name) - } -} - -func TestGetWithFloat64IDKey(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, ":lookup") { - w.Header().Set("Content-Type", "application/json") - // Return entity with ID as float64 - if err := json.NewEncoder(w).Encode(map[string]any{ - "found": []map[string]any{ - { - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "TestKind", - "id": float64(67890), // ID as float64 - }, - }, - }, - "properties": map[string]any{ - "value": map[string]any{"integerValue": "42"}, - }, - }, - }, - }, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - type TestEntity struct { - Value int64 `datastore:"value"` - } - - ctx := context.Background() - key := datastore.IDKey("TestKind", 67890, nil) - var entity TestEntity - err = client.Get(ctx, key, &entity) - if err != nil { - t.Fatalf("Get with float64 ID key failed: %v", err) - } - - if entity.Value != 42 { - t.Errorf("expected value 42, got %d", entity.Value) - } -} - -func TestGetWithInvalidStringIDFormat(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, ":lookup") { - w.Header().Set("Content-Type", "application/json") - // Return entity with invalid ID string format - if err := json.NewEncoder(w).Encode(map[string]any{ - "found": []map[string]any{ - { - "entity": map[string]any{ - "key": map[string]any{ - "path": []any{ - map[string]any{ - "kind": "TestKind", - "id": "not-a-number", // Invalid ID format - }, - }, - }, - "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() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - type TestEntity struct { - Name string `datastore:"name"` - } - - ctx := context.Background() - key := datastore.IDKey("TestKind", 12345, nil) - var entity TestEntity - err = client.Get(ctx, key, &entity) - // May or may not error depending on parsing behavior - if err != nil { - t.Logf("Get with invalid string ID format failed: %v", err) - } else { - t.Logf("Get with invalid string ID format succeeded unexpectedly") - } -} - -func TestGetJSONUnmarshalError(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, ":lookup") { - // Return malformed JSON - w.Header().Set("Content-Type", "application/json") - if _, err := w.Write([]byte("not valid json")); err != nil { - t.Logf("write failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - key := datastore.NameKey("Test", "test-key", nil) - var entity testEntity - - err = client.Get(ctx, key, &entity) - if err == nil { - t.Error("expected error with malformed JSON") - } -} - -func TestPutMultiLengthValidation(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - keys := []*datastore.Key{datastore.NameKey("Test", "key1", nil)} - entities := []testEntity{{Name: "test1"}, {Name: "test2"}} - - _, err := client.PutMulti(ctx, keys, entities) - if err == nil { - t.Error("expected error with mismatched lengths") - } -} - -func TestDeleteMultiMixedResults(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, ":commit") { - w.Header().Set("Content-Type", "application/json") - // Return empty mutation results - if err := json.NewEncoder(w).Encode(map[string]any{ - "mutationResults": []any{}, - }); err != nil { - t.Logf("encode failed: %v", err) - } - return - } - w.WriteHeader(http.StatusNotFound) - })) - defer apiServer.Close() - - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() - - client, err := datastore.NewClient(context.Background(), "test-project") - if err != nil { - t.Fatalf("NewClient failed: %v", err) - } - - ctx := context.Background() - keys := []*datastore.Key{ - datastore.NameKey("Test", "key1", nil), - datastore.NameKey("Test", "key2", nil), - } - - err = client.DeleteMulti(ctx, keys) - // May or may not error depending on implementation - if err != nil { - t.Logf("DeleteMulti with mismatched results: %v", err) - } -} - -func TestBackwardsCompatibility(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - // Test 1: Close() method exists and can be called (even though it's a no-op) - t.Run("Close", func(t *testing.T) { - err := client.Close() - if err != nil { - t.Errorf("Close() returned error: %v", err) - } - }) - - // Test 2: RunInTransaction returns (*Commit, error) - t.Run("RunInTransactionSignature", func(t *testing.T) { - key := datastore.NameKey("TestKind", "test-tx-compat", nil) - - commit, err := client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { - entity := &testEntity{ - Name: "transaction test", - Count: 100, - Active: true, - Score: 99.9, - UpdatedAt: time.Now().UTC().Truncate(time.Microsecond), - } - _, err := tx.Put(key, entity) - return err - }) - if err != nil { - t.Fatalf("RunInTransaction failed: %v", err) - } - - if commit == nil { - t.Error("Expected non-nil Commit, got nil") - } - }) - - // Test 3: GetAll() method retrieves entities and returns keys - t.Run("GetAll", func(t *testing.T) { - // Setup: Create some test entities - entities := []testEntity{ - {Name: "entity1", Count: 1, Active: true, Score: 1.1, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, - {Name: "entity2", Count: 2, Active: false, Score: 2.2, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, - {Name: "entity3", Count: 3, Active: true, Score: 3.3, UpdatedAt: time.Now().UTC().Truncate(time.Microsecond)}, - } - - keys := []*datastore.Key{ - datastore.NameKey("GetAllTest", "key1", nil), - datastore.NameKey("GetAllTest", "key2", nil), - datastore.NameKey("GetAllTest", "key3", nil), - } - - _, err := client.PutMulti(ctx, keys, entities) - if err != nil { - t.Fatalf("PutMulti failed: %v", err) - } - - // Test GetAll - query := datastore.NewQuery("GetAllTest") - var results []testEntity - returnedKeys, err := client.GetAll(ctx, query, &results) - if err != nil { - t.Fatalf("GetAll failed: %v", err) - } - - if len(results) != 3 { - t.Errorf("Expected 3 entities, got %d", len(results)) - } - - if len(returnedKeys) != 3 { - t.Errorf("Expected 3 keys, got %d", len(returnedKeys)) - } - - // Verify entities were properly decoded - foundNames := make(map[string]bool) - for _, entity := range results { - foundNames[entity.Name] = true - } - - for _, expectedName := range []string{"entity1", "entity2", "entity3"} { - if !foundNames[expectedName] { - t.Errorf("Expected to find entity %s, but didn't", expectedName) - } - } - - // Verify keys match entities - for i, key := range returnedKeys { - if key.Kind != "GetAllTest" { - t.Errorf("Key %d has wrong kind: %s", i, key.Kind) - } - } - }) - - // Test 4: GetAll with limit - t.Run("GetAllWithLimit", func(t *testing.T) { - query := datastore.NewQuery("GetAllTest").Limit(2) - var results []testEntity - returnedKeys, err := client.GetAll(ctx, query, &results) - if err != nil { - t.Fatalf("GetAll with limit failed: %v", err) - } - - if len(results) != 2 { - t.Errorf("Expected 2 entities with limit, got %d", len(results)) - } - - if len(returnedKeys) != 2 { - t.Errorf("Expected 2 keys with limit, got %d", len(returnedKeys)) - } - }) -} - -func TestArraySliceSupport(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - t.Run("StringSlice", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "strings", nil) - entity := &arrayEntity{ - Strings: []string{"hello", "world", "test"}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with string slice failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if len(result.Strings) != 3 { - t.Errorf("Expected 3 strings, got %d", len(result.Strings)) - } - if result.Strings[0] != "hello" || result.Strings[1] != "world" || result.Strings[2] != "test" { - t.Errorf("String slice values incorrect: %v", result.Strings) - } - }) - - t.Run("Int64Slice", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "ints", nil) - entity := &arrayEntity{ - Ints: []int64{1, 2, 3, 42, 100}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with int64 slice failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if len(result.Ints) != 5 { - t.Errorf("Expected 5 ints, got %d", len(result.Ints)) - } - if result.Ints[3] != 42 { - t.Errorf("Expected Ints[3] = 42, got %d", result.Ints[3]) - } - }) - - t.Run("Float64Slice", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "floats", nil) - entity := &arrayEntity{ - Floats: []float64{1.1, 2.2, 3.3}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with float64 slice failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if len(result.Floats) != 3 { - t.Errorf("Expected 3 floats, got %d", len(result.Floats)) - } - if result.Floats[0] != 1.1 { - t.Errorf("Expected Floats[0] = 1.1, got %f", result.Floats[0]) - } - }) - - t.Run("BoolSlice", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "bools", nil) - entity := &arrayEntity{ - Bools: []bool{true, false, true}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with bool slice failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if len(result.Bools) != 3 { - t.Errorf("Expected 3 bools, got %d", len(result.Bools)) - } - if result.Bools[0] != true || result.Bools[1] != false { - t.Errorf("Bool slice values incorrect: %v", result.Bools) - } - }) - - t.Run("EmptySlices", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "empty", nil) - entity := &arrayEntity{ - Strings: []string{}, - Ints: []int64{}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with empty slices failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if result.Strings == nil || len(result.Strings) != 0 { - t.Errorf("Expected empty string slice, got %v", result.Strings) - } - if result.Ints == nil || len(result.Ints) != 0 { - t.Errorf("Expected empty int slice, got %v", result.Ints) - } - }) - - t.Run("MixedArrays", func(t *testing.T) { - key := datastore.NameKey("ArrayTest", "mixed", nil) - entity := &arrayEntity{ - Strings: []string{"a", "b"}, - Ints: []int64{10, 20, 30}, - Floats: []float64{1.5}, - Bools: []bool{true, false, true, false}, - } - - _, err := client.Put(ctx, key, entity) - if err != nil { - t.Fatalf("Put with mixed arrays failed: %v", err) - } - - var result arrayEntity - if err := client.Get(ctx, key, &result); err != nil { - t.Fatalf("Get failed: %v", err) - } - - if len(result.Strings) != 2 || len(result.Ints) != 3 || len(result.Floats) != 1 || len(result.Bools) != 4 { - t.Errorf("Mixed array lengths incorrect: strings=%d, ints=%d, floats=%d, bools=%d", - len(result.Strings), len(result.Ints), len(result.Floats), len(result.Bools)) - } - }) -} - -func TestAllocateIDs(t *testing.T) { - client, cleanup := datastore.NewMockClient(t) - defer cleanup() - - ctx := context.Background() - - t.Run("AllocateIncompleteKeys", func(t *testing.T) { - keys := []*datastore.Key{ - datastore.IncompleteKey("Task", nil), - datastore.IncompleteKey("Task", nil), - datastore.IncompleteKey("Task", nil), - } - - allocated, err := client.AllocateIDs(ctx, keys) - if err != nil { - t.Fatalf("AllocateIDs failed: %v", err) - } - - if len(allocated) != 3 { - t.Errorf("Expected 3 allocated keys, got %d", len(allocated)) - } - - for i, key := range allocated { - if key.Incomplete() { - t.Errorf("Key %d is still incomplete", i) - } - if key.ID == 0 { - t.Errorf("Key %d has zero ID", i) - } - } - }) - - t.Run("AllocateMixedKeys", func(t *testing.T) { - keys := []*datastore.Key{ - datastore.NameKey("Task", "complete", nil), - datastore.IncompleteKey("Task", nil), - datastore.IDKey("Task", 123, nil), - datastore.IncompleteKey("Task", nil), - } - - allocated, err := client.AllocateIDs(ctx, keys) - if err != nil { - t.Fatalf("AllocateIDs with mixed keys failed: %v", err) - } - - if len(allocated) != 4 { - t.Errorf("Expected 4 keys, got %d", len(allocated)) - } - - // First key should still be the named key - if allocated[0].Name != "complete" { - t.Errorf("First key should be unchanged") - } - - // Second key should now have an ID - if allocated[1].Incomplete() { - t.Errorf("Second key should be allocated") - } - - // Third key should be unchanged - if allocated[2].ID != 123 { - t.Errorf("Third key should be unchanged") - } - - // Fourth key should now have an ID - if allocated[3].Incomplete() { - t.Errorf("Fourth key should be allocated") - } - }) - - t.Run("AllocateEmptySlice", func(t *testing.T) { - keys := []*datastore.Key{} - - allocated, err := client.AllocateIDs(ctx, keys) - if err != nil { - t.Fatalf("AllocateIDs with empty slice failed: %v", err) - } - - if len(allocated) != 0 { - t.Errorf("Expected empty slice, got %d keys", len(allocated)) - } - }) - - t.Run("AllocateAllCompleteKeys", func(t *testing.T) { - keys := []*datastore.Key{ - datastore.NameKey("Task", "key1", nil), - datastore.IDKey("Task", 100, nil), - } - - allocated, err := client.AllocateIDs(ctx, keys) - if err != nil { - t.Fatalf("AllocateIDs with complete keys failed: %v", err) - } - - if len(allocated) != 2 { - t.Errorf("Expected 2 keys, got %d", len(allocated)) - } - - // Keys should be unchanged - if allocated[0].Name != "key1" || allocated[1].ID != 100 { - t.Errorf("Complete keys should be unchanged") - } - }) -} diff --git a/pkg/datastore/query.go b/pkg/datastore/query.go index b45cebf..7cc648b 100644 --- a/pkg/datastore/query.go +++ b/pkg/datastore/query.go @@ -14,16 +14,18 @@ import ( ) // Query represents a Datastore query. +// +//nolint:govet // Field order prioritizes logical grouping over memory optimization type Query struct { - ancestor *Key - kind string filters []queryFilter orders []queryOrder projection []string distinctOn []string - namespace string startCursor Cursor endCursor Cursor + kind string + namespace string + ancestor *Key limit int offset int keysOnly bool @@ -319,6 +321,7 @@ func buildQueryMap(query *Query) map[string]any { // AllKeys returns all keys matching the query. // This is a convenience method for KeysOnly queries. func (c *Client) AllKeys(ctx context.Context, q *Query) ([]*Key, error) { + ctx = c.withClientConfig(ctx) if !q.keysOnly { c.logger.WarnContext(ctx, "AllKeys called on non-KeysOnly query") return nil, errors.New("AllKeys requires KeysOnly query") @@ -385,6 +388,7 @@ func (c *Client) AllKeys(ctx context.Context, q *Query) ([]*Key, error) { // Returns the keys of the retrieved entities and any error. // This matches the API of cloud.google.com/go/datastore. func (c *Client) GetAll(ctx context.Context, query *Query, dst any) ([]*Key, error) { + ctx = c.withClientConfig(ctx) c.logger.DebugContext(ctx, "querying for entities", "kind", query.kind, "limit", query.limit) token, err := auth.AccessToken(ctx) @@ -467,6 +471,7 @@ func (c *Client) GetAll(ctx context.Context, query *Query, dst any) ([]*Key, err // Deprecated: Use aggregation queries with RunAggregationQuery instead. // API compatible with cloud.google.com/go/datastore. func (c *Client) Count(ctx context.Context, q *Query) (int, error) { + ctx = c.withClientConfig(ctx) c.logger.DebugContext(ctx, "counting entities", "kind", q.kind) token, err := auth.AccessToken(ctx) @@ -548,6 +553,7 @@ func (c *Client) Count(ctx context.Context, q *Query) (int, error) { // Run executes the query and returns an iterator for the results. // API compatible with cloud.google.com/go/datastore. func (c *Client) Run(ctx context.Context, q *Query) *Iterator { + ctx = c.withClientConfig(ctx) return &Iterator{ ctx: ctx, client: c, diff --git a/pkg/datastore/test_helper.go b/pkg/datastore/test_helper.go new file mode 100644 index 0000000..066d406 --- /dev/null +++ b/pkg/datastore/test_helper.go @@ -0,0 +1,25 @@ +package datastore + +import ( + "context" + + "github.com/codeGROOVE-dev/ds9/auth" +) + +// TestConfig creates a context with test configuration for 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{ + MetadataURL: metadataURL, + SkipADC: true, + }, + }) +} diff --git a/pkg/datastore/transaction.go b/pkg/datastore/transaction.go index d8c56ee..ad6a679 100644 --- a/pkg/datastore/transaction.go +++ b/pkg/datastore/transaction.go @@ -70,6 +70,7 @@ func WithReadTime(t time.Time) TransactionOption { // The caller must call Commit or Rollback when done. // API compatible with cloud.google.com/go/datastore. func (c *Client) NewTransaction(ctx context.Context, opts ...TransactionOption) (*Transaction, error) { + ctx = c.withClientConfig(ctx) settings := transactionSettings{ maxAttempts: 3, // default (not used for NewTransaction, but kept for consistency) } @@ -162,6 +163,7 @@ func (c *Client) NewTransaction(ctx context.Context, opts ...TransactionOption) // The function should use the transaction's Get and Put methods. // API compatible with cloud.google.com/go/datastore. func (c *Client) RunInTransaction(ctx context.Context, f func(*Transaction) error, opts ...TransactionOption) (*Commit, error) { + ctx = c.withClientConfig(ctx) settings := transactionSettings{ maxAttempts: 3, // default } diff --git a/pkg/datastore/transaction_coverage_test.go b/pkg/datastore/transaction_coverage_test.go index 54209c6..ecc2a53 100644 --- a/pkg/datastore/transaction_coverage_test.go +++ b/pkg/datastore/transaction_coverage_test.go @@ -9,6 +9,8 @@ import ( ) // Test manual transaction API (NewTransaction, Get, Put, Delete, Commit, Rollback) +// +//nolint:gocognit,maintidx // Comprehensive test coverage requires many test cases func TestManualTransaction(t *testing.T) { client, cleanup := datastore.NewMockClient(t) defer cleanup() diff --git a/pkg/datastore/transaction_test.go b/pkg/datastore/transaction_test.go index c4a06fa..892ac7c 100644 --- a/pkg/datastore/transaction_test.go +++ b/pkg/datastore/transaction_test.go @@ -257,10 +257,8 @@ func TestTransactionWithDatabaseID(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClientWithDatabase(ctx, "test-project", "tx-db") if err != nil { t.Fatalf("NewClientWithDatabase failed: %v", err) @@ -357,10 +355,8 @@ func TestTransactionBeginFailure(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -456,10 +452,8 @@ func TestTransactionCommitAbortedRetry(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -546,10 +540,8 @@ func TestTransactionMaxRetriesExceeded(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -725,10 +717,8 @@ func TestTransactionGetWithInvalidResponse(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -811,10 +801,8 @@ func TestTransactionWithNonRetriableError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -877,10 +865,8 @@ func TestTransactionWithInvalidTxResponse(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -979,10 +965,8 @@ func TestTransactionGetWithDecodeError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - ctx := context.Background() client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) @@ -1059,15 +1043,12 @@ func TestTransactionGetMissingEntity(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "nonexistent", nil) _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { @@ -1147,15 +1128,12 @@ func TestTransactionGetDecodeError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { @@ -1222,15 +1200,12 @@ func TestTransactionCommitInvalidResponse(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) entity := &testEntity{Name: "test"} @@ -1292,15 +1267,12 @@ func TestTransactionCommitUnmarshalError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "key", nil) entity := &testEntity{Name: "test"} @@ -1375,15 +1347,12 @@ func TestTransactionGetNotFound(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "nonexistent", nil) _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { @@ -1435,15 +1404,12 @@ func TestTransactionGetAccessTokenError(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "test-key", nil) _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { @@ -1518,15 +1484,12 @@ func TestTransactionGetNonOKStatus(t *testing.T) { })) defer apiServer.Close() - restore := datastore.SetTestURLs(metadataServer.URL, apiServer.URL) - defer restore() + ctx := datastore.TestConfig(context.Background(), metadataServer.URL, apiServer.URL) - client, err := datastore.NewClient(context.Background(), "test-project") + client, err := datastore.NewClient(ctx, "test-project") if err != nil { t.Fatalf("NewClient failed: %v", err) } - - ctx := context.Background() key := datastore.NameKey("Test", "test-key", nil) _, err = client.RunInTransaction(ctx, func(tx *datastore.Transaction) error { diff --git a/pkg/mock/mock.go b/pkg/mock/mock.go index 4f9ceb6..8602af2 100644 --- a/pkg/mock/mock.go +++ b/pkg/mock/mock.go @@ -50,6 +50,8 @@ func NewStore() *Store { // NewMockServers creates mock metadata and API servers for testing. // Returns the metadata URL, API URL, and a cleanup function. // This function doesn't import datastore to avoid import cycles. +// +// For convenience, use datastore.NewMockClient() instead which handles all setup. func NewMockServers(t *testing.T) (metadataURL, apiURL string, cleanup func()) { t.Helper() @@ -800,7 +802,7 @@ func matchesFilter(entity map[string]any, filterMap map[string]any) bool { // handleRunAggregationQuery handles :runAggregationQuery requests. func (s *Store) handleRunAggregationQuery(w http.ResponseWriter, r *http.Request) { - var req struct { + var req struct { //nolint:govet // Local anonymous struct for JSON unmarshaling DatabaseID string `json:"databaseId"` AggregationQuery map[string]any `json:"aggregationQuery"` }