github.com/gravitational/teleport/api@v0.0.0-20240507183017-3110591cbafc/types/database_test.go (about)

     1  /*
     2  Copyright 2021 Gravitational, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package types
    18  
    19  import (
    20  	"encoding/json"
    21  	"strings"
    22  	"testing"
    23  
    24  	"github.com/google/go-cmp/cmp"
    25  	"github.com/google/go-cmp/cmp/cmpopts"
    26  	"github.com/gravitational/trace"
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  // TestDatabaseRDSEndpoint verifies AWS info is correctly populated
    31  // based on the RDS endpoint.
    32  func TestDatabaseRDSEndpoint(t *testing.T) {
    33  	isBadParamErrFn := func(tt require.TestingT, err error, i ...any) {
    34  		require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err)
    35  	}
    36  
    37  	for _, tt := range []struct {
    38  		name                 string
    39  		labels               map[string]string
    40  		spec                 DatabaseSpecV3
    41  		errorCheck           require.ErrorAssertionFunc
    42  		expectedAWS          AWS
    43  		expectedEndpointType string
    44  	}{
    45  		{
    46  			name: "aurora instance",
    47  			spec: DatabaseSpecV3{
    48  				Protocol: "postgres",
    49  				URI:      "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
    50  			},
    51  			errorCheck: require.NoError,
    52  			expectedAWS: AWS{
    53  				Region: "us-west-1",
    54  				RDS: RDS{
    55  					InstanceID: "aurora-instance-1",
    56  				},
    57  			},
    58  			expectedEndpointType: "instance",
    59  		},
    60  		{
    61  			name: "invalid account id",
    62  			spec: DatabaseSpecV3{
    63  				Protocol: "postgres",
    64  				URI:      "marcotest-db001.abcdefghijklmnop.us-east-1.rds.amazonaws.com:5432",
    65  				AWS: AWS{
    66  					AccountID: "invalid",
    67  				},
    68  			},
    69  			errorCheck: isBadParamErrFn,
    70  		},
    71  		{
    72  			name: "valid account id",
    73  			spec: DatabaseSpecV3{
    74  				Protocol: "postgres",
    75  				URI:      "marcotest-db001.cluster-ro-abcdefghijklmnop.us-east-1.rds.amazonaws.com:5432",
    76  				AWS: AWS{
    77  					AccountID: "123456789012",
    78  				},
    79  			},
    80  			errorCheck: require.NoError,
    81  			expectedAWS: AWS{
    82  				Region: "us-east-1",
    83  				RDS: RDS{
    84  					ClusterID: "marcotest-db001",
    85  				},
    86  				AccountID: "123456789012",
    87  			},
    88  			expectedEndpointType: "reader",
    89  		},
    90  		{
    91  			name: "discovered instance",
    92  			labels: map[string]string{
    93  				"account-id":                        "123456789012",
    94  				"endpoint-type":                     "primary",
    95  				"engine":                            "aurora-postgresql",
    96  				"engine-version":                    "15.2",
    97  				"region":                            "us-west-1",
    98  				"teleport.dev/cloud":                "AWS",
    99  				"teleport.dev/origin":               "cloud",
   100  				"teleport.internal/discovered-name": "rds",
   101  			},
   102  			spec: DatabaseSpecV3{
   103  				Protocol: "postgres",
   104  				URI:      "discovered.rds.com:5432",
   105  				AWS: AWS{
   106  					Region: "us-west-1",
   107  					RDS: RDS{
   108  						InstanceID: "aurora-instance-1",
   109  						IAMAuth:    true,
   110  					},
   111  				},
   112  			},
   113  			errorCheck: require.NoError,
   114  			expectedAWS: AWS{
   115  				Region: "us-west-1",
   116  				RDS: RDS{
   117  					InstanceID: "aurora-instance-1",
   118  					IAMAuth:    true,
   119  				},
   120  			},
   121  			expectedEndpointType: "primary",
   122  		},
   123  	} {
   124  		tt := tt
   125  		t.Run(tt.name, func(t *testing.T) {
   126  			database, err := NewDatabaseV3(
   127  				Metadata{
   128  					Labels: tt.labels,
   129  					Name:   "rds",
   130  				},
   131  				tt.spec,
   132  			)
   133  			tt.errorCheck(t, err)
   134  			if err != nil {
   135  				return
   136  			}
   137  
   138  			require.Equal(t, tt.expectedAWS, database.GetAWS())
   139  			require.Equal(t, tt.expectedEndpointType, database.GetEndpointType())
   140  		})
   141  	}
   142  }
   143  
   144  // TestDatabaseRDSProxyEndpoint verifies AWS info is correctly populated based
   145  // on the RDS Proxy endpoint.
   146  func TestDatabaseRDSProxyEndpoint(t *testing.T) {
   147  	database, err := NewDatabaseV3(Metadata{
   148  		Name: "rdsproxy",
   149  	}, DatabaseSpecV3{
   150  		Protocol: "postgres",
   151  		URI:      "my-proxy.proxy-abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432",
   152  	})
   153  	require.NoError(t, err)
   154  	require.Equal(t, AWS{
   155  		Region: "us-west-1",
   156  		RDSProxy: RDSProxy{
   157  			Name: "my-proxy",
   158  		},
   159  	}, database.GetAWS())
   160  }
   161  
   162  // TestDatabaseRedshiftEndpoint verifies AWS info is correctly populated
   163  // based on the Redshift endpoint.
   164  func TestDatabaseRedshiftEndpoint(t *testing.T) {
   165  	database, err := NewDatabaseV3(Metadata{
   166  		Name: "redshift",
   167  	}, DatabaseSpecV3{
   168  		Protocol: "postgres",
   169  		URI:      "redshift-cluster-1.abcdefghijklmnop.us-east-1.redshift.amazonaws.com:5438",
   170  	})
   171  	require.NoError(t, err)
   172  	require.Equal(t, AWS{
   173  		Region: "us-east-1",
   174  		Redshift: Redshift{
   175  			ClusterID: "redshift-cluster-1",
   176  		},
   177  	}, database.GetAWS())
   178  }
   179  
   180  // TestDatabaseStatus verifies database resource status field usage.
   181  func TestDatabaseStatus(t *testing.T) {
   182  	database, err := NewDatabaseV3(Metadata{
   183  		Name: "test",
   184  	}, DatabaseSpecV3{
   185  		Protocol: "postgres",
   186  		URI:      "localhost:5432",
   187  	})
   188  	require.NoError(t, err)
   189  
   190  	caCert := "test"
   191  	database.SetStatusCA(caCert)
   192  	require.Equal(t, caCert, database.GetCA())
   193  
   194  	awsMeta := AWS{AccountID: "account-id"}
   195  	database.SetStatusAWS(awsMeta)
   196  	require.Equal(t, awsMeta, database.GetAWS())
   197  }
   198  
   199  func TestDatabaseElastiCacheEndpoint(t *testing.T) {
   200  	t.Run("valid URI", func(t *testing.T) {
   201  		database, err := NewDatabaseV3(Metadata{
   202  			Name: "elasticache",
   203  		}, DatabaseSpecV3{
   204  			Protocol: "redis",
   205  			URI:      "clustercfg.my-redis-cluster.xxxxxx.cac1.cache.amazonaws.com:6379",
   206  		})
   207  
   208  		require.NoError(t, err)
   209  		require.Equal(t, AWS{
   210  			Region: "ca-central-1",
   211  			ElastiCache: ElastiCache{
   212  				ReplicationGroupID:       "my-redis-cluster",
   213  				TransitEncryptionEnabled: true,
   214  				EndpointType:             "configuration",
   215  			},
   216  		}, database.GetAWS())
   217  		require.True(t, database.IsElastiCache())
   218  		require.True(t, database.IsAWSHosted())
   219  		require.True(t, database.IsCloudHosted())
   220  	})
   221  
   222  	t.Run("invalid URI", func(t *testing.T) {
   223  		database, err := NewDatabaseV3(Metadata{
   224  			Name: "elasticache",
   225  		}, DatabaseSpecV3{
   226  			Protocol: "redis",
   227  			URI:      "some.endpoint.cache.amazonaws.com:6379",
   228  			AWS: AWS{
   229  				Region: "us-east-5",
   230  				ElastiCache: ElastiCache{
   231  					ReplicationGroupID: "some-id",
   232  				},
   233  			},
   234  		})
   235  
   236  		// A warning is logged, no error is returned, and AWS metadata is not
   237  		// updated.
   238  		require.NoError(t, err)
   239  		require.Equal(t, AWS{
   240  			Region: "us-east-5",
   241  			ElastiCache: ElastiCache{
   242  				ReplicationGroupID: "some-id",
   243  			},
   244  		}, database.GetAWS())
   245  	})
   246  }
   247  
   248  func TestDatabaseMemoryDBEndpoint(t *testing.T) {
   249  	t.Run("valid URI", func(t *testing.T) {
   250  		database, err := NewDatabaseV3(Metadata{
   251  			Name: "memorydb",
   252  		}, DatabaseSpecV3{
   253  			Protocol: "redis",
   254  			URI:      "clustercfg.my-memorydb.xxxxxx.memorydb.us-east-1.amazonaws.com:6379",
   255  		})
   256  
   257  		require.NoError(t, err)
   258  		require.Equal(t, AWS{
   259  			Region: "us-east-1",
   260  			MemoryDB: MemoryDB{
   261  				ClusterName:  "my-memorydb",
   262  				TLSEnabled:   true,
   263  				EndpointType: "cluster",
   264  			},
   265  		}, database.GetAWS())
   266  		require.True(t, database.IsMemoryDB())
   267  		require.True(t, database.IsAWSHosted())
   268  		require.True(t, database.IsCloudHosted())
   269  	})
   270  
   271  	t.Run("invalid URI", func(t *testing.T) {
   272  		database, err := NewDatabaseV3(Metadata{
   273  			Name: "memorydb",
   274  		}, DatabaseSpecV3{
   275  			Protocol: "redis",
   276  			URI:      "some.endpoint.memorydb.amazonaws.com:6379",
   277  			AWS: AWS{
   278  				Region: "us-east-5",
   279  				MemoryDB: MemoryDB{
   280  					ClusterName: "clustername",
   281  				},
   282  			},
   283  		})
   284  
   285  		// A warning is logged, no error is returned, and AWS metadata is not
   286  		// updated.
   287  		require.NoError(t, err)
   288  		require.Equal(t, AWS{
   289  			Region: "us-east-5",
   290  			MemoryDB: MemoryDB{
   291  				ClusterName: "clustername",
   292  			},
   293  		}, database.GetAWS())
   294  	})
   295  }
   296  
   297  func TestDatabaseAzureEndpoints(t *testing.T) {
   298  	t.Parallel()
   299  
   300  	tests := []struct {
   301  		name        string
   302  		spec        DatabaseSpecV3
   303  		expectError bool
   304  		expectAzure Azure
   305  	}{
   306  		{
   307  			name: "valid MySQL",
   308  			spec: DatabaseSpecV3{
   309  				Protocol: "mysql",
   310  				URI:      "example-mysql.mysql.database.azure.com:3306",
   311  			},
   312  			expectAzure: Azure{
   313  				Name: "example-mysql",
   314  			},
   315  		},
   316  		{
   317  			name: "valid PostgresSQL",
   318  			spec: DatabaseSpecV3{
   319  				Protocol: "postgres",
   320  				URI:      "example-postgres.postgres.database.azure.com:5432",
   321  			},
   322  			expectAzure: Azure{
   323  				Name: "example-postgres",
   324  			},
   325  		},
   326  		{
   327  			name: "invalid database endpoint",
   328  			spec: DatabaseSpecV3{
   329  				Protocol: "postgres",
   330  				URI:      "invalid.database.azure.com:5432",
   331  			},
   332  			expectError: true,
   333  		},
   334  		{
   335  			name: "valid Redis",
   336  			spec: DatabaseSpecV3{
   337  				Protocol: "redis",
   338  				URI:      "example-redis.redis.cache.windows.net:6380",
   339  				Azure: Azure{
   340  					ResourceID: "/subscriptions/sub-id/resourceGroups/group-name/providers/Microsoft.Cache/Redis/example-redis",
   341  				},
   342  			},
   343  			expectAzure: Azure{
   344  				Name:       "example-redis",
   345  				ResourceID: "/subscriptions/sub-id/resourceGroups/group-name/providers/Microsoft.Cache/Redis/example-redis",
   346  			},
   347  		},
   348  		{
   349  			name: "valid Redis Enterprise",
   350  			spec: DatabaseSpecV3{
   351  				Protocol: "redis",
   352  				URI:      "rediss://example-redis-enterprise.region.redisenterprise.cache.azure.net?mode=cluster",
   353  				Azure: Azure{
   354  					ResourceID: "/subscriptions/sub-id/resourceGroups/group-name/providers/Microsoft.Cache/redisEnterprise/example-redis-enterprise",
   355  				},
   356  			},
   357  			expectAzure: Azure{
   358  				Name:       "example-redis-enterprise",
   359  				ResourceID: "/subscriptions/sub-id/resourceGroups/group-name/providers/Microsoft.Cache/redisEnterprise/example-redis-enterprise",
   360  			},
   361  		},
   362  		{
   363  			name: "invalid Redis (missing resource ID)",
   364  			spec: DatabaseSpecV3{
   365  				Protocol: "redis",
   366  				URI:      "rediss://example-redis-enterprise.region.redisenterprise.cache.azure.net?mode=cluster",
   367  			},
   368  			expectError: true,
   369  		},
   370  		{
   371  			name: "invalid Redis (unknown format)",
   372  			spec: DatabaseSpecV3{
   373  				Protocol: "redis",
   374  				URI:      "rediss://bad-format.redisenterprise.cache.azure.net?mode=cluster",
   375  				Azure: Azure{
   376  					ResourceID: "/subscriptions/sub-id/resourceGroups/group-name/providers/Microsoft.Cache/redisEnterprise/bad-format",
   377  				},
   378  			},
   379  			expectError: true,
   380  		},
   381  	}
   382  
   383  	for _, test := range tests {
   384  		t.Run(test.name, func(t *testing.T) {
   385  			database, err := NewDatabaseV3(Metadata{
   386  				Name: "test",
   387  			}, test.spec)
   388  
   389  			if test.expectError {
   390  				require.Error(t, err)
   391  			} else {
   392  				require.NoError(t, err)
   393  				require.Equal(t, test.expectAzure, database.GetAzure())
   394  			}
   395  		})
   396  	}
   397  }
   398  
   399  func TestMySQLVersionValidation(t *testing.T) {
   400  	t.Parallel()
   401  
   402  	t.Run("correct config", func(t *testing.T) {
   403  		database, err := NewDatabaseV3(Metadata{
   404  			Name: "test",
   405  		}, DatabaseSpecV3{
   406  			Protocol: "mysql",
   407  			URI:      "localhost:5432",
   408  			MySQL: MySQLOptions{
   409  				ServerVersion: "8.0.18",
   410  			},
   411  		})
   412  		require.NoError(t, err)
   413  		require.Equal(t, "8.0.18", database.GetMySQLServerVersion())
   414  	})
   415  
   416  	t.Run("incorrect config - wrong protocol", func(t *testing.T) {
   417  		_, err := NewDatabaseV3(Metadata{
   418  			Name: "test",
   419  		}, DatabaseSpecV3{
   420  			Protocol: "Postgres",
   421  			URI:      "localhost:5432",
   422  			MySQL: MySQLOptions{
   423  				ServerVersion: "8.0.18",
   424  			},
   425  		})
   426  		require.Error(t, err)
   427  		require.Contains(t, err.Error(), "ServerVersion")
   428  	})
   429  }
   430  
   431  func TestMySQLServerVersion(t *testing.T) {
   432  	t.Parallel()
   433  
   434  	database, err := NewDatabaseV3(Metadata{
   435  		Name: "test",
   436  	}, DatabaseSpecV3{
   437  		Protocol: "mysql",
   438  		URI:      "localhost:5432",
   439  	})
   440  	require.NoError(t, err)
   441  
   442  	require.Equal(t, "", database.GetMySQLServerVersion())
   443  
   444  	database.SetMySQLServerVersion("8.0.1")
   445  	require.Equal(t, "8.0.1", database.GetMySQLServerVersion())
   446  }
   447  
   448  func TestCassandraAWSEndpoint(t *testing.T) {
   449  	t.Parallel()
   450  
   451  	t.Run("aws cassandra url from region", func(t *testing.T) {
   452  		database, err := NewDatabaseV3(Metadata{
   453  			Name: "test",
   454  		}, DatabaseSpecV3{
   455  			Protocol: "cassandra",
   456  			AWS: AWS{
   457  				Region:    "us-west-1",
   458  				AccountID: "123456789012",
   459  			},
   460  		})
   461  		require.NoError(t, err)
   462  		require.Equal(t, "cassandra.us-west-1.amazonaws.com:9142", database.GetURI())
   463  	})
   464  
   465  	t.Run("aws cassandra custom uri", func(t *testing.T) {
   466  		database, err := NewDatabaseV3(Metadata{
   467  			Name: "test",
   468  		}, DatabaseSpecV3{
   469  			Protocol: "cassandra",
   470  			URI:      "cassandra.us-west-1.amazonaws.com:9142",
   471  			AWS: AWS{
   472  				AccountID: "123456789012",
   473  			},
   474  		})
   475  		require.NoError(t, err)
   476  		require.Equal(t, "cassandra.us-west-1.amazonaws.com:9142", database.GetURI())
   477  		require.Equal(t, "us-west-1", database.GetAWS().Region)
   478  	})
   479  
   480  	t.Run("aws cassandra custom fips uri", func(t *testing.T) {
   481  		database, err := NewDatabaseV3(Metadata{
   482  			Name: "test",
   483  		}, DatabaseSpecV3{
   484  			Protocol: "cassandra",
   485  			URI:      "cassandra-fips.us-west-2.amazonaws.com:9142",
   486  			AWS: AWS{
   487  				AccountID: "123456789012",
   488  			},
   489  		})
   490  		require.NoError(t, err)
   491  		require.Equal(t, "cassandra-fips.us-west-2.amazonaws.com:9142", database.GetURI())
   492  		require.Equal(t, "us-west-2", database.GetAWS().Region)
   493  	})
   494  
   495  	t.Run("aws cassandra missing AccountID", func(t *testing.T) {
   496  		_, err := NewDatabaseV3(Metadata{
   497  			Name: "test",
   498  		}, DatabaseSpecV3{
   499  			Protocol: "cassandra",
   500  			URI:      "cassandra.us-west-1.amazonaws.com:9142",
   501  			AWS: AWS{
   502  				AccountID: "",
   503  			},
   504  		})
   505  		require.Error(t, err)
   506  	})
   507  }
   508  
   509  func TestDatabaseFromRedshiftServerlessEndpoint(t *testing.T) {
   510  	t.Parallel()
   511  
   512  	t.Run("workgroup", func(t *testing.T) {
   513  		database, err := NewDatabaseV3(Metadata{
   514  			Name: "test",
   515  		}, DatabaseSpecV3{
   516  			Protocol: "postgres",
   517  			URI:      "my-workgroup.123456789012.us-east-1.redshift-serverless.amazonaws.com:5439",
   518  		})
   519  		require.NoError(t, err)
   520  		require.Equal(t, AWS{
   521  			AccountID: "123456789012",
   522  			Region:    "us-east-1",
   523  			RedshiftServerless: RedshiftServerless{
   524  				WorkgroupName: "my-workgroup",
   525  			},
   526  		}, database.GetAWS())
   527  	})
   528  
   529  	t.Run("vpc endpoint", func(t *testing.T) {
   530  		database, err := NewDatabaseV3(Metadata{
   531  			Name: "test",
   532  		}, DatabaseSpecV3{
   533  			Protocol: "postgres",
   534  			URI:      "my-vpc-endpoint-xxxyyyzzz.123456789012.us-east-1.redshift-serverless.amazonaws.com:5439",
   535  			AWS: AWS{
   536  				RedshiftServerless: RedshiftServerless{
   537  					WorkgroupName: "my-workgroup",
   538  				},
   539  			},
   540  		})
   541  		require.NoError(t, err)
   542  		require.Equal(t, AWS{
   543  			AccountID: "123456789012",
   544  			Region:    "us-east-1",
   545  			RedshiftServerless: RedshiftServerless{
   546  				WorkgroupName: "my-workgroup",
   547  				EndpointName:  "my-vpc",
   548  			},
   549  		}, database.GetAWS())
   550  	})
   551  }
   552  
   553  func TestDatabaseSelfHosted(t *testing.T) {
   554  	t.Parallel()
   555  
   556  	tests := []struct {
   557  		name     string
   558  		inputURI string
   559  	}{
   560  		{
   561  			name:     "localhost",
   562  			inputURI: "localhost:5432",
   563  		},
   564  		{
   565  			name:     "ec2 hostname",
   566  			inputURI: "ec2-11-22-33-44.us-east-2.compute.amazonaws.com:5432",
   567  		},
   568  	}
   569  
   570  	for _, test := range tests {
   571  		t.Run(test.name, func(t *testing.T) {
   572  			database, err := NewDatabaseV3(Metadata{
   573  				Name: "self-hosted-localhost",
   574  			}, DatabaseSpecV3{
   575  				Protocol: "postgres",
   576  				URI:      test.inputURI,
   577  			})
   578  			require.NoError(t, err)
   579  			require.Equal(t, DatabaseTypeSelfHosted, database.GetType())
   580  			require.False(t, database.IsCloudHosted())
   581  		})
   582  	}
   583  }
   584  
   585  func TestDynamoDBConfig(t *testing.T) {
   586  	t.Parallel()
   587  
   588  	tests := []struct {
   589  		desc       string
   590  		uri        string
   591  		region     string
   592  		account    string
   593  		roleARN    string
   594  		externalID string
   595  		wantSpec   DatabaseSpecV3
   596  		wantErrMsg string
   597  	}{
   598  		{
   599  			desc:    "account and region and empty URI is correct",
   600  			region:  "us-west-1",
   601  			account: "123456789012",
   602  			wantSpec: DatabaseSpecV3{
   603  				URI: "aws://dynamodb.us-west-1.amazonaws.com",
   604  				AWS: AWS{
   605  					Region:    "us-west-1",
   606  					AccountID: "123456789012",
   607  				},
   608  			},
   609  		},
   610  		{
   611  			desc:       "account and region and assume role is correct",
   612  			region:     "us-west-1",
   613  			account:    "123456789012",
   614  			roleARN:    "arn:aws:iam::123456789012:role/DBDiscoverer",
   615  			externalID: "externalid123",
   616  			wantSpec: DatabaseSpecV3{
   617  				URI: "aws://dynamodb.us-west-1.amazonaws.com",
   618  				AWS: AWS{
   619  					Region:        "us-west-1",
   620  					AccountID:     "123456789012",
   621  					AssumeRoleARN: "arn:aws:iam::123456789012:role/DBDiscoverer",
   622  					ExternalID:    "externalid123",
   623  				},
   624  			},
   625  		},
   626  		{
   627  			desc:    "account and AWS URI and empty region is correct",
   628  			uri:     "dynamodb.us-west-1.amazonaws.com",
   629  			account: "123456789012",
   630  			wantSpec: DatabaseSpecV3{
   631  				URI: "dynamodb.us-west-1.amazonaws.com",
   632  				AWS: AWS{
   633  					Region:    "us-west-1",
   634  					AccountID: "123456789012",
   635  				},
   636  			},
   637  		},
   638  		{
   639  			desc:    "account and AWS streams dynamodb URI and empty region is correct",
   640  			uri:     "streams.dynamodb.us-west-1.amazonaws.com",
   641  			account: "123456789012",
   642  			wantSpec: DatabaseSpecV3{
   643  				URI: "streams.dynamodb.us-west-1.amazonaws.com",
   644  				AWS: AWS{
   645  					Region:    "us-west-1",
   646  					AccountID: "123456789012",
   647  				},
   648  			},
   649  		},
   650  		{
   651  			desc:    "account and AWS dax URI and empty region is correct",
   652  			uri:     "dax.us-west-1.amazonaws.com",
   653  			account: "123456789012",
   654  			wantSpec: DatabaseSpecV3{
   655  				URI: "dax.us-west-1.amazonaws.com",
   656  				AWS: AWS{
   657  					Region:    "us-west-1",
   658  					AccountID: "123456789012",
   659  				},
   660  			},
   661  		},
   662  		{
   663  			desc:    "account and region and matching AWS URI region is correct",
   664  			uri:     "dynamodb.us-west-1.amazonaws.com",
   665  			region:  "us-west-1",
   666  			account: "123456789012",
   667  			wantSpec: DatabaseSpecV3{
   668  				URI: "dynamodb.us-west-1.amazonaws.com",
   669  				AWS: AWS{
   670  					Region:    "us-west-1",
   671  					AccountID: "123456789012",
   672  				},
   673  			},
   674  		},
   675  		{
   676  			desc:    "account and region and custom URI is correct",
   677  			uri:     "localhost:8080",
   678  			region:  "us-west-1",
   679  			account: "123456789012",
   680  			wantSpec: DatabaseSpecV3{
   681  				URI: "localhost:8080",
   682  				AWS: AWS{
   683  					Region:    "us-west-1",
   684  					AccountID: "123456789012",
   685  				},
   686  			},
   687  		},
   688  		{
   689  			desc:       "configured external ID but not assume role is ok",
   690  			uri:        "localhost:8080",
   691  			region:     "us-west-1",
   692  			account:    "123456789012",
   693  			externalID: "externalid123",
   694  			wantSpec: DatabaseSpecV3{
   695  				URI: "localhost:8080",
   696  				AWS: AWS{
   697  					Region:     "us-west-1",
   698  					AccountID:  "123456789012",
   699  					ExternalID: "externalid123",
   700  				},
   701  			},
   702  		},
   703  		{
   704  			desc:       "region and different AWS URI region is an error",
   705  			uri:        "dynamodb.us-west-2.amazonaws.com",
   706  			region:     "us-west-1",
   707  			account:    "123456789012",
   708  			wantErrMsg: "does not match the configured URI",
   709  		},
   710  		{
   711  			desc:       "invalid AWS URI is an error",
   712  			uri:        "a.streams.dynamodb.us-west-1.amazonaws.com",
   713  			region:     "us-west-1",
   714  			account:    "123456789012",
   715  			wantErrMsg: "invalid DynamoDB endpoint",
   716  		},
   717  		{
   718  			desc:       "custom URI and missing region is an error",
   719  			uri:        "localhost:8080",
   720  			account:    "123456789012",
   721  			wantErrMsg: "region is empty",
   722  		},
   723  		{
   724  			desc:       "missing URI and missing region is an error",
   725  			account:    "123456789012",
   726  			wantErrMsg: "URI is empty",
   727  		},
   728  		{
   729  			desc:       "invalid AWS account ID is an error",
   730  			uri:        "localhost:8080",
   731  			region:     "us-west-1",
   732  			account:    "12345",
   733  			wantErrMsg: "must be 12-digit",
   734  		},
   735  		{
   736  			region:     "us-west-1",
   737  			desc:       "missing account id",
   738  			wantErrMsg: "account ID is empty",
   739  		},
   740  	}
   741  
   742  	for _, tt := range tests {
   743  		tt := tt
   744  		t.Run(tt.desc, func(t *testing.T) {
   745  			t.Parallel()
   746  			database, err := NewDatabaseV3(Metadata{
   747  				Name: "test",
   748  			}, DatabaseSpecV3{
   749  				Protocol: "dynamodb",
   750  				URI:      tt.uri,
   751  				AWS: AWS{
   752  					Region:        tt.region,
   753  					AccountID:     tt.account,
   754  					AssumeRoleARN: tt.roleARN,
   755  					ExternalID:    tt.externalID,
   756  				},
   757  			})
   758  			if tt.wantErrMsg != "" {
   759  				require.Error(t, err)
   760  				require.ErrorContains(t, err, tt.wantErrMsg)
   761  				return
   762  			}
   763  			require.NoError(t, err)
   764  			diff := cmp.Diff(tt.wantSpec, database.Spec, cmpopts.IgnoreFields(DatabaseSpecV3{}, "Protocol"))
   765  			require.Empty(t, diff)
   766  		})
   767  	}
   768  }
   769  
   770  func TestOpenSearchConfig(t *testing.T) {
   771  	t.Parallel()
   772  
   773  	tests := []struct {
   774  		desc       string
   775  		uri        string
   776  		region     string
   777  		account    string
   778  		wantSpec   DatabaseSpecV3
   779  		wantErrMsg string
   780  	}{
   781  		{
   782  			desc:       "missing account is an error",
   783  			uri:        "my-opensearch-instance-xxxxxx.us-west-2.amazonaws.com",
   784  			region:     "us-west-2",
   785  			account:    "",
   786  			wantErrMsg: "database \"test\" AWS account ID is empty",
   787  		},
   788  		{
   789  			desc:       "custom URI without region is an error",
   790  			uri:        "localhost:8080",
   791  			region:     "",
   792  			account:    "123456789012",
   793  			wantErrMsg: "database \"test\" AWS region is missing and cannot be derived from the URI \"localhost:8080\"",
   794  		},
   795  		{
   796  			desc:    "custom URI with region is correct",
   797  			uri:     "localhost:8080",
   798  			region:  "eu-central-1",
   799  			account: "123456789012",
   800  			wantSpec: DatabaseSpecV3{
   801  				Protocol: "opensearch",
   802  				URI:      "localhost:8080",
   803  				AWS: AWS{
   804  					Region:    "eu-central-1",
   805  					AccountID: "123456789012",
   806  				},
   807  			},
   808  		},
   809  		{
   810  			desc:       "AWS URI for wrong service",
   811  			uri:        "my-opensearch-instance-xxxxxx.eu-central-1.foobar.amazonaws.com",
   812  			region:     "eu-central-1",
   813  			account:    "123456789012",
   814  			wantErrMsg: "invalid OpenSearch endpoint \"my-opensearch-instance-xxxxxx.eu-central-1.foobar.amazonaws.com\", invalid service \"foobar\"",
   815  		},
   816  		{
   817  			desc:    "region is optional if it can be derived from URI",
   818  			uri:     "my-opensearch-instance-xxxxxx.eu-central-1.es.amazonaws.com",
   819  			region:  "",
   820  			account: "123456789012",
   821  			wantSpec: DatabaseSpecV3{
   822  				Protocol: "opensearch",
   823  				URI:      "my-opensearch-instance-xxxxxx.eu-central-1.es.amazonaws.com",
   824  				AWS: AWS{
   825  					Region:    "eu-central-1",
   826  					AccountID: "123456789012",
   827  				},
   828  			},
   829  		},
   830  		{
   831  			desc:       "URI-derived region must match explicit region",
   832  			uri:        "my-opensearch-instance-xxxxxx.eu-central-1.es.amazonaws.com",
   833  			region:     "eu-central-2",
   834  			account:    "123456789012",
   835  			wantErrMsg: "database \"test\" AWS region \"eu-central-2\" does not match the configured URI region \"eu-central-1\"",
   836  		},
   837  
   838  		{
   839  			desc:    "no error when full data provided and matches",
   840  			uri:     "my-opensearch-instance-xxxxxx.eu-central-1.es.amazonaws.com",
   841  			region:  "eu-central-1",
   842  			account: "123456789012",
   843  			wantSpec: DatabaseSpecV3{
   844  				Protocol: "opensearch",
   845  				URI:      "my-opensearch-instance-xxxxxx.eu-central-1.es.amazonaws.com",
   846  				AWS: AWS{
   847  					Region:    "eu-central-1",
   848  					AccountID: "123456789012",
   849  				},
   850  			},
   851  		},
   852  
   853  		{
   854  			desc:       "invalid AWS account ID is an error",
   855  			uri:        "localhost:8080",
   856  			region:     "us-west-1",
   857  			account:    "12345",
   858  			wantErrMsg: "must be 12-digit",
   859  		},
   860  	}
   861  
   862  	for _, tt := range tests {
   863  		tt := tt
   864  		t.Run(tt.desc, func(t *testing.T) {
   865  			t.Parallel()
   866  			database, err := NewDatabaseV3(Metadata{
   867  				Name: "test",
   868  			}, DatabaseSpecV3{
   869  				Protocol: "opensearch",
   870  				URI:      tt.uri,
   871  				AWS: AWS{
   872  					Region:    tt.region,
   873  					AccountID: tt.account,
   874  				},
   875  			})
   876  
   877  			if tt.wantErrMsg != "" {
   878  				require.Error(t, err)
   879  				require.ErrorContains(t, err, tt.wantErrMsg)
   880  				return
   881  			}
   882  
   883  			require.NoError(t, err)
   884  			require.True(t, database.IsOpenSearch())
   885  			require.Equal(t, tt.wantSpec, database.Spec)
   886  		})
   887  	}
   888  }
   889  
   890  func TestAWSIsEmpty(t *testing.T) {
   891  	t.Parallel()
   892  
   893  	tests := []struct {
   894  		name   string
   895  		input  AWS
   896  		assert require.BoolAssertionFunc
   897  	}{
   898  		{
   899  			name:   "true",
   900  			input:  AWS{},
   901  			assert: require.True,
   902  		},
   903  		{
   904  			name: "true with unrecognized bytes",
   905  			input: AWS{
   906  				XXX_unrecognized: []byte{66, 0},
   907  			},
   908  			assert: require.True,
   909  		},
   910  		{
   911  			name: "true with nested unrecognized bytes",
   912  			input: AWS{
   913  				MemoryDB: MemoryDB{
   914  					XXX_unrecognized: []byte{99, 0},
   915  				},
   916  			},
   917  			assert: require.True,
   918  		},
   919  		{
   920  			name: "false",
   921  			input: AWS{
   922  				Region: "us-west-1",
   923  			},
   924  			assert: require.False,
   925  		},
   926  	}
   927  
   928  	for _, test := range tests {
   929  		t.Run(test.name, func(t *testing.T) {
   930  			test.assert(t, test.input.IsEmpty())
   931  		})
   932  	}
   933  }
   934  
   935  func TestValidateDatabaseName(t *testing.T) {
   936  	t.Parallel()
   937  
   938  	tests := []struct {
   939  		name              string
   940  		dbName            string
   941  		expectErrContains string
   942  	}{
   943  		{
   944  			name:   "valid long name and uppercase chars",
   945  			dbName: strings.Repeat("aA", 100),
   946  		},
   947  		{
   948  			name:              "invalid trailing hyphen",
   949  			dbName:            "invalid-database-name-",
   950  			expectErrContains: `"invalid-database-name-" does not match regex`,
   951  		},
   952  		{
   953  			name:              "invalid first character",
   954  			dbName:            "1-invalid-database-name",
   955  			expectErrContains: `"1-invalid-database-name" does not match regex`,
   956  		},
   957  	}
   958  
   959  	for _, test := range tests {
   960  		t.Run(test.name, func(t *testing.T) {
   961  			err := ValidateDatabaseName(test.dbName)
   962  			if test.expectErrContains != "" {
   963  				require.Error(t, err)
   964  				require.ErrorContains(t, err, test.expectErrContains)
   965  				return
   966  			}
   967  			require.NoError(t, err)
   968  		})
   969  	}
   970  }
   971  
   972  func TestIAMPolicyStatusJSON(t *testing.T) {
   973  	t.Parallel()
   974  
   975  	status := IAMPolicyStatus_IAM_POLICY_STATUS_SUCCESS
   976  
   977  	marshaled, err := status.MarshalJSON()
   978  	require.NoError(t, err)
   979  	require.Equal(t, `"IAM_POLICY_STATUS_SUCCESS"`, string(marshaled))
   980  
   981  	data, err := json.Marshal("IAM_POLICY_STATUS_FAILED")
   982  	require.NoError(t, err)
   983  	require.NoError(t, status.UnmarshalJSON(data))
   984  	require.Equal(t, IAMPolicyStatus_IAM_POLICY_STATUS_FAILED, status)
   985  }