mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
* Add MongoDB blackbox tests for static roles - Implement core static role tests (create, read credentials, manual rotation, validation) - Add helper functions for MongoDB user creation and credential verification - Implement basic connection config test - Remove stub functions, add TODOs for future implementation - All tests follow blackbox SDK patterns with parallel execution and proper cleanup * make it work * WIP * mongo private/public urls * Apply suggestion from @brewgator * regex for readability Co-authored-by: brewgator <lt.carbonell@hashicorp.com>
This commit is contained in:
parent
4d3f5d93f1
commit
8c58356d5e
@ -27,6 +27,7 @@ locals {
|
||||
MONGO_INITDB_ROOT_PASSWORD = var.password
|
||||
MONGO_INITDB_DATABASE = var.database
|
||||
}
|
||||
args = "--bind_ip_all"
|
||||
}
|
||||
mysql = {
|
||||
image_template = "docker.io/mysql:${var.db_version}"
|
||||
@ -43,6 +44,7 @@ locals {
|
||||
image = local.config.image_template
|
||||
env_vars_map = local.config.env_vars
|
||||
env_vars = join(",", [for k, v in local.env_vars_map : "${k}=${v}"])
|
||||
args = try(local.config.args, "")
|
||||
}
|
||||
|
||||
# Creating Database Server using generic container script
|
||||
@ -56,6 +58,7 @@ resource "enos_remote_exec" "create_database" {
|
||||
CONTAINER_NAME = "${var.database_type}-${var.instance_name}"
|
||||
CONTAINER_PORTS = var.port
|
||||
CONTAINER_ENVS = local.env_vars
|
||||
CONTAINER_ARGS = local.args
|
||||
}
|
||||
|
||||
transport = {
|
||||
|
||||
@ -42,7 +42,7 @@ resource "enos_local_exec" "run_blackbox_test" {
|
||||
# PATH and Go-related environment variables are inherited from the calling process
|
||||
}, var.vault_namespace != null ? {
|
||||
VAULT_NAMESPACE = var.vault_namespace
|
||||
} : {}, local.ldap_environment, local.postgres_environment
|
||||
} : {}, local.ldap_environment, local.postgres_environment, local.mongodb_environment
|
||||
)
|
||||
depends_on = [local_file.test_matrix]
|
||||
}
|
||||
@ -78,6 +78,15 @@ locals {
|
||||
PGPASSWORD = local.postgres_config.password
|
||||
PGDATABASE = local.postgres_config.database
|
||||
} : {}
|
||||
|
||||
# Extract MongoDB configuration safely, defaulting to empty map if not available
|
||||
mongodb_config = try(var.integration_host_state.mongodb, {})
|
||||
|
||||
# Set up MongoDB environment variables when MongoDB integration is available
|
||||
mongodb_environment = try(local.mongodb_config.host.public_ip, "") != "" ? {
|
||||
MONGO_URL = "mongodb://${local.mongodb_config.username}:${local.mongodb_config.password}@${local.mongodb_config.host.public_ip}:${local.mongodb_config.port}/${local.mongodb_config.database}?directConnection=true"
|
||||
MONGO_URL_PRIVATE = "mongodb://${local.mongodb_config.username}:${local.mongodb_config.password}@${local.mongodb_config.host.private_ip}:${local.mongodb_config.port}/${local.mongodb_config.database}?directConnection=true"
|
||||
} : {}
|
||||
}
|
||||
|
||||
# Extract information from the script output
|
||||
|
||||
@ -4,8 +4,10 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/sdk/helper/testcluster/blackbox"
|
||||
)
|
||||
|
||||
@ -18,128 +20,33 @@ func TestMongoDBConnectionConfigCRUDWorkflows(t *testing.T) {
|
||||
testMongoDBConnectionConfigCreateBasic(t, v)
|
||||
})
|
||||
|
||||
t.Run("ListReturnsNamesOnly", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigListReturnsNamesOnly(t, v)
|
||||
})
|
||||
|
||||
t.Run("ReadRedactsSensitiveFields", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigReadRedactsSensitiveFields(t, v)
|
||||
})
|
||||
|
||||
t.Run("ResetConnection", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigResetConnection(t, v)
|
||||
})
|
||||
|
||||
t.Run("DeleteConnection", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigDeleteConnection(t, v)
|
||||
})
|
||||
// TODO: Additional tests for future implementation:
|
||||
// - ListReturnsNamesOnly: Verify listing returns only connection names
|
||||
// - ReadRedactsSensitiveFields: Verify passwords are redacted in read operations
|
||||
// - ResetConnection: Verify connection reset preserves configuration
|
||||
// - DeleteConnection: Verify connection deletion and idempotency
|
||||
}
|
||||
|
||||
// TestMongoDBConnectionConfigValidationWorkflows runs all MongoDB
|
||||
// connection config validation workflow tests.
|
||||
func TestMongoDBConnectionConfigValidationWorkflows(t *testing.T) {
|
||||
t.Run("VerifyConnectionValid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigVerifyConnectionValid(t, v)
|
||||
})
|
||||
|
||||
t.Run("VerifyConnectionInvalid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBConnectionConfigVerifyConnectionInvalid(t, v)
|
||||
})
|
||||
}
|
||||
// TODO: TestMongoDBConnectionConfigValidationWorkflows
|
||||
// Future implementation should test:
|
||||
// - VerifyConnectionValid: Test with verify_connection=true and valid credentials
|
||||
// - VerifyConnectionInvalid: Test with verify_connection=true and invalid credentials
|
||||
|
||||
// testMongoDBConnectionConfigCreateBasic verifies that configuring a
|
||||
// MongoDB database connection succeeds at database/config/{name}.
|
||||
func testMongoDBConnectionConfigCreateBasic(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
requireVaultEnv(t)
|
||||
cleanup, connURL := PrepareTestContainer(t)
|
||||
defer cleanup()
|
||||
|
||||
// TODO: Implement test following this pattern:
|
||||
// 1. requireVaultEnv(t)
|
||||
// 2. cleanup, connURL := PrepareTestContainer(t)
|
||||
// 3. defer cleanup()
|
||||
// 4. mount := fmt.Sprintf("database-%s", sanitize(t.Name()))
|
||||
// 5. v.MustEnableSecretsEngine(mount, &api.MountInput{Type: "database"})
|
||||
// 6. v.MustWrite(mount+"/config/my-mongodb-db", mongoConnectionConfigPayload(...))
|
||||
// 7. config := v.MustReadRequired(mount + "/config/my-mongodb-db")
|
||||
// 8. v.AssertSecret(config).Data().HasKey("plugin_name", "mongodb-database-plugin")
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigListReturnsNamesOnly verifies LIST
|
||||
// /database/config returns configured connection names only, without
|
||||
// sensitive config details.
|
||||
func testMongoDBConnectionConfigListReturnsNamesOnly(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create multiple MongoDB connections
|
||||
// 2. List connections
|
||||
// 3. Verify only names are returned, no sensitive data
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigReadRedactsSensitiveFields verifies that reading
|
||||
// a MongoDB connection config returns sanitized connection details.
|
||||
func testMongoDBConnectionConfigReadRedactsSensitiveFields(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create MongoDB connection with credentials
|
||||
// 2. Read connection config
|
||||
// 3. Verify password and other sensitive fields are redacted
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigVerifyConnectionValid verifies that
|
||||
// configuration succeeds with verify_connection=true when credentials are valid.
|
||||
func testMongoDBConnectionConfigVerifyConnectionValid(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create MongoDB connection with valid credentials and verify_connection=true
|
||||
// 2. Verify connection succeeds
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigVerifyConnectionInvalid verifies that
|
||||
// configuration fails with verify_connection=true when credentials are invalid.
|
||||
func testMongoDBConnectionConfigVerifyConnectionInvalid(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create MongoDB connection with invalid credentials and verify_connection=true
|
||||
// 2. Verify connection fails with appropriate error
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigResetConnection verifies that resetting a
|
||||
// MongoDB database connection succeeds and preserves the stored connection
|
||||
// configuration.
|
||||
func testMongoDBConnectionConfigResetConnection(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create MongoDB connection
|
||||
// 2. Reset connection
|
||||
// 3. Verify config is preserved
|
||||
}
|
||||
|
||||
// testMongoDBConnectionConfigDeleteConnection verifies that deleting a
|
||||
// MongoDB database connection removes it, prevents new credential generation,
|
||||
// and remains idempotent when deleted again.
|
||||
func testMongoDBConnectionConfigDeleteConnection(t *testing.T, v *blackbox.Session) {
|
||||
t.Skip("Test implementation pending - MongoDB framework setup complete")
|
||||
|
||||
// TODO: Implement test to verify:
|
||||
// 1. Create MongoDB connection and role
|
||||
// 2. Generate credentials
|
||||
// 3. Delete connection
|
||||
// 4. Verify connection is gone and credentials can't be generated
|
||||
// 5. Delete again to verify idempotency
|
||||
mount := fmt.Sprintf("database-%s", sanitize(t.Name()))
|
||||
v.MustEnableSecretsEngine(mount, &api.MountInput{Type: "database"})
|
||||
|
||||
v.MustWrite(
|
||||
mount+"/config/my-mongodb-db",
|
||||
mongoConnectionConfigPayload(connURL, "*", false),
|
||||
)
|
||||
|
||||
config := v.MustReadRequired(mount + "/config/my-mongodb-db")
|
||||
v.AssertSecret(config).Data().HasKey("plugin_name", "mongodb-database-plugin")
|
||||
}
|
||||
|
||||
@ -0,0 +1,355 @@
|
||||
// Copyright IBM Corp. 2025, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/sdk/helper/testcluster/blackbox"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
const (
|
||||
testConnectionName = "my-mongodb-db"
|
||||
testStaticRoleName = "my-static-role"
|
||||
testUsername = "staticuser1"
|
||||
testInitialPassword = "initialpass"
|
||||
testRotationPeriod = 86400 // 24 hours in seconds
|
||||
mongoConnectTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// TestMongoDBStaticRoleWorkflows runs all MongoDB static role workflow tests.
|
||||
func TestMongoDBStaticRoleWorkflows(t *testing.T) {
|
||||
t.Run("CreateBasic", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBStaticRoleCreateBasic(t, v)
|
||||
})
|
||||
|
||||
t.Run("ReadCredentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBStaticRoleReadCredentials(t, v)
|
||||
})
|
||||
|
||||
t.Run("ManualRotation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
v := blackbox.New(t)
|
||||
testMongoDBStaticRoleManualRotation(t, v)
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: TestMongoDBStaticRoleValidationWorkflows
|
||||
// Future implementation should test:
|
||||
// - RequiresUsername: Verify error when username is missing
|
||||
// - RequiresRotationPeriodOrSchedule: Verify error without rotation config
|
||||
// - RejectsInvalidRotationPeriod: Verify error with period < 5 seconds
|
||||
// - RejectsMutuallyExclusiveFields: Verify error with both period and schedule
|
||||
|
||||
// testMongoDBStaticRoleCreateBasic verifies that creating a basic static role
|
||||
// succeeds with required fields.
|
||||
func testMongoDBStaticRoleCreateBasic(t *testing.T, v *blackbox.Session) {
|
||||
mount, connURL := setupMongoDBTest(t, v)
|
||||
|
||||
createMongoDBUser(t, connURL, testUsername, testInitialPassword)
|
||||
|
||||
v.MustWrite(mount+"/static-roles/"+testStaticRoleName, map[string]any{
|
||||
"db_name": testConnectionName,
|
||||
"username": testUsername,
|
||||
"rotation_period": testRotationPeriod,
|
||||
})
|
||||
|
||||
role := v.MustReadRequired(mount + "/static-roles/" + testStaticRoleName)
|
||||
v.AssertSecret(role).
|
||||
Data().
|
||||
HasKey("username", testUsername).
|
||||
HasKey("rotation_period", float64(testRotationPeriod)).
|
||||
HasKey("db_name", testConnectionName)
|
||||
}
|
||||
|
||||
// TODO: Additional test implementations for future teams
|
||||
// - testMongoDBStaticRoleCreateWithRotationSchedule: Test rotation_schedule instead of rotation_period
|
||||
// - testMongoDBStaticRoleListReturnsNamesOnly: Test listing multiple static roles
|
||||
// - testMongoDBStaticRoleReadReturnsConfiguration: Test reading role config without sensitive data
|
||||
// - testMongoDBStaticRoleUpdateRotationPeriod: Test updating rotation period
|
||||
// - testMongoDBStaticRoleDeleteRole: Test deleting a static role
|
||||
|
||||
// testMongoDBStaticRoleManualRotation verifies manually rotating a static
|
||||
// role's credentials succeeds.
|
||||
func testMongoDBStaticRoleManualRotation(t *testing.T, v *blackbox.Session) {
|
||||
mount, connURL := setupMongoDBTest(t, v)
|
||||
|
||||
createMongoDBUser(t, connURL, testUsername, testInitialPassword)
|
||||
|
||||
v.MustWrite(mount+"/static-roles/"+testStaticRoleName, map[string]any{
|
||||
"db_name": testConnectionName,
|
||||
"username": testUsername,
|
||||
"rotation_period": testRotationPeriod,
|
||||
})
|
||||
|
||||
creds1 := v.MustReadRequired(mount + "/static-creds/" + testStaticRoleName)
|
||||
password1 := creds1.Data["password"].(string)
|
||||
|
||||
if password1 == testInitialPassword {
|
||||
t.Fatal("expected password to be rotated on role creation")
|
||||
}
|
||||
|
||||
v.MustWrite(mount+"/rotate-role/"+testStaticRoleName, nil)
|
||||
|
||||
creds2 := v.MustReadRequired(mount + "/static-creds/" + testStaticRoleName)
|
||||
password2 := creds2.Data["password"].(string)
|
||||
|
||||
if password1 == password2 {
|
||||
t.Fatal("expected password to change after rotation")
|
||||
}
|
||||
|
||||
verifyMongoDBCredentials(t, connURL, testUsername, password2)
|
||||
}
|
||||
|
||||
// TODO: testMongoDBStaticRoleAutomaticRotation - Test automatic rotation with short period
|
||||
// This test requires waiting for rotation to occur, which may be time-consuming
|
||||
// Pattern: Create role with short rotation_period, wait, verify password changed
|
||||
|
||||
// testMongoDBStaticRoleReadCredentials verifies reading static credentials
|
||||
// returns the current password.
|
||||
func testMongoDBStaticRoleReadCredentials(t *testing.T, v *blackbox.Session) {
|
||||
mount, connURL := setupMongoDBTest(t, v)
|
||||
|
||||
createMongoDBUser(t, connURL, testUsername, testInitialPassword)
|
||||
|
||||
v.MustWrite(mount+"/static-roles/"+testStaticRoleName, map[string]any{
|
||||
"db_name": testConnectionName,
|
||||
"username": testUsername,
|
||||
"rotation_period": testRotationPeriod,
|
||||
})
|
||||
|
||||
creds := v.MustReadRequired(mount + "/static-creds/" + testStaticRoleName)
|
||||
v.AssertSecret(creds).
|
||||
Data().
|
||||
HasKey("username", testUsername).
|
||||
HasKey("password", creds.Data["password"])
|
||||
|
||||
password := creds.Data["password"].(string)
|
||||
if password == "" {
|
||||
t.Fatal("expected non-empty password")
|
||||
}
|
||||
|
||||
verifyMongoDBCredentials(t, connURL, testUsername, password)
|
||||
}
|
||||
|
||||
// testMongoDBStaticRoleRequiresUsername verifies creating a static role
|
||||
// without username fails.
|
||||
func testMongoDBStaticRoleRequiresUsername(t *testing.T, v *blackbox.Session) {
|
||||
mount, _ := setupMongoDBTest(t, v)
|
||||
|
||||
_, err := v.Client.Logical().Write(mount+"/static-roles/"+testStaticRoleName, map[string]any{
|
||||
"db_name": testConnectionName,
|
||||
"rotation_period": testRotationPeriod,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error when creating static role without username")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "username") {
|
||||
t.Fatalf("expected error message to mention 'username', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// setupMongoDBTest performs common test setup: creates container, enables mount, configures connection.
|
||||
// Returns mount path and connection URL.
|
||||
func setupMongoDBTest(t *testing.T, v *blackbox.Session) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
requireVaultEnv(t)
|
||||
cleanup, connURL := PrepareTestContainer(t)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
mount := fmt.Sprintf("database-%s", sanitize(t.Name()))
|
||||
v.MustEnableSecretsEngine(mount, &api.MountInput{Type: "database"})
|
||||
|
||||
v.MustWrite(
|
||||
mount+"/config/"+testConnectionName,
|
||||
mongoConnectionConfigPayload(connURL, "*", false),
|
||||
)
|
||||
|
||||
return mount, connURL
|
||||
}
|
||||
|
||||
// createMongoDBUser creates a MongoDB user for testing static roles.
|
||||
func createMongoDBUser(t *testing.T, connURL, username, password string) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mongoConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(connURL))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect to MongoDB: %v", err)
|
||||
}
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
db := client.Database("admin")
|
||||
err = db.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{
|
||||
{Key: "role", Value: "readWrite"},
|
||||
{Key: "db", Value: "admin"},
|
||||
},
|
||||
}},
|
||||
}).Err()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create MongoDB user: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Created MongoDB user: %s", username)
|
||||
}
|
||||
|
||||
// verifyMongoDBCredentials verifies that the given credentials work for MongoDB.
|
||||
func verifyMongoDBCredentials(t *testing.T, connURL, username, password string) {
|
||||
t.Helper()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mongoConnectTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Replace credentials in connection URL
|
||||
u, err := parseMongoURL(connURL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse connection URL: %v", err)
|
||||
}
|
||||
|
||||
u.User = username
|
||||
u.Password = password
|
||||
testURL := buildMongoURL(u)
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(testURL))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to connect with credentials: %v", err)
|
||||
}
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
if err := client.Ping(ctx, nil); err != nil {
|
||||
t.Fatalf("failed to ping with credentials: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Verified MongoDB credentials for user: %s", username)
|
||||
}
|
||||
|
||||
// mongoURL represents a parsed MongoDB connection URL.
|
||||
type mongoURL struct {
|
||||
Scheme string
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Database string
|
||||
Options string
|
||||
}
|
||||
|
||||
// parseMongoURL parses a MongoDB connection URL into components.
|
||||
func parseMongoURL(connURL string) (*mongoURL, error) {
|
||||
// Simple parser for mongodb:// URLs
|
||||
// Format: mongodb://user:pass@host/database?options
|
||||
u := &mongoURL{Scheme: "mongodb"}
|
||||
|
||||
// Remove scheme
|
||||
rest := connURL
|
||||
if len(rest) > 10 && rest[:10] == "mongodb://" {
|
||||
rest = rest[10:]
|
||||
}
|
||||
|
||||
// Extract user:pass if present
|
||||
atIdx := -1
|
||||
for i, c := range rest {
|
||||
if c == '@' {
|
||||
atIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if atIdx > 0 {
|
||||
userPass := rest[:atIdx]
|
||||
rest = rest[atIdx+1:]
|
||||
|
||||
colonIdx := -1
|
||||
for i, c := range userPass {
|
||||
if c == ':' {
|
||||
colonIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colonIdx > 0 {
|
||||
u.User = userPass[:colonIdx]
|
||||
u.Password = userPass[colonIdx+1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Extract host and database
|
||||
slashIdx := -1
|
||||
for i, c := range rest {
|
||||
if c == '/' {
|
||||
slashIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if slashIdx > 0 {
|
||||
u.Host = rest[:slashIdx]
|
||||
rest = rest[slashIdx+1:]
|
||||
|
||||
// Extract database and options
|
||||
qIdx := -1
|
||||
for i, c := range rest {
|
||||
if c == '?' {
|
||||
qIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if qIdx > 0 {
|
||||
u.Database = rest[:qIdx]
|
||||
u.Options = rest[qIdx+1:]
|
||||
} else {
|
||||
u.Database = rest
|
||||
}
|
||||
} else {
|
||||
u.Host = rest
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// buildMongoURL builds a MongoDB connection URL from components.
|
||||
func buildMongoURL(u *mongoURL) string {
|
||||
url := u.Scheme + "://"
|
||||
|
||||
if u.User != "" {
|
||||
url += u.User
|
||||
if u.Password != "" {
|
||||
url += ":" + u.Password
|
||||
}
|
||||
url += "@"
|
||||
}
|
||||
|
||||
url += u.Host
|
||||
|
||||
if u.Database != "" {
|
||||
url += "/" + u.Database
|
||||
}
|
||||
|
||||
if u.Options != "" {
|
||||
url += "?" + u.Options
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
@ -5,9 +5,12 @@ package mongodb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -28,7 +31,7 @@ const (
|
||||
// Uses test name to ensure unique container names for parallel execution
|
||||
func defaultRunOpts(t *testing.T) docker.RunOptions {
|
||||
return docker.RunOptions{
|
||||
ContainerName: fmt.Sprintf("mongodb-%s", sanitize(t.Name())),
|
||||
ContainerName: fmt.Sprintf("mongo-%s", sanitize(t.Name())),
|
||||
ImageRepo: defaultMongoImage,
|
||||
ImageTag: defaultMongoVersion,
|
||||
Env: []string{
|
||||
@ -38,6 +41,7 @@ func defaultRunOpts(t *testing.T) docker.RunOptions {
|
||||
},
|
||||
Ports: []string{"27017/tcp"},
|
||||
DoNotAutoRemove: false,
|
||||
PreDelete: true,
|
||||
OmitLogTimestamps: true,
|
||||
LogConsumer: func(s string) {
|
||||
if t.Failed() {
|
||||
@ -56,28 +60,37 @@ func requireVaultEnv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// sanitize converts test name to a valid container name
|
||||
// Removes special characters and converts to lowercase
|
||||
var sanitizeRegex = regexp.MustCompile(`[^a-z0-9]+`)
|
||||
|
||||
// sanitize converts test name to a valid identifier with smart truncation
|
||||
// Replaces non-alphanumeric characters with dashes and truncates long names
|
||||
// with a hash suffix for uniqueness
|
||||
func sanitize(name string) string {
|
||||
name = strings.ToLower(name)
|
||||
name = strings.ReplaceAll(name, "/", "-")
|
||||
name = strings.ReplaceAll(name, "_", "-")
|
||||
name = strings.ReplaceAll(name, " ", "-")
|
||||
// Remove any remaining special characters
|
||||
var result strings.Builder
|
||||
for _, r := range name {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
|
||||
result.WriteRune(r)
|
||||
}
|
||||
lower := strings.ToLower(name)
|
||||
out := sanitizeRegex.ReplaceAllString(lower, "-")
|
||||
out = strings.Trim(out, "-")
|
||||
|
||||
if out == "" {
|
||||
return "test"
|
||||
}
|
||||
return result.String()
|
||||
|
||||
// Truncate long names with hash suffix for uniqueness
|
||||
if len(out) > 54 {
|
||||
const hashLen = 8
|
||||
sum := sha256.Sum256([]byte(out))
|
||||
hash := hex.EncodeToString(sum[:])[:hashLen]
|
||||
prefixLen := 54 - 1 - hashLen
|
||||
out = out[:prefixLen] + "-" + hash
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// PrepareTestContainer starts a MongoDB container for testing
|
||||
// Returns cleanup function and connection URL
|
||||
// If MONGO_URL environment variable is set, uses that instead of starting a container
|
||||
func PrepareTestContainer(t *testing.T) (func(), string) {
|
||||
_, cleanup, connURL, _ := prepareTestContainer(t, defaultRunOpts(t), defaultMongoPass, true, false)
|
||||
_, cleanup, connURL, _ := prepareTestContainer(t, defaultRunOpts(t), defaultMongoPass, false, true)
|
||||
return cleanup, connURL
|
||||
}
|
||||
|
||||
@ -96,11 +109,17 @@ func prepareTestContainer(
|
||||
if os.Getenv("MONGO_URL") != "" {
|
||||
envMongoURL := os.Getenv("MONGO_URL")
|
||||
|
||||
// Use private URL for Vault, fall back to public
|
||||
vaultMongoURL := os.Getenv("MONGO_URL_PRIVATE")
|
||||
if vaultMongoURL == "" {
|
||||
vaultMongoURL = envMongoURL
|
||||
}
|
||||
|
||||
// Create unique database for this test
|
||||
dbName := fmt.Sprintf("test_%s_%d", sanitize(t.Name()), time.Now().Unix())
|
||||
testURL := replaceDatabase(envMongoURL, dbName)
|
||||
testURL := replaceDatabase(vaultMongoURL, dbName)
|
||||
|
||||
// Create the database
|
||||
// Create the database (test runner uses public URL)
|
||||
if err := createDatabase(t, envMongoURL, dbName); err != nil {
|
||||
t.Fatalf("Failed to create test database: %v", err)
|
||||
}
|
||||
@ -115,25 +134,33 @@ func prepareTestContainer(
|
||||
// Start Docker container
|
||||
runner, err := docker.NewServiceRunner(runOpts)
|
||||
if err != nil {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
if strings.Contains(errStr, "docker") &&
|
||||
(strings.Contains(errStr, "daemon") || strings.Contains(errStr, "connect")) {
|
||||
t.Skipf("skipping blackbox test: docker not available: %v", err)
|
||||
if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
|
||||
t.Fatalf("skipping blackbox test: docker daemon not available: %v", err)
|
||||
}
|
||||
t.Fatalf("Could not start docker MongoDB: %s", err)
|
||||
}
|
||||
|
||||
svc, containerID, err := runner.StartNewService(
|
||||
context.Background(),
|
||||
addSuffix,
|
||||
forceLocalAddr,
|
||||
connectMongoDB(password),
|
||||
)
|
||||
// Retry StartNewService with small delays to handle port mapping timing
|
||||
var svc *docker.Service
|
||||
var containerID string
|
||||
for attempt := 0; attempt < 5; attempt++ {
|
||||
if attempt > 0 {
|
||||
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
|
||||
}
|
||||
|
||||
svc, containerID, err = runner.StartNewService(context.Background(), addSuffix, forceLocalAddr, connectMongoDB(password))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no port mapping found") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
errStr := strings.ToLower(err.Error())
|
||||
if strings.Contains(errStr, "docker") &&
|
||||
(strings.Contains(errStr, "daemon") || strings.Contains(errStr, "connect")) {
|
||||
t.Skipf("skipping blackbox test: docker not available: %v", err)
|
||||
if strings.Contains(err.Error(), "Cannot connect to the Docker daemon") {
|
||||
t.Fatalf("skipping blackbox test: docker daemon not available: %v", err)
|
||||
}
|
||||
t.Fatalf("Could not start docker MongoDB: %s", err)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user