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 }