// Copyright (C) MongoDB, Inc. 2023-present. // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 package integration import ( "context" "os" "runtime" "testing" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/internal/assert" "go.mongodb.org/mongo-driver/v2/internal/assert/assertbson" "go.mongodb.org/mongo-driver/v2/internal/handshake" "go.mongodb.org/mongo-driver/v2/internal/integration/mtest" "go.mongodb.org/mongo-driver/v2/internal/require" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.mongodb.org/mongo-driver/v2/version" "go.mongodb.org/mongo-driver/v2/x/bsonx/bsoncore" "go.mongodb.org/mongo-driver/v2/x/mongo/driver/wiremessage" ) func TestHandshakeProse(t *testing.T) { mt := mtest.New(t) if len(os.Getenv("DOCKER_RUNNING")) > 0 { t.Skip("These tests gives different results when run in Docker due to extra environment data.") } opts := mtest.NewOptions(). CreateCollection(false). ClientType(mtest.Proxy) driverInfo := &options.DriverInfo{ Name: "outer-library-name", Version: "outer-library-version", Platform: "outer-library-platform", } // Reset the environment variables to avoid environment namespace // collision. t.Setenv("AWS_EXECUTION_ENV", "") t.Setenv("FUNCTIONS_WORKER_RUNTIME", "") t.Setenv("K_SERVICE", "") t.Setenv("VERCEL", "") t.Setenv("AWS_REGION", "") t.Setenv("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "") t.Setenv("FUNCTION_MEMORY_MB", "") t.Setenv("FUNCTION_TIMEOUT_SEC", "") t.Setenv("FUNCTION_REGION", "") t.Setenv("VERCEL_REGION", "") testCases := []struct { name string env map[string]string opts *options.ClientOptions want []byte }{ { name: "1. valid AWS", env: map[string]string{ "AWS_EXECUTION_ENV": "AWS_Lambda_java8", "AWS_REGION": "us-east-2", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ bson.E{Key: "name", Value: "aws.lambda"}, bson.E{Key: "memory_mb", Value: 1024}, bson.E{Key: "region", Value: "us-east-2"}, }}, }), }, { name: "2. valid Azure", env: map[string]string{ "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ bson.E{Key: "name", Value: "azure.func"}, }}, }), }, { name: "3. valid GCP", env: map[string]string{ "K_SERVICE": "servicename", "FUNCTION_MEMORY_MB": "1024", "FUNCTION_TIMEOUT_SEC": "60", "FUNCTION_REGION": "us-central1", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ bson.E{Key: "name", Value: "gcp.func"}, bson.E{Key: "memory_mb", Value: 1024}, bson.E{Key: "region", Value: "us-central1"}, bson.E{Key: "timeout_sec", Value: int32(60)}, }}, }), }, { name: "4. valid Vercel", env: map[string]string{ "VERCEL": "1", "VERCEL_REGION": "cdg1", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ bson.E{Key: "name", Value: "vercel"}, bson.E{Key: "region", Value: "cdg1"}, }}, }), }, { name: "5. invalid multiple providers", env: map[string]string{ "AWS_EXECUTION_ENV": "AWS_Lambda_java8", "FUNCTIONS_WORKER_RUNTIME": "node", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, }), }, { name: "6. invalid long string", env: map[string]string{ "AWS_EXECUTION_ENV": "AWS_Lambda_java8", "AWS_REGION": func() string { var s string for i := 0; i < 512; i++ { s += "a" } return s }(), }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ {Key: "name", Value: "aws.lambda"}, }}, }), }, { name: "7. invalid wrong types", env: map[string]string{ "AWS_EXECUTION_ENV": "AWS_Lambda_java8", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "big", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, {Key: "env", Value: bson.D{ {Key: "name", Value: "aws.lambda"}, }}, }), }, { name: "8. Invalid - AWS_EXECUTION_ENV does not start with \"AWS_Lambda_\"", env: map[string]string{ "AWS_EXECUTION_ENV": "EC2", }, opts: nil, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, }), }, { name: "driver info included", opts: options.Client().SetDriverInfo(driverInfo), want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|outer-library-name"}, {Key: "version", Value: version.Driver + "|outer-library-version"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|outer-library-platform"}, }), }, } for _, tc := range testCases { tc := tc // Avoid implicit memory aliasing in for loop. mt.RunOpts(tc.name, opts, func(mt *mtest.T) { for k, v := range tc.env { mt.Setenv(k, v) } if tc.opts != nil { mt.ResetClient(tc.opts) } // Ping the server to ensure the handshake has completed. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) firstMessage := mt.GetProxyCapture().TryNext() require.NotNil(mt, firstMessage, "expected to capture a proxied message") assert.True(mt, firstMessage.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, firstMessage.Sent.Command) assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } func TestLoadBalancedConnectionHandshake(t *testing.T) { mt := mtest.New(t) lbopts := mtest.NewOptions().ClientType(mtest.Proxy).Topologies( mtest.LoadBalanced) mt.RunOpts("LB connection handshake uses OP_MSG", lbopts, func(mt *mtest.T) { // Ping the server to ensure the handshake has completed. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) firstMessage := mt.GetProxyCapture().TryNext() require.NotNil(mt, firstMessage, "expected to capture a proxied message") // Per the specifications, if loadBalanced=true, drivers MUST use the hello // command for the initial handshake and use the OP_MSG protocol. assert.Equal(mt, "hello", firstMessage.CommandName) assert.Equal(mt, wiremessage.OpMsg, firstMessage.Sent.OpCode) }) opts := mtest.NewOptions().ClientType(mtest.Proxy).Topologies( mtest.ReplicaSet, mtest.Sharded, mtest.Single, mtest.ShardedReplicaSet) mt.RunOpts("non-LB connection handshake uses OP_QUERY", opts, func(mt *mtest.T) { // Ping the server to ensure the handshake has completed. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) firstMessage := mt.GetProxyCapture().TryNext() require.NotNil(mt, firstMessage, "expected to capture a proxied message") want := wiremessage.OpQuery hello := handshake.LegacyHello if os.Getenv("REQUIRE_API_VERSION") == "true" { hello = "hello" // If the server API version is requested, then we should use OP_MSG // regardless of the topology want = wiremessage.OpMsg } assert.Equal(mt, hello, firstMessage.CommandName, "expected first message to be a handshake") assert.Equal(mt, want, firstMessage.Sent.OpCode) }) } // Test 1: Test that the driver updates metadata // Test 2: Multiple Successive Metadata Updates // Test 3: Multiple Successive Metadata Updates with Duplicate Data func TestHandshakeProse_AppendMetadata_Test1_Test2_Test3(t *testing.T) { mt := mtest.New(t) initialDriverInfo := options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", } testCases := []struct { name string driverInfo options.DriverInfo want []byte // append initialDriverInfo using client.AppendDriverInfo instead of as a // client-level constructor. append bool }{ { name: "test1.1: append new driver info", driverInfo: options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: false, }, { name: "test1.2: append with no platform", driverInfo: options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: false, }, { name: "test1.3: append with no version", driverInfo: options.DriverInfo{ Name: "framework", Version: "", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: false, }, { name: "test1.4: append with name only", driverInfo: options.DriverInfo{ Name: "framework", Version: "", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: false, }, { name: "test2.1: append new driver info after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: true, }, { name: "test2.2: append with no platform after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test2.3: append with no version after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: true, }, { name: "test2.4: append with name only after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test3.1: same driver info after appending", driverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test3.2: same version and platform after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "1.2", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test3.3: same name and platform after appending", driverInfo: options.DriverInfo{ Name: "library", Version: "2.0", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test3.4: same name and version after appending", driverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: true, }, { name: "test3.5: same platform after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), append: true, }, { name: "test3.6: same version after appending", driverInfo: options.DriverInfo{ Name: "framework", Version: "1.2", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: true, }, { name: "test3.7: same name after appending", driverInfo: options.DriverInfo{ Name: "library", Version: "2.0", Platform: "Framework Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }), append: true, }, } for _, tc := range testCases { tc := tc // Avoid implicit memory aliasing in for loop. // Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. opts := mtest.NewOptions().CreateClient(false).ClientType(mtest.Proxy) mt.RunOpts(tc.name, opts, func(mt *mtest.T) { clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond) if !tc.append { clientOpts = clientOpts.SetDriverInfo(&initialDriverInfo) } mt.ResetClient(clientOpts) if tc.append { mt.Client.AppendDriverInfo(initialDriverInfo) } // Send a ping command to the server and verify that the command succeeded. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // Save intercepted `client` document as `initialClientMetadata`. initialClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, initialClientMetadata, "expected to capture a proxied message") assert.True(mt, initialClientMetadata.IsHandshake(), "expected first message to be a handshake") // Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) mt.Client.AppendDriverInfo(tc.driverInfo) // Drain the proxy mt.GetProxyCapture().Drain() // Send a ping command to the server and verify that the command succeeded. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // Capture the first message sent after appending driver info. gotMessage := mt.GetProxyCapture().TryNext() require.NotNil(mt, gotMessage, "expected to capture a proxied message") assert.True(mt, gotMessage.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, gotMessage.Sent.Command) assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } // Test 4: Multiple Metadata Updates with Duplicate Data. func TestHandshakeProse_AppendMetadata_MultipleUpdatesWithDuplicateFields(t *testing.T) { opts := mtest.NewOptions().ClientType(mtest.Proxy) mt := mtest.New(t, opts) clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond) // 1. Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. mt.ResetClient(clientOpts) originalDriverInfo := options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", } // 2. Append initial driver info using client.AppendDriverInfo. mt.Client.AppendDriverInfo(originalDriverInfo) // 3. Send a ping command to the server and verify that the command succeeded. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 4. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ Name: "framework", Version: "2.0", Platform: "Framework Platform", }) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 6. Send a ping command to the server and verify that the command succeeded. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 7. Save intercepted `client` document as `clientMetadata`. // // NOTE: The Go Driver statically defineds the expected client // metadata value to make the tests more reliable and prevent // false-positive assertion results. That deviates from the prose // test. want := mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2|2.0"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }) // 8. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 9. Append the original driver info again. mt.Client.AppendDriverInfo(originalDriverInfo) // 10. Send a ping command to the server and verify that the command succeeded. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 11. Save intercepted `client` document as `clientMetadata`. updatedClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) assertbson.EqualDocument(mt, want, clientMetadata) } // Test 5: Metadata is not appended if identical to initial metadata func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical(t *testing.T) { opts := mtest.NewOptions().ClientType(mtest.Proxy) mt := mtest.New(t, opts) originalDriverInfo := options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", } clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond). SetDriverInfo(&originalDriverInfo) // 1. Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. mt.ResetClient(clientOpts) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 2. Send a ping command to the server and verify that the command succeeded. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // NOTE: The Go Driver statically defineds the expected client // metadata value to make the tests more reliable and prevent // false-positive assertion results. That deviates from the prose // test. want := mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }) // 3. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 5. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", }) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 6. Send a ping command to the server and verify that the command succeeded. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 7. Save intercepted `client` document as `updatedClientMetadata`. updatedClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) assertbson.EqualDocument(mt, want, clientMetadata) } // Test 6: Metadata is not appended if identical to initial metadata (separated // by non-identical metadata) func TestHandshakeProse_AppendMetadata_NotAppendedIfIdentical_NonSequential(t *testing.T) { opts := mtest.NewOptions().ClientType(mtest.Proxy) mt := mtest.New(t, opts) originalDriverInfo := options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", } clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond). SetDriverInfo(&originalDriverInfo) // 1. Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. mt.ResetClient(clientOpts) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 2. Send a ping command to the server and verify that the command succeeded. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 3. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 4. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ Name: "framework", Version: "1.2", Platform: "Framework Platform", }) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 5. Send a ping command to the server and verify that the command succeeded. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 6. Save intercepted `client` document as `clientMetadata`. // // NOTE: The Go Driver statically defineds the expected client // metadata value to make the tests more reliable and prevent // false-positive assertion results. That deviates from the prose // test. want := mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library|framework"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform|Framework Platform"}, }) // 7. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 8. Append new driver info. mt.Client.AppendDriverInfo(options.DriverInfo{ Name: "library", Version: "1.2", Platform: "Library Platform", }) // Drain the proxy to ensure we only capture messages after appending. mt.GetProxyCapture().Drain() // 9. Send a `ping` command to the server and verify that the command // succeeds. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 10. Save intercepted `client` document as `updatedClientMetadata`. updatedClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) assertbson.EqualDocument(mt, want, clientMetadata) } // Test 7: Empty strings are considered unset when appending duplicate metadata. func TestHandshakeProse_AppendMetadata_EmptyStrings(t *testing.T) { mt := mtest.New(t) testCases := []struct { name string initialDriverInfo options.DriverInfo toAppendDriverInfo options.DriverInfo want []byte }{ { name: "name empty", initialDriverInfo: options.DriverInfo{ Name: "", Version: "1.2", Platform: "Library Platform", }, toAppendDriverInfo: options.DriverInfo{ Name: "", Version: "1.2", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), }, { name: "version empty", initialDriverInfo: options.DriverInfo{ Name: "library", Version: "", Platform: "Library Platform", }, toAppendDriverInfo: options.DriverInfo{ Name: "library", Version: "", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), }, { name: "platform empty", initialDriverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "", }, toAppendDriverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, }), }, } for _, tc := range testCases { tc := tc // Avoid implicit memory aliasing in for loop. // Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. opts := mtest.NewOptions().CreateClient(false).ClientType(mtest.Proxy) mt.RunOpts(tc.name, opts, func(mt *mtest.T) { // 1. Create a `MongoClient` instance. clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond) mt.ResetClient(clientOpts) // 2. Append the `DriverInfoOptions` from the selected test case from // the initial metadata section. mt.Client.AppendDriverInfo(tc.initialDriverInfo) mt.GetProxyCapture().Drain() // 3. Send a `ping` command to the server and verify that the command // succeeds. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 4. Save intercepted `client` document as `initialClientMetadata`. // // NOTE: The Go Driver statically defineds the expected client // metadata value to make the tests more reliable and prevent // false-positive assertion results. That deviates from the prose // test. // // See the "want" field in each test case for the expected client // metadata value. // 5. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 6. Append the `DriverInfoOptions` from the selected test case from // the appended metadata section. mt.Client.AppendDriverInfo(tc.toAppendDriverInfo) // Drain the proxy mt.GetProxyCapture().Drain() // 7. Send a `ping` command to the server and verify the command // succeeds. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // Capture the first message sent after appending driver info. updatedClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } // Test 8: Empty strings are considered unset when appending metadata identical // to initial metadata func TestHandshakeProse_AppendMetadata_EmptyStrings_InitializedClient(t *testing.T) { mt := mtest.New(t) testCases := []struct { name string initialDriverInfo options.DriverInfo toAppendDriverInfo options.DriverInfo want []byte }{ { name: "name empty", initialDriverInfo: options.DriverInfo{ Name: "", Version: "1.2", Platform: "Library Platform", }, toAppendDriverInfo: options.DriverInfo{ Name: "", Version: "1.2", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), }, { name: "version empty", initialDriverInfo: options.DriverInfo{ Name: "library", Version: "", Platform: "Library Platform", }, toAppendDriverInfo: options.DriverInfo{ Name: "library", Version: "", Platform: "Library Platform", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version() + "|Library Platform"}, }), }, { name: "platform empty", initialDriverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "", }, toAppendDriverInfo: options.DriverInfo{ Name: "library", Version: "1.2", Platform: "", }, want: mustMarshalBSON(bson.D{ {Key: "driver", Value: bson.D{ {Key: "name", Value: "mongo-go-driver|library"}, {Key: "version", Value: version.Driver + "|1.2"}, }}, {Key: "os", Value: bson.D{ {Key: "type", Value: runtime.GOOS}, {Key: "architecture", Value: runtime.GOARCH}, }}, {Key: "platform", Value: runtime.Version()}, }), }, } for _, tc := range testCases { tc := tc // Avoid implicit memory aliasing in for loop. // Create a top-level client that can be shared among sub-tests. This is // necessary to test appending driver info to an existing client. opts := mtest.NewOptions().CreateClient(false).ClientType(mtest.Proxy) mt.RunOpts(tc.name, opts, func(mt *mtest.T) { // 1. Create a `MongoClient` instance. clientOpts := options.Client(). // Set idle timeout to 1ms to force new connections to be created // throughout the lifetime of the test. SetMaxConnIdleTime(1 * time.Millisecond). SetDriverInfo(&tc.initialDriverInfo) mt.ResetClient(clientOpts) // 2. Send a `ping` command to the server and verify that the command // succeeds. err := mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 3. Save intercepted `client` document as `initialClientMetadata`. // // NOTE: The Go Driver statically defineds the expected client // metadata value to make the tests more reliable and prevent // false-positive assertion results. That deviates from the prose // test. // // See the "want" field in each test case for the expected client // metadata value. // 4. Wait 5ms for the connection to become idle. time.Sleep(5 * time.Millisecond) // 5. Append the `DriverInfoOptions` from the selected test case from // the appended metadata section. mt.Client.AppendDriverInfo(tc.toAppendDriverInfo) // Drain the proxy mt.GetProxyCapture().Drain() // 6. Send a `ping` command to the server and verify the command // succeeds. err = mt.Client.Ping(context.Background(), nil) require.NoError(mt, err, "Ping error: %v", err) // 7. Store the response as `updatedClientMetadata`. updatedClientMetadata := mt.GetProxyCapture().TryNext() require.NotNil(mt, updatedClientMetadata, "expected to capture a proxied message") assert.True(mt, updatedClientMetadata.IsHandshake(), "expected first message to be a handshake") // 8. Assert that `initialClientMetadata` is identical to `updatedClientMetadata`. clientMetadata := clientMetadataFromHandshake(mt, updatedClientMetadata.Sent.Command) assertbson.EqualDocument(mt, tc.want, clientMetadata) }) } } // mustMarshalBSON marshals a value to BSON. It panics if any error occurs. func mustMarshalBSON(val interface{}) []byte { bytes, err := bson.Marshal(val) if err != nil { panic(err) } return bytes } // clientMetadataFromHandshake returns the BSON document from the "client" field // of the command document. func clientMetadataFromHandshake(mt *mtest.T, cmd bsoncore.Document) []byte { mt.Helper() client, err := cmd.LookupErr("client") require.NoError(mt, err, "no client field in handshake command document") clientDoc, ok := client.DocumentOK() require.True(mt, ok, "the client field is not a BSON document") return clientDoc }